轉:http://blog.sina.com.cn/s/blog_51396f890100nd91.html
概要html
電影文件有不少基本的組成部分。首先,文件自己被稱爲容器Container,容器的類型決定了信息被存放在文件中的位置。AVI和Quicktime就是容器的例子。接着,你有一組流,例如,你常常有的是一個音頻流和一個視頻流。(一個流只是一種想像出來的詞語,用來表示一連串的經過時間來串連的數據元素)。在流中的數據元素被稱爲幀Frame。每一個流是由不一樣的編碼器來編碼生成的。編解碼器描述了實際的數據是如何被編碼Coded和解碼DECoded的,所以它的名字叫作CODEC。Divx和 MP3就是編解碼器的例子。接着從流中被讀出來的叫作包Packets。包是一段數據,它包含了一段能夠被解碼成方便咱們最後在應用程序中操做的原始幀的數據。根據咱們的目的,每一個包包含了完整的幀或者對於音頻來講是許多格式的完整幀。程序員
基本上來講,處理視頻和音頻流是很容易的:編程
10 從video.avi文件中打開視頻流video_stream數組 20 從視頻流中讀取包到幀中網絡 30 若是這個幀還不完整,跳到20多線程 40 對這個幀進行一些操做併發 50 跳回到20app |
在這個程序中使用ffmpeg來處理多種媒體是至關容易的,雖然不少程序可能在對幀進行操做的時候很是的複雜。所以在這篇指導中,咱們將打開一個文件,讀取裏面的視頻流,並且咱們對幀的操做將是把這個幀寫到一個PPM文件中。ide
打開文件
首先,來看一下咱們如何打開一個文件。經過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) |
咱們經過第一個參數來得到文件名。這個函數讀取文件的頭部而且把信息保存到咱們給的AVFormatContext結構體中。最後三個參數用來指定特殊的文件格式,緩衝大小和格式參數,但若是把它們設置爲空NULL或者0,libavformat將自動檢測這些參數。
這個函數只是檢測了文件的頭部,因此接着咱們須要檢查在文件中的流的信息:
// Retrieve stream information if(av_find_stream_info(pFormatCtx)<0) |
這個函數爲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(videoStream==-1) // 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) { } // Open codec if(avcodec_open(pCodecCtx, pCodec)<0) |
有些人可能會從舊的指導中記得有兩個關於這些代碼其它部分:添加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) |
即便咱們申請了一幀的內存,當轉換的時候,咱們仍然須要一個地方來放置原始的數據。咱們使用avpicture_get_size來得到咱們須要的大小,而後手工申請內存空間:
uint8_t *buffer; int numBytes; // Determine required buffer size and allocate buffer numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, 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, |
最後,咱們已經準備好來從流中讀取數據了。
讀取數據
咱們將要作的是經過讀取包來讀取整個視頻流,而後把它解碼成幀,最好後轉換格式而且保存。
int frameFinished; AVPacket packet; i=0; while(av_read_frame(pFormatCtx, &packet)>=0) { } |
這個循環過程是比較簡單的: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包含的要麼是完整的要麼是多種完整的幀。 |
如今咱們須要作的是讓SaveFrame函數能把RGB信息定稿到一個PPM格式的文件中。咱們將生成一個簡單的PPM格式文件,請相信,它是能夠工做的。
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) { } |
咱們作了一些標準的文件打開動做,而後寫入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文件。可使用一些電影文件來進行測試。
指導2:輸出到屏幕
SDL和視頻
爲了在屏幕上顯示,咱們將使用SDL.SDL是Simple Direct Layer的縮寫。它是一個出色的多媒體庫,適用於多平臺,而且被用在許多工程中。你能夠從它的官方網站的網址 http://www.libsdl.org/上來獲得這個庫的源代碼或者若是有可能的話你能夠直接下載開發包到你的操做系統中。按照這個指導,你將須要編譯這個庫。(剩下的幾個指導中也是同樣)
SDL庫中有許多種方式來在屏幕上繪製圖形,並且它有一個特殊的方式來在屏幕上顯示圖像――這種方式叫作YUV覆蓋。YUV(從技術上來說並不叫YUV而是叫作YCbCr)是一種相似於RGB方式的存儲原始圖像的格式。粗略的講,Y是亮度份量,U和V是色度份量。(這種格式比RGB複雜的多,由於不少的顏色信息被丟棄了,並且你能夠每2個Y有1個U和1個V)。SDL的YUV覆蓋使用一組原始的YUV數據而且在屏幕上顯示出他們。它能夠容許4種不一樣的 YUV格式,可是其中的YV12是最快的一種。還有一個叫作YUV420P的YUV格式,它和YV12是同樣的,除了U和V份量的位置被調換了之外。 420意味着它以4:2:0的比例進行了二次抽樣,基本上就意味着1個顏色份量對應着4個亮度份量。因此它的色度信息只有原來的1/4。這是一種節省帶寬的好方式,由於人眼感受不到這種變化。在名稱中的P表示這種格式是平面的――簡單的說就是Y,U和V份量分別在不一樣的數組中。FFMPEG能夠把圖像格式轉換爲YUV420P,可是如今不少視頻流的格式已是YUV420P的了或者能夠被很容易的轉換成YUV420P格式。
因而,咱們如今計劃把指導1中的SaveFrame()函數替換掉,讓它直接輸出咱們的幀到屏幕上去。但一開始咱們必須要先看一下如何使用SDL庫。首先咱們必需先包含SDL庫的頭文件而且初始化它。
#include <SDL.h> #include <SDL_thread.h> if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) { } |
SDL_Init()函數告訴了SDL庫,哪些特性咱們將要用到。固然SDL_GetError()是一個用來手工除錯的函數。
建立一個顯示
如今咱們須要在屏幕上的一個地方放上一些東西。在SDL中顯示圖像的基本區域叫作面surface。
SDL_Surface *screen; screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0); if(!screen) { } |
這就建立了一個給定高度和寬度的屏幕。下一個選項是屏幕的顏色深度――0表示使用和當前同樣的深度。(這個在OS X系統上不能正常工做,緣由請看源代碼)
如今咱們在屏幕上來建立一個YUV覆蓋以便於咱們輸入視頻上去:
SDL_Overlay bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, |
正如前面咱們所說的,咱們使用YV12來顯示圖像。
顯示圖像
前面那些都是很簡單的。如今咱們須要來顯示圖像。讓咱們看一下是如何來處理完成後的幀的。咱們將原來對RGB處理的方式,而且替換SaveFrame() 爲顯示到屏幕上的代碼。爲了顯示到屏幕上,咱們將先創建一個AVPicture結構體而且設置其數據指針和行尺寸來爲咱們的YUV覆蓋服務:
|
首先,咱們鎖定這個覆蓋,由於咱們將要去改寫它。這是一個避免之後發生問題的好習慣。正如前面所示的,這個AVPicture結構體有一個數據指針指向一個有4個元素的指針數據。因爲咱們處理的是YUV420P,因此咱們只須要3個通道即只要三組數據。其它的格式可能須要第四個指針來表示alpha通道或者其它參數。行尺寸正如它的名字表示的意義同樣。在YUV覆蓋中相同功能的結構體是像素pixel和程度pitch。(程度pitch是在SDL裏用來表示指定行數據寬度的值)。因此咱們如今作的是讓咱們的覆蓋中的pict.data中的三個指針有一個指向必要的空間的地址。相似的,咱們能夠直接從覆蓋中獲得行尺寸信息。像前面同樣咱們使用img_convert來把格式轉換成PIX_FMT_YUV420P。
繪製圖像
但咱們仍然須要告訴SDL如何來實際顯示咱們給的數據。咱們也會傳遞一個代表電影位置、寬度、高度和縮放大小的矩形參數給SDL的函數。這樣,SDL爲咱們作縮放而且它能夠經過顯卡的幫忙來進行快速縮放。
SDL_Rect rect; |
如今咱們的視頻顯示出來了!
讓咱們再花一點時間來看一下SDL的特性:它的事件驅動系統。SDL被設置成當你在SDL中點擊或者移動鼠標或者向它發送一個信號它都將產生一個事件的驅動方式。若是你的程序想要處理用戶輸入的話,它就會檢測這些事件。你的程序也能夠產生事件而且傳遞給SDL事件系統。當使用SDL進行多線程編程的時候,這至關有用,這方面代碼咱們能夠在指導4中看到。在這個程序中,咱們將在處理完包之後就當即輪詢事件。如今而言,咱們將處理SDL_QUIT事件以便於咱們退出:
SDL_Event |
讓咱們去掉舊的冗餘代碼,開始編譯。若是你使用的是Linux或者其變體,使用SDL庫進行編譯的最好方式爲:
gcc -o tutorial02 tutorial02.c -lavutil -lavformat -lavcodec -lz -lm \ `sdl-config --cflags --libs` |
這裏的sdl-config命令會打印出用於gcc編譯的包含正確SDL庫的適當參數。爲了進行編譯,在你本身的平臺你可能須要作的有點不一樣:請查閱一下SDL文檔中關於你的系統的那部分。一旦能夠編譯,就立刻運行它。
當運行這個程序的時候會發生什麼呢?電影簡直跑瘋了!實際上,咱們只是以咱們能從文件中解碼幀的最快速度顯示了全部的電影的幀。如今咱們沒有任何代碼來計算出咱們何時須要顯示電影的幀。最後(在指導5),咱們將花足夠的時間來探討同步問題。但一開始咱們會先忽略這個,由於咱們有更加劇要的事情要處理:音頻!
指導3:播放聲音
如今咱們要來播放聲音。SDL也爲咱們準備了輸出聲音的方法。函數SDL_OpenAudio()自己就是用來打開聲音設備的。它使用一個叫作SDL_AudioSpec結構體做爲參數,這個結構體中包含了咱們將要輸出的音頻的全部信息。
在咱們展現如何創建以前,讓咱們先解釋一下電腦是如何處理音頻的。數字音頻是由一長串的樣本流組成的。每一個樣本表示聲音波形中的一個值。聲音按照一個特定的採樣率來進行錄製,採樣率表示以多快的速度來播放這段樣本流,它的表示方式爲每秒多少次採樣。例如22050和44100的採樣率就是電臺和CD經常使用的採樣率。此外,大多音頻有不僅一個通道來表示立體聲或者環繞。例如,若是採樣是立體聲,那麼每次的採樣數就爲2個。當咱們從一個電影文件中等到數據的時候,咱們不知道咱們將獲得多少個樣本,可是ffmpeg將不會給咱們部分的樣本――這意味着它將不會把立體聲分割開來。
SDL播放聲音的方式是這樣的:你先設置聲音的選項:採樣率(在SDL的結構體中被叫作freq的表示頻率frequency),聲音通道數和其它的參數,而後咱們設置一個回調函數和一些用戶數據userdata。當開始播放音頻的時候,SDL將不斷地調用這個回調函數而且要求它來向聲音緩衝填入一個特定的數量的字節。當咱們把這些信息放到SDL_AudioSpec結構體中後,咱們調用函數SDL_OpenAudio()就會打開聲音設備而且給咱們送回另一個AudioSpec結構體。這個結構體是咱們實際上用到的--由於咱們不能保證獲得咱們所要求的。
設置音頻
目前先把講的記住,由於咱們實際上尚未任何關於聲音流的信息。讓咱們回過頭來看一下咱們的代碼,看咱們是如何找到視頻流的,一樣咱們也能夠找到聲音流。
// Find the first video stream videoStream=-1; audioStream=-1; for(i=0; i < pFormatCtx->nb_streams; i++) { } if(videoStream==-1) if(audioStream==-1) |
從這裏咱們能夠從描述流的AVCodecContext中獲得咱們想要的信息,就像咱們獲得視頻流的信息同樣。
AVCodecContext *aCodecCtx; aCodecCtx=pFormatCtx->streams[audioStream]->codec; |
包含在編解碼上下文中的全部信息正是咱們所須要的用來創建音頻的信息:
wanted_spec.freq = aCodecCtx->sample_rate; wanted_spec.format = AUDIO_S16SYS; wanted_spec.channels = aCodecCtx->channels; wanted_spec.silence = 0; wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE; wanted_spec.callback = audio_callback; wanted_spec.userdata = aCodecCtx; if(SDL_OpenAudio(&wanted_spec, &spec) < 0) { } |
讓咱們瀏覽一下這些:
最後,咱們使用SDL_OpenAudio函數來打開聲音。
若是你還記得前面的指導,咱們仍然須要打開聲音編解碼器自己。這是很顯然的。
AVCodec aCodec = avcodec_find_decoder(aCodecCtx->codec_id); if(!aCodec) { } avcodec_open(aCodecCtx, aCodec); |
隊列
嗯!如今咱們已經準備好從流中取出聲音信息。可是咱們如何來處理這些信息呢?咱們將會不斷地從文件中獲得這些包,但同時SDL也將調用回調函數。解決方法爲建立一個全局的結構體變量以便於咱們從文件中獲得的聲音包有地方存放同時也保證SDL中的聲音回調函數audio_callback能從這個地方獲得聲音數據。因此咱們要作的是建立一個包的隊列queue。在ffmpeg中有一個叫AVPacketList的結構體能夠幫助咱們,這個結構體實際是一串包的鏈表。下面就是咱們的隊列結構體:
typedef struct PacketQueue { } PacketQueue; |
首先,咱們應當指出nb_packets是與size不同的--size表示咱們從packet->size中獲得的字節數。你會注意到咱們有一個互斥量mutex和一個條件變量cond在結構體裏面。這是由於SDL是在一個獨立的線程中來進行音頻處理的。若是咱們沒有正確的鎖定這個隊列,咱們有可能把數據搞亂。咱們未來看一個這個隊列是如何來運行的。每個程序員應當知道如何來生成的一個隊列,可是咱們將把這部分也來討論從而能夠學習到SDL的函數。
一開始咱們先建立一個函數來初始化隊列:
void packet_queue_init(PacketQueue *q) { } |
接着咱們再作一個函數來給隊列中填入東西:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) { } |
函數SDL_LockMutex()鎖定隊列的互斥量以便於咱們向隊列中添加東西,而後函數SDL_CondSignal()經過咱們的條件變量爲一個接收函數(若是它在等待)發出一個信號來告訴它如今已經有數據了,接着就會解鎖互斥量並讓隊列能夠自由訪問。
下面是相應的接收函數。注意函數SDL_CondWait()是如何按照咱們的要求讓函數阻塞block的(例如一直等到隊列中有數據)。
int quit = 0; static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) { } |
正如你所看到的,咱們已經用一個無限循環包裝了這個函數以便於咱們想用阻塞的方式來獲得數據。咱們經過使用SDL中的函數SDL_CondWait()來避免無限循環。基本上,全部的CondWait只等待從SDL_CondSignal()函數(或者SDL_CondBroadcast()函數)中發出的信號,而後再繼續執行。然而,雖然看起來咱們陷入了咱們的互斥體中--若是咱們一直保持着這個鎖,咱們的函數將永遠沒法把數據放入到隊列中去!可是,SDL_CondWait()函數也爲咱們作了解鎖互斥量的動做而後才嘗試着在獲得信號後去從新鎖定它。
意外狀況
大家將會注意到咱們有一個全局變量quit,咱們用它來保證尚未設置程序退出的信號(SDL會自動處理TERM相似的信號)。不然,這個線程將不停地運行直到咱們使用kill -9來結束程序。FFMPEG一樣也提供了一個函數來進行回調並檢查咱們是否須要退出一些被阻塞的函數:這個函數就是url_set_interrupt_cb。
int decode_interrupt_cb(void) { } ... main() { ... ... ... |
固然,這僅僅是用來給ffmpeg中的阻塞狀況使用的,而不是SDL中的。咱們還必須要設置quit標誌爲1。
爲隊列提供包
剩下的咱們惟一須要爲隊列所作的事就是提供包了:
PacketQueue audioq; main() { ... |
函數SDL_PauseAudio()讓音頻設備最終開始工做。若是沒有當即供給足夠的數據,它會播放靜音。
咱們已經創建好咱們的隊列,如今咱們準備爲它提供包。先看一下咱們的讀取包的循環:
while(av_read_frame(pFormatCtx, &packet)>=0) { |
注意:咱們沒有在把包放到隊列裏的時候釋放它,咱們將在解碼後來釋放它。
取出包
如今,讓咱們最後讓聲音回調函數audio_callback來從隊列中取出包。回調函數的格式必需爲void callback(void *userdata, Uint8 *stream, int len),這裏的userdata就是咱們給到SDL的指針,stream是咱們要把聲音數據寫入的緩衝區指針,len是緩衝區的大小。下面就是代碼:
void audio_callback(void *userdata, Uint8 *stream, int len) { } |
這基本上是一個簡單的從另一個咱們將要寫的audio_decode_frame()函數中獲取數據的循環,這個循環把結果寫入到中間緩衝區,嘗試着向流中寫入len字節而且在咱們沒有足夠的數據的時候會獲取更多的數據或者當咱們有多餘數據的時候保存下來爲後面使用。這個audio_buf的大小爲 1.5倍的聲音幀的大小以便於有一個比較好的緩衝,這個聲音幀的大小是ffmpeg給出的。
最後解碼音頻
讓咱們看一下解碼器的真正部分:audio_decode_frame
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, } |
整個過程實際上從函數的尾部開始,在這裏咱們調用了packet_queue_get()函數。咱們從隊列中取出包,而且保存它的信息。而後,一旦咱們有了可使用的包,咱們就調用函數avcodec_decode_audio2(),它的功能就像它的姐妹函數 avcodec_decode_video()同樣,惟一的區別是它的一個包裏可能有不止一個聲音幀,因此你可能要調用不少次來解碼出包中全部的數據。同時也要記住進行指針audio_buf的強制轉換,由於SDL給出的是8位整型緩衝指針而ffmpeg給出的數據是16位的整型指針。你應該也會注意到 len1和data_size的不一樣,len1表示解碼使用的數據的在包中的大小,data_size表示實際返回的原始聲音數據的大小。
當咱們獲得一些數據的時候,咱們馬上返回來看一下是否仍然須要從隊列中獲得更加多的數據或者咱們已經完成了。若是咱們仍然有更加多的數據要處理,咱們把它保存到下一次。若是咱們完成了一個包的處理,咱們最後要釋放它。
就是這樣。咱們利用主的讀取隊列循環從文件獲得音頻並送到隊列中,而後被audio_callback函數從隊列中讀取並處理,最後把數據送給SDL,因而SDL就至關於咱們的聲卡。讓咱們繼續而且編譯:
gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lz -lm \ `sdl-config --cflags --libs` |
啊哈!視頻雖然仍是像原來那樣快,可是聲音能夠正常播放了。這是爲何呢?由於聲音信息中的採樣率--雖然咱們把聲音數據儘量快的填充到聲卡緩衝中,可是聲音設備卻會按照原來指定的採樣率來進行播放。
咱們幾乎已經準備好來開始同步音頻和視頻了,可是首先咱們須要的是一點程序的組織。用隊列的方式來組織和播放音頻在一個獨立的線程中工做的很好:它使得程序更加更加易於控制和模塊化。在咱們開始同步音視頻以前,咱們須要讓咱們的代碼更加容易處理。因此下次要講的是:建立一個線程。
--
這裏沒有什麼新東西,除了咱們給音頻和視頻隊列限定了一個最大值而且咱們添加一個檢測讀錯誤的函數。格式上下文裏面有一個叫作pb的 ByteIOContext類型結構體。這個結構體是用來保存一些低級的文件信息。函數url_ferror用來檢測結構體並發現是否有些讀取文件錯誤。
在循環之後,咱們的代碼是用等待其他的程序結束和提示咱們已經結束的。這些代碼是有益的,由於它指示出了如何驅動事件--後面咱們將顯示影像。
|
咱們使用SDL常量SDL_USEREVENT來從用戶事件中獲得值。第一個用戶事件的值應當是SDL_USEREVENT,下一個是 SDL_USEREVENT+1而且依此類推。在咱們的程序中FF_QUIT_EVENT被定義成SDL_USEREVENT+2。若是喜歡,咱們也能夠傳遞用戶數據,在這裏咱們傳遞的是大結構體的指針。最後咱們調用SDL_PushEvent()函數。在咱們的事件分支中,咱們只是像之前放入 SDL_QUIT_EVENT部分同樣。咱們將在本身的事件隊列中詳細討論,如今只是確保咱們正確放入了FF_QUIT_EVENT事件,咱們將在後面捕捉到它而且設置咱們的退出標誌quit。
獲得幀:video_thread
當咱們準備好解碼器後,咱們開始視頻線程。這個線程從視頻隊列中讀取包,把它解碼成視頻幀,而後調用queue_picture函數把處理好的幀放入到圖片隊列中:
int video_thread(void *arg) { } |
在這裏的不少函數應該很熟悉吧。咱們把avcodec_decode_video函數移到了這裏,替換了一些參數,例如:咱們把AVStream保存在咱們本身的大結構體中,因此咱們能夠從那裏獲得編解碼器的信息。咱們僅僅是不斷的從視頻隊列中取包一直到有人告訴咱們要中止或者出錯爲止。
把幀隊列化
讓咱們看一下保存解碼後的幀pFrame到圖像隊列中去的函數。由於咱們的圖像隊列是SDL的覆蓋的集合(基本上不用讓視頻顯示函數再作計算了),咱們須要把幀轉換成相應的格式。咱們保存到圖像隊列中的數據是咱們本身作的一個結構體。
typedef struct VideoPicture { } VideoPicture; |
咱們的大結構體有一個能夠保存這些緩衝區。然而,咱們須要本身來申請SDL_Overlay(注意:allocated標誌會指明咱們是否已經作了這個申請的動做與否)。
爲了使用這個隊列,咱們有兩個指針--寫入指針和讀取指針。咱們也要保證必定數量的實際數據在緩衝中。要寫入到隊列中,咱們先要等待緩衝清空以便於有位置來保存咱們的VideoPicture。而後咱們檢查看咱們是否已經申請到了一個能夠寫入覆蓋的索引號。若是沒有,咱們要申請一段空間。咱們也要從新申請緩衝若是窗口的大小已經改變。然而,爲了不被鎖定,滿是避免在這裏申請(我如今還不太清楚緣由;我相信是爲了不在其它線程中調用SDL覆蓋函數的緣由)。
int queue_picture(VideoState *is, AVFrame *pFrame) { |
這裏的事件機制與前面咱們想要退出的時候看到的同樣。咱們已經定義了事件FF_ALLOC_EVENT做爲SDL_USEREVENT。咱們把事件發到事件隊列中而後等待申請內存的函數設置好條件變量。
讓咱們來看一看如何來修改事件循環:
for(;;) { |
記住event.user.data1是咱們的大結構體。就這麼簡單。讓咱們看一下alloc_picture()函數:
void alloc_picture(void *userdata) { } |
你能夠看到咱們把SDL_CreateYUVOverlay函數從主循環中移到了這裏。這段代碼應該徹底能夠自我註釋。記住咱們把高度和寬度保存到VideoPicture結構體中由於咱們須要保存咱們的視頻的大小沒有由於某些緣由而改變。
好,咱們幾乎已經所有解決而且能夠申請到YUV覆蓋和準備好接收圖像。讓咱們回顧一下queue_picture並看一個拷貝幀到覆蓋的代碼。你應該能認出其中的一部分:
int queue_picture(VideoState *is, AVFrame *pFrame) { } |
這部分代碼和前面用到的同樣,主要是簡單的用咱們的幀來填充YUV覆蓋。最後一點只是簡單的給隊列加1。這個隊列在寫的時候會一直寫入到滿爲止,在讀的時候會一直讀空爲止。所以全部的都依賴於is->pictq_size值,這要求咱們必須要鎖定它。這裏咱們作的是增長寫指針(在必要的時候採用輪轉的方式),而後鎖定隊列而且增長尺寸。如今咱們的讀者函數將會知道隊列中有了更多的信息,當隊列滿的時候,咱們的寫入函數也會知道。
顯示視頻
這就是咱們的視頻線程。如今咱們看過了幾乎全部的線程除了一個--記得咱們調用schedule_refresh()函數嗎?讓咱們看一下實際中是如何作的:
static void schedule_refresh(VideoState *is, int delay) { } |
函數SDL_AddTimer()是SDL中的一個定時(特定的毫秒)執行用戶定義的回調函數(能夠帶一些參數user data)的簡單函數。咱們將用這個函數來定時刷新視頻--每次咱們調用這個函數的時候,它將設置一個定時器來觸發定時事件來把一幀從圖像隊列中顯示到屏幕上。
可是,讓咱們先觸發那個事件。
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) { } |
這裏向隊列中寫入了一個如今很熟悉的事件。FF_REFRESH_EVENT被定義成SDL_USEREVENT+1。要注意的一件事是當返回0的時候,SDL中止定時器,因而回調就不會再發生。
如今咱們產生了一個FF_REFRESH_EVENT事件,咱們須要在事件循環中處理它:
for(;;) { |
因而咱們就運行到了這個函數,在這個函數中會把數據從圖像隊列中取出:
void video_refresh_timer(void *userdata) { } |
如今,這只是一個極其簡單的函數:當隊列中有數據的時候,他從其中得到數據,爲下一幀設置定時器,調用video_display函數來真正顯示圖像到屏幕上,而後把隊列讀索引值加1,而且把隊列的尺寸size減1。你可能會注意到在這個函數中咱們並無真正對vp作一些實際的動做,緣由是這樣的:咱們將在後面處理。咱們將在後面同步音頻和視頻的時候用它來訪問時間信息。你會在這裏看到這個註釋信息「timing密碼here」。那裏咱們將討論何時顯示下一幀視頻,而後把相應的值寫入到schedule_refresh()函數中。如今咱們只是隨便寫入一個值80。從技術上來說,你能夠猜想並驗證這個值,而且爲每一個電影從新編譯程序,可是:1)過一段時間它會漂移;2)這種方式是很笨的。咱們將在後面來討論它。
咱們幾乎作完了;咱們僅僅剩了最後一件事:顯示視頻!下面就是video_display函數:
void video_display(VideoState *is) { } |
由於咱們的屏幕能夠是任意尺寸(咱們設置爲640x480而且用戶能夠本身來改變尺寸),咱們須要動態計算出咱們顯示的圖像的矩形大小。因此一開始咱們須要計算出電影的縱橫比aspect ratio,表示方式爲寬度除以高度。某些編解碼器會有奇數採樣縱橫比,只是簡單表示了一個像素或者一個採樣的寬度除以高度的比例。由於寬度和高度在咱們的編解碼器中是用像素爲單位的,因此實際的縱橫比與縱橫比乘以樣本縱橫比相同。某些編解碼器會顯示縱橫比爲0,這表示每一個像素的縱橫比爲1x1。而後咱們把電影縮放到適合屏幕的儘量大的尺寸。這裏的& -3表示與-3作與運算,其實是讓它們4字節對齊。而後咱們把電影移到中心位置,接着調用SDL_DisplayYUVOverlay()函數。
結果是什麼?咱們作完了嗎?嗯,咱們仍然要從新改寫聲音部分的代碼來使用新的VideoStruct結構體,可是那些只是嘗試着改變,你能夠看一下那些參考示例代碼。最後咱們要作的是改變ffmpeg提供的默認退出回調函數爲咱們的退出回調函數。
VideoState *global_video_state; int decode_interrupt_cb(void) { } |
咱們在主函數中爲大結構體設置了global_video_state。
這就是了!讓咱們編譯它:
gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lz -lm \ `sdl-config --cflags --libs` |
請享受一下沒有通過同步的電影!下次咱們將編譯一個能夠最終工做的電影播放器
標籤:雜談 |
指導5:同步視頻
如何同步視頻
前面整個的一段時間,咱們有了一個幾乎無用的電影播放器。固然,它能播放視頻,也能播放音頻,可是它還不能被稱爲一部電影。那麼咱們還要作什麼呢?
PTS和DTS
幸運的是,音頻和視頻流都有一些關於以多快速度和什麼時間來播放它們的信息在裏面。音頻流有采樣,視頻流有每秒的幀率。然而,若是咱們只是簡單的經過數幀和乘以幀率的方式來同步視頻,那麼就頗有可能會失去同步。因而做爲一種補充,在流中的包有種叫作DTS(解碼時間戳)和PTS(顯示時間戳)的機制。爲了這兩個參數,你須要瞭解電影存放的方式。像MPEG等格式,使用被叫作B幀(B表示雙向bidrectional)的方式。另外兩種幀被叫作I幀和P幀(I表示關鍵幀,P表示預測幀)。I幀包含了某個特定的完整圖像。P幀依賴於前面的I幀和P幀而且使用比較或者差分的方式來編碼。B幀與P幀有點相似,可是它是依賴於前面和後面的幀的信息的。這也就解釋了爲何咱們可能在調用avcodec_decode_video之後會得不到一幀圖像。
因此對於一個電影,幀是這樣來顯示的:I B B P。如今咱們須要在顯示B幀以前知道P幀中的信息。所以,幀可能會按照這樣的方式來存儲:IPBB。這就是爲何咱們會有一個解碼時間戳和一個顯示時間戳的緣由。解碼時間戳告訴咱們何時須要解碼,顯示時間戳告訴咱們何時須要顯示。因此,在這種狀況下,咱們的流能夠是這樣的:
Stream: I P B B |
一般PTS和DTS只有在流中有B幀的時候會不一樣。
當咱們調用av_read_frame()獲得一個包的時候,PTS和DTS的信息也會保存在包中。可是咱們真正想要的PTS是咱們剛剛解碼出來的原始幀的PTS,這樣咱們才能知道何時來顯示它。然而,咱們從avcodec_decode_video()函數中獲得的幀只是一個AVFrame,其中並無包含有用的PTS值(注意:AVFrame並無包含時間戳信息,但當咱們等到幀的時候並非咱們想要的樣子)。然而,ffmpeg從新排序包以便於被avcodec_decode_video()函數處理的包的DTS能夠老是與其返回的PTS相同。可是,另外的一個警告是:咱們也並非總能獲得這個信息。
不用擔憂,由於有另一種辦法能夠找到帖的PTS,咱們可讓程序本身來從新排序包。咱們保存一幀的第一個包的PTS:這將做爲整個這一幀的PTS。咱們能夠經過函數avcodec_decode_video()來計算出哪一個包是一幀的第一個包。怎樣實現呢?任什麼時候候當一個包開始一幀的時候,avcodec_decode_video()將調用一個函數來爲一幀申請一個緩衝。固然,ffmpeg容許咱們從新定義那個分配內存的函數。因此咱們製做了一個新的函數來保存一個包的時間戳。
固然,儘管那樣,咱們可能仍是得不到一個正確的時間戳。咱們將在後面處理這個問題。
同步
如今,知道了何時來顯示一個視頻幀真好,可是咱們怎樣來實際操做呢?這裏有個主意:當咱們顯示了一幀之後,咱們計算出下一幀顯示的時間。而後咱們簡單的設置一個新的定時器來。你可能會想,咱們檢查下一幀的PTS值而不是系統時鐘來看超時是否會到。這種方式能夠工做,可是有兩種狀況要處理。
首先,要知道下一個PTS是什麼。如今咱們能添加視頻速率到咱們的PTS中--太對了!然而,有些電影須要幀重複。這意味着咱們重複播放當前的幀。這將致使程序顯示下一幀太快了。因此咱們須要計算它們。
第二,正如程序如今這樣,視頻和音頻播放很歡快,一點也不受同步的影響。若是一切都工做得很好的話,咱們沒必要擔憂。可是,你的電腦並非最好的,不少視頻文件也不是無缺的。因此,咱們有三種選擇:同步音頻到視頻,同步視頻到音頻,或者都同步到外部時鐘(例如你的電腦時鐘)。從如今開始,咱們將同步視頻到音頻。
寫代碼:得到幀的時間戳
如今讓咱們到代碼中來作這些事情。咱們將須要爲咱們的大結構體添加一些成員,可是咱們會根據須要來作。首先,讓咱們看一下視頻線程。記住,在這裏咱們獲得瞭解碼線程輸出到隊列中的包。這裏咱們須要的是從avcodec_decode_video函數中獲得幀的時間戳。咱們討論的第一種方式是從上次處理的包中獲得DTS,這是很容易的:
|
若是咱們得不到PTS就把它設置爲0。
好,那是很容易的。可是咱們所說的若是包的DTS不能幫到咱們,咱們須要使用這一幀的第一個包的PTS。咱們經過讓ffmpeg使用咱們本身的申請幀程序來實現。下面的是函數的格式:
int get_buffer(struct AVCodecContext *c, AVFrame *pic); void release_buffer(struct AVCodecContext *c, AVFrame *pic); |
申請函數沒有告訴咱們關於包的任何事情,因此咱們要本身每次在獲得一個包的時候把PTS保存到一個全局變量中去。咱們本身以讀到它。而後,咱們把值保存到AVFrame結構體難理解的變量中去。因此一開始,這就是咱們的函數:
uint64_t global_video_pkt_pts = AV_NOPTS_VALUE; int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) { } void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) { } |
函數avcodec_default_get_buffer和avcodec_default_release_buffer是ffmpeg中默認的申請緩衝的函數。函數av_freep是一個內存管理函數,它不但把內存釋放並且把指針設置爲NULL。
如今到了咱們流打開的函數(stream_component_open),咱們添加這幾行來告訴ffmpeg如何去作:
|
如今咱們必需添加代碼來保存PTS到全局變量中,而後在須要的時候來使用它。咱們的代碼如今看起來應該是這樣子:
|
技術提示:你可能已經注意到咱們使用int64來表示PTS。這是由於PTS是以整型來保存的。這個值是一個時間戳至關於時間的度量,用來以流的 time_base爲單位進行時間度量。例如,若是一個流是24幀每秒,值爲42的PTS表示這一幀應該排在第42個幀的位置若是咱們每秒有24幀(這裏並不徹底正確)。
咱們能夠經過除以幀率來把這個值轉化爲秒。流中的time_base值表示1/framerate(對於固定幀率來講),因此獲得了以秒爲單位的PTS,咱們須要乘以time_base。
寫代碼:使用PTS來同步
如今咱們獲得了PTS。咱們要注意前面討論到的兩個同步問題。咱們將定義一個函數叫作synchronize_video,它能夠更新同步的PTS。這個函數也能最終處理咱們得不到PTS的狀況。同時咱們要知道下一幀的時間以便於正確設置刷新速率。咱們可使用內部的反映當前視頻已經播放時間的時鐘 video_clock來完成這個功能。咱們把這些值添加到大結構體中。
typedef struct VideoState { |
下面的是函數synchronize_video,它能夠很好的自我註釋:
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) { } |
你也會注意到咱們也計算了重複的幀。
如今讓咱們獲得正確的PTS而且使用queue_picture來隊列化幀,添加一個新的時間戳參數pts:
|
對於queue_picture來講惟一改變的事情就是咱們把時間戳值pts保存到VideoPicture結構體中,咱們咱們必需添加一個時間戳變量到結構體中而且添加一行代碼:
typedef struct VideoPicture { } int queue_picture(VideoState *is, AVFrame *pFrame, double pts) { |
如今咱們的圖像隊列中的全部圖像都有了正確的時間戳值,因此讓咱們看一下視頻刷新函數。你會記得上次咱們用80ms的刷新時間來欺騙它。那麼,如今咱們將會算出實際的值。
咱們的策略是經過簡單計算前一幀和如今這一幀的時間戳來預測出下一個時間戳的時間。同時,咱們須要同步視頻到音頻。咱們將設置一個音頻時間audio clock;一個內部值記錄了咱們正在播放的音頻的位置。就像從任意的mp3播放器中讀出來的數字同樣。既然咱們把視頻同步到音頻,視頻線程使用這個值來算出是否太快仍是太慢。
咱們將在後面來實現這些代碼;如今咱們假設咱們已經有一個能夠給咱們音頻時間的函數get_audio_clock。一旦咱們有了這個值,咱們在音頻和視頻失去同步的時候應該作些什麼呢?簡單而有點笨的辦法是試着用跳過正確幀或者其它的方式來解決。做爲一種替代的手段,咱們會調整下次刷新的值;若是時間戳太落後於音頻時間,咱們加倍計算延遲。若是時間戳太領先於音頻時間,咱們將盡量快的刷新。既然咱們有了調整過的時間和延遲,咱們將把它和咱們經過 frame_timer計算出來的時間進行比較。這個幀時間frame_timer將會統計出電影播放中全部的延時。換句話說,這個 frame_timer就是指咱們何時來顯示下一幀。咱們簡單的添加新的幀定時器延時,把它和電腦的系統時間進行比較,而後使用那個值來調度下一次刷新。這可能有點難以理解,因此請認真研究代碼:
void video_refresh_timer(void *userdata) { } |
咱們在這裏作了不少檢查:首先,咱們保證如今的時間戳和上一個時間戳之間的處以delay是有意義的。若是不是的話,咱們就猜想着用上次的延遲。接着,咱們有一個同步閾值,由於在同步的時候事情並不老是那麼完美的。在ffplay中使用0.01做爲它的值。咱們也保證閾值不會比時間戳之間的間隔短。最後,咱們把最小的刷新值設置爲10毫秒。
(這句不知道應該放在哪裏)事實上這裏咱們應該跳過這一幀,可是咱們不想爲此而煩惱。 |
咱們給大結構體添加了不少的變量,因此不要忘記檢查一下代碼。同時也不要忘記在函數streame_component_open中初始化幀時間frame_timer和前面的幀延遲frame delay:
|
同步:聲音時鐘
如今讓咱們看一下怎樣來獲得聲音時鐘。咱們能夠在聲音解碼函數audio_decode_frame中更新時鐘時間。如今,請記住咱們並非每次調用這個函數的時候都在處理新的包,因此有咱們要在兩個地方更新時鐘。第一個地方是咱們獲得新的包的時候:咱們簡單的設置聲音時鐘爲這個包的時間戳。而後,若是一個包裏有許多幀,咱們經過樣本數和採樣率來計算,因此當咱們獲得包的時候:
|
而後當咱們處理這個包的時候:
|
一點細節:臨時函數被改爲包含pts_ptr,因此要保證你已經改了那些。這時的pts_ptr是一個用來通知audio_callback函數當前聲音包的時間戳的指針。這將在下次用來同步聲音和視頻。
如今咱們能夠最後來實現咱們的get_audio_clock函數。它並不像獲得is->audio_clock值那樣簡單。注意咱們會在每次處理它的時候設置聲音時間戳,可是若是你看了audio_callback函數,它花費了時間來把數據從聲音包中移到咱們的輸出緩衝區中。這意味着咱們聲音時鐘中記錄的時間比實際的要早太多。因此咱們必需要檢查一下咱們還有多少沒有寫入。下面是完整的代碼:
double get_audio_clock(VideoState *is) { } |
你應該知道爲何這個函數能夠正常工做了;)
這就是了!讓咱們編譯它:
gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
最後,你可使用咱們本身的電影播放器來看電影了。下次咱們將看一下聲音同步,而後接下來的指導咱們會討論查詢
7
指導6:同步音頻
同步音頻
如今咱們已經有了一個比較像樣的播放器。因此讓咱們看一下還有哪些零碎的東西沒處理。上次,咱們掩飾了一點同步問題,也就是同步音頻到視頻而不是其它的同步方式。咱們將採用和視頻同樣的方式:作一個內部視頻時鐘來記錄視頻線程播放了多久,而後同步音頻到上面去。後面咱們也來看一下如何推而廣之把音頻和視頻都同步到外部時鐘。
生成一個視頻時鐘
如今咱們要生成一個相似於上次咱們的聲音時鐘的視頻時鐘:一個給出當前視頻播放時間的內部值。開始,你可能會想這和使用上一幀的時間戳來更新定時器同樣簡單。可是,不要忘了視頻幀之間的時間間隔是很長的,以毫秒爲計量的。解決辦法是跟蹤另一個值:咱們在設置上一幀時間戳的時候的時間值。因而當前視頻時間值就是PTS_of_last_frame + (current_time - time_elapsed_since_PTS_value_was_set)。這種解決方式與咱們在函數get_audio_clock中的方式很相似。
所在在咱們的大結構體中,咱們將放上一個雙精度浮點變量video_current_pts和一個64位寬整型變量video_current_pts_time。時鐘更新將被放在video_refresh_timer函數中。
void video_refresh_timer(void *userdata) { |
不要忘記在stream_component_open函數中初始化它:
|
如今咱們須要一種獲得信息的方式:
double get_video_clock(VideoState *is) { } |
提取時鐘
可是爲何要強制使用視頻時鐘呢?咱們更改視頻同步代碼以至於音頻和視頻不會試着去相互同步。想像一下咱們讓它像ffplay同樣有一個命令行參數。因此讓咱們抽象同樣這件事情:咱們將作一個新的封裝函數get_master_clock,用來檢測av_sync_type變量而後決定調用 get_audio_clock仍是get_video_clock或者其它的想使用的得到時鐘的函數。咱們甚至可使用電腦時鐘,這個函數咱們叫作 get_external_clock:
enum { }; #define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER double get_master_clock(VideoState *is) { } main() { ... ... } |
同步音頻
如今是最難的部分:同步音頻到視頻時鐘。咱們的策略是測量聲音的位置,把它與視頻時間比較而後算出咱們須要修正多少的樣本數,也就是說:咱們是否須要經過丟棄樣本的方式來加速播放仍是須要經過插值樣本的方式來放慢播放?
咱們將在每次處理聲音樣本的時候運行一個synchronize_audio的函數來正確的收縮或者擴展聲音樣本。然而,咱們不想在每次發現有誤差的時候都進行同步,由於這樣會使同步音頻多於視頻包。因此咱們爲函數synchronize_audio設置一個最小連續值來限定須要同步的時刻,這樣咱們就不會老是在調整了。固然,就像上次那樣,「失去同步」意味着聲音時鐘和視頻時鐘的差別大於咱們的閾值。
因此咱們將使用一個分數係數,叫c,因此如今能夠說咱們獲得了N個失去同步的聲音樣本。失去同步的數量可能會有不少變化,因此咱們要計算一下失去同步的長度的均值。例如,第一次調用的時候,顯示出來咱們失去同步的長度爲40ms,下次變爲50ms等等。可是咱們不會使用一個簡單的均值,由於距離如今最近的值比靠前的值要重要的多。因此咱們將使用一個分數系統,叫c,而後用這樣的公式來計算差別:diff_sum = new_diff + diff_sum*c。當咱們準備好去找平均差別的時候,咱們用簡單的計算方式:avg_diff = diff_sum * (1-c)。
注意:爲何會在這裏?這個公式看來很神奇!嗯,它基本上是一個使用等比級數的加權平均值。我不知道這是否有名字(我甚至查過維基百科!),可是若是想要更多的信息,這裏是一個解釋http://www.dranger.com/ffmpeg/weightedmean.html或者在http://www.dranger.com/ffmpeg/weightedmean.txt裏。 |
下面是咱們的函數:
int synchronize_audio(VideoState *is, short *samples, } |
如今咱們已經作得很好;咱們已經近似的知道如何用視頻或者其它的時鐘來調整音頻了。因此讓咱們來計算一下要在添加和砍掉多少樣本,而且如何在「Shrinking/expanding buffer code」部分來寫上代碼:
if(fabs(avg_diff) >= is->audio_diff_threshold) { |
記住audio_length * (sample_rate * # of channels * 2)就是audio_length秒時間的聲音的樣本數。因此,咱們想要的樣本數就是咱們根據聲音偏移添加或者減小後的聲音樣本數。咱們也能夠設置一個範圍來限定咱們一次進行修正的長度,由於若是咱們改變的太多,用戶會聽到刺耳的聲音。
修正樣本數
如今咱們要真正的修正一下聲音。你可能會注意到咱們的同步函數synchronize_audio返回了一個樣本數,這能夠告訴咱們有多少個字節被送到流中。因此咱們只要調整樣本數爲wanted_size就能夠了。這會讓樣本更小一些。可是若是咱們想讓它變大,咱們不能只是讓樣本大小變大,由於在緩衝區中沒有多餘的數據!因此咱們必需添加上去。可是咱們怎樣來添加呢?最笨的辦法就是試着來推算聲音,因此讓咱們用已有的數據在緩衝的末尾添加上最後的樣本。
if(wanted_size < samples_size) { } else if(wanted_size > samples_size) { } |
如今咱們經過這個函數返回的是樣本數。咱們如今要作的是使用它:
void audio_callback(void *userdata, Uint8 *stream, int len) { |
咱們要作的是把函數synchronize_audio插入進去。(同時,保證在初始化上面變量的時候檢查一下代碼,這些我沒有贅述)。
結束以前的最後一件事情:咱們須要添加一個if語句來保證咱們不會在視頻爲主時鐘的時候也來同步視頻。
if(is->av_sync_type != AV_SYNC_VIDEO_MASTER) { } |
添加後就能夠了。要保證整個程序中我沒有贅述的變量都被初始化過了。而後編譯它:
gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
而後你就能夠運行它了。
下次咱們要作的是讓你可讓電影快退和快進。
--
指導7:快進快退
處理快進快退命令
如今咱們來爲咱們的播放器加入一些快進和快退的功能,由於若是你不能全局搜索一部電影是很讓人討厭的。同時,這將告訴你av_seek_frame函數是多麼容易使用。
咱們將在電影播放中使用左方向鍵和右方向鍵來表示向後和向前一小段,使用向上和向下鍵來表示向前和向後一大段。這裏一小段是10秒,一大段是60秒。因此咱們須要設置咱們的主循環來捕捉鍵盤事件。然而當咱們捕捉到鍵盤事件後咱們不能直接調用av_seek_frame函數。咱們要主要的解碼線程 decode_thread的循環中作這些。因此,咱們要添加一些變量到大結構體中,用來包含新的跳轉位置和一些跳轉標誌:
|
如今讓咱們在主循環中捕捉按鍵:
|
爲了檢測按鍵,咱們先查了一下是否有SDL_KEYDOWN事件。而後咱們使用event.key.keysym.sym來判斷哪一個按鍵被按下。一旦咱們知道了如何來跳轉,咱們就來計算新的時間,方法爲把增長的時間值加到從函數get_master_clock中獲得的時間值上。而後咱們調用 stream_seek函數來設置seek_pos等變量。咱們把新的時間轉換成爲avcodec中的內部時間戳單位。在流中調用那個時間戳將使用幀而不是用秒來計算,公式爲seconds = frames * time_base(fps)。默認的avcodec值爲1,000,000fps(因此2秒的內部時間戳爲2,000,000)。在後面咱們來看一下爲何要把這個值進行一下轉換。
這就是咱們的stream_seek函數。請注意咱們設置了一個標誌爲後退服務:
void stream_seek(VideoState *is, int64_t pos, int rel) { } |
如今讓咱們看一下若是在decode_thread中實現跳轉。你會注意到咱們已經在源文件中標記了一個叫作「seek stuff goes here」的部分。如今咱們將把代碼寫在這裏。
跳轉是圍繞着av_seek_frame函數的。這個函數用到了一個格式上下文,一個流,一個時間戳和一組標記來做爲它的參數。這個函數將會跳轉到你所給的時間戳的位置。時間戳的單位是你傳遞給函數的流的時基time_base。然而,你並非必須要傳給它一個流(流能夠用-1來代替)。若是你這樣作了,時基time_base將會是avcodec中的內部時間戳單位,或者是1000000fps。這就是爲何咱們在設置seek_pos的時候會把位置乘以AV_TIME_BASER的緣由。
可是,若是給av_seek_frame函數的stream參數傳遞傳-1,你有時會在播放某些文件的時候遇到問題(比較少見),因此咱們會取文件中的第一個流而且把它傳遞到av_seek_frame函數。不要忘記咱們也要把時間戳timestamp的單位進行轉化。
if(is->seek_req) { |
這裏av_rescale_q(a,b,c)是用來把時間戳從一個時基調整到另一個時基時候用的函數。它基本的動做是計算a*b/c,可是這個函數仍是必需的,由於直接計算會有溢出的狀況發生。AV_TIME_BASE_Q是AV_TIME_BASE做爲分母后的版本。它們是很不相同的:AV_TIME_BASE * time_in_seconds = avcodec_timestamp而AV_TIME_BASE_Q * avcodec_timestamp = time_in_seconds(注意AV_TIME_BASE_Q其實是一個AVRational對象,因此你必需使用avcodec中特定的q函數來處理它)。
清空咱們的緩衝
咱們已經正確設定了跳轉位置,可是咱們尚未結束。記住咱們有一個堆放了不少包的隊列。既然咱們跳到了不一樣的位置,咱們必需把隊列中的內容清空不然電影是不會跳轉的。不只如此,avcodec也有它本身的內部緩衝,也須要每次被清空。
要實現這個,咱們須要首先寫一個函數來清空咱們的包隊列。而後咱們須要一種命令聲音和視頻線程來清空avcodec內部緩衝的辦法。咱們能夠在清空隊列後把特定的包放入到隊列中,而後當它們檢測到特定的包的時候,它們就會把本身的內部緩衝清空。
讓咱們開始寫清空函數。其實很簡單的,因此我直接把代碼寫在下面:
static void packet_queue_flush(PacketQueue *q) { } |
既然隊列已經清空了,咱們放入「清空包」。可是開始咱們要定義和建立這個包:
AVPacket flush_pkt; main() { } |
如今咱們把這個包放到隊列中:
} |
(這些代碼片斷是接着前面decode_thread中的代碼片斷的)咱們也須要修改packet_queue_put函數纔不至於直接簡單複製了這個包:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) { |
而後在聲音線程和視頻線程中,咱們在packet_queue_get後當即調用函數avcodec_flush_buffers:
|
上面的代碼片斷與視頻線程中的同樣,只要把「audio」換成「video」。
就這樣,讓咱們編譯咱們的播放器:
gcc -o tutorial07 tutorial07.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
試一下!咱們幾乎已經都作完了;下次咱們只要作一點小的改動就行了,那就是檢測ffmpeg提供的小的軟件縮放採樣。
指導8:軟件縮放
軟件縮放庫libswscale
近來ffmpeg添加了新的接口:libswscale來處理圖像縮放。
可是在前面咱們使用img_convert來把RGB轉換成YUV12,咱們如今使用新的接口。新接口更加標準和快速,並且我相信裏面有了MMX優化代碼。換句話說,它是作縮放更好的方式。
咱們將用來縮放的基本函數是sws_scale。但一開始,咱們必需創建一個SwsContext的概念。這將讓咱們進行想要的轉換,而後把它傳遞給 sws_scale函數。相似於在SQL中的預備階段或者是在Python中編譯的規則表達式regexp。要準備這個上下文,咱們使用 sws_getContext函數,它須要咱們源的寬度和高度,咱們想要的寬度和高度,源的格式和想要轉換成的格式,同時還有一些其它的參數和標誌。而後咱們像使用img_convert同樣來使用sws_scale函數,惟一不一樣的是咱們傳遞給的是SwsContext:
#include <ffmpeg/swscale.h> // include the header! int queue_picture(VideoState *is, AVFrame *pFrame, double pts) { |
咱們把新的縮放器放到了合適的位置。但願這會讓你知道libswscale能作什麼。
就這樣,咱們作完了!編譯咱們的播放器:
gcc -o tutorial08 tutorial08.c -lavutil -lavformat -lavcodec -lz -lm `sdl-config --cflags --libs` |
享受咱們用C寫的少於1000行的電影播放器吧。
固然,還有不少事情要作。
如今還要作什麼?
咱們已經有了一個能夠工做的播放器,可是它確定還不夠好。咱們作了不少,可是還有不少要添加的性能:
·錯誤處理。咱們代碼中的錯誤處理是無窮的,多處理一些會更好。
·暫停。咱們不能暫停電影,這是一個頗有用的功能。咱們能夠在大結構體中使用一個內部暫停變量,當用戶暫停的時候就設置它。而後咱們的音頻,視頻和解碼線程檢測到它後就再也不輸出任何東西。咱們也使用av_read_play來支持網絡。這很容易解釋,可是你卻不能明顯的計算出,因此把這個做爲一個家庭做業,若是你想嘗試的話。提示,能夠參考ffplay.c。
·支持視頻硬件特性。一個參考的例子,請參考Frame Grabbing在Martin的舊的指導中的相關部分。http://www.inb.uni-luebeck.de/~boehme/libavcodec_update.html
·按字節跳轉。若是你能夠按照字節而不是秒的方式來計算出跳轉位置,那麼對於像VOB文件同樣的有不連續時間戳的視頻文件來講,定位會更加精確。
·丟棄幀。若是視頻落後的太多,咱們應當把下一幀丟棄掉而不是設置一個短的刷新時間。
·支持網絡。如今的電影播放器還不能播放網絡流媒體。
·支持像YUV文件同樣的原始視頻流。若是咱們的播放器支持的話,由於咱們不能猜想出時基和大小,咱們應該加入一些參數來進行相應的設置。
·全屏。
·多種參數,例如:不一樣圖像格式;參考ffplay.c中的命令開關。
·其它事情,例如:在結構體中的音頻緩衝區應該對齊。