電影文件有很多基本的組成部分。首先,文件本身被稱爲容器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[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"(編解碼器上下文)的東西。這裏麪包含了流中所使用的關於編解碼器的所有信息,現在我們有了一個指向他的指針。但是我們必需要找到真正的編解碼器並且打開它:
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); // 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包含的要麼是完整的要麼是多種完整的幀。 |
現在我們需要做的是讓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文件。可以使用一些電影文件來進行測試。
ffmpeg文檔2
(2008-08-26 09:41:29)指導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)) { fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError()); exit(1); } |
SDL_Init()函數告訴了SDL庫,哪些特性我們將要用到。當然SDL_GetError()是一個用來手工除錯的函數。
創建一個顯示
現在我們需要在屏幕上的一個地方放上一些東西。在SDL中顯示圖像的基本區域叫做面surface。
SDL_Surface *screen; screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0); if(!screen) { fprintf(stderr, "SDL: could not set video mode - exiting\n"); exit(1); } |
這就創建了一個給定高度和寬度的屏幕。下一個選項是屏幕的顏色深度――0表示使用和當前一樣的深度。(這個在OS X系統上不能正常工作,原因請看源代碼)
現在我們在屏幕上來創建一個YUV覆蓋以便於我們輸入視頻上去:
SDL_Overlay *bmp; bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height, SDL_YV12_OVERLAY, screen); |
正如前面我們所說的,我們使用YV12來顯示圖像。
顯示圖像
前面那些都是很簡單的。現在我們需要來顯示圖像。讓我們看一下是如何來處理完成後的幀的。我們將原來對RGB處理的方式,並且替換SaveFrame() 爲顯示到屏幕上的代碼。爲了顯示到屏幕上,我們將先建立一個AVPicture結構體並且設置其數據指針和行尺寸來爲我們的YUV覆蓋服務:
if(frameFinished) { SDL_LockYUVOverlay(bmp); AVPicture pict; pict.data[0] = bmp->pixels[0]; pict.data[1] = bmp->pixels[2]; pict.data[2] = bmp->pixels[1]; pict.linesize[0] = bmp->pitches[0]; pict.linesize[1] = bmp->pitches[2]; pict.linesize[2] = bmp->pitches[1]; // Convert the image into YUV format that SDL uses img_convert(&pict, PIX_FMT_YUV420P, (AVPicture *)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); } |
首先,我們鎖定這個覆蓋,因爲我們將要去改寫它。這是一個避免以後發生問題的好習慣。正如前面所示的,這個AVPicture結構體有一個數據指針指向一個有4個元素的指針數據。由於我們處理的是YUV420P,所以我們只需要3個通道即只要三組數據。其它的格式可能需要第四個指針來表示alpha通道或者其它參數。行尺寸正如它的名字表示的意義一樣。在YUV覆蓋中相同功能的結構體是像素pixel和程度pitch。(程度pitch是在SDL裏用來表示指定行數據寬度的值)。所以我們現在做的是讓我們的覆蓋中的pict.data中的三個指針有一個指向必要的空間的地址。類似的,我們可以直接從覆蓋中得到行尺寸信息。像前面一樣我們使用img_convert來把格式轉換成PIX_FMT_YUV420P。
繪製圖像
但我們仍然需要告訴SDL如何來實際顯示我們給的數據。我們也會傳遞一個表明電影位置、寬度、高度和縮放大小的矩形參數給SDL的函數。這樣,SDL爲我們做縮放並且它可以通過顯卡的幫忙來進行快速縮放。
SDL_Rect rect; if(frameFinished) { // Convert the image into YUV format that SDL uses img_convert(&pict, PIX_FMT_YUV420P, (AVPicture *)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); SDL_UnlockYUVOverlay(bmp); rect.x = 0; rect.y = 0; rect.w = pCodecCtx->width; rect.h = pCodecCtx->height; SDL_DisplayYUVOverlay(bmp, &rect); } |
現在我們的視頻顯示出來了!
讓我們再花一點時間來看一下SDL的特性:它的事件驅動系統。SDL被設置成當你在SDL中點擊或者移動鼠標或者向它發送一個信號它都將產生一個事件的驅動方式。如果你的程序想要處理用戶輸入的話,它就會檢測這些事件。你的程序也可以產生事件並且傳遞給SDL事件系統。當使用SDL進行多線程編程的時候,這相當有用,這方面代碼我們可以在指導4中看到。在這個程序中,我們將在處理完包以後就立即輪詢事件。現在而言,我們將處理SDL_QUIT事件以便於我們退出:
SDL_Event event; av_free_packet(&packet); SDL_PollEvent(&event); switch(event.type) { case SDL_QUIT: SDL_Quit(); exit(0); break; default: break; } |
讓我們去掉舊的冗餘代碼,開始編譯。如果你使用的是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(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO && videoStream < 0) { videoStream=i; } if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_AUDIO && audioStream < 0) { audioStream=i; } } if(videoStream==-1) return -1; // Didn't find a video stream if(audioStream==-1) return -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) { fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError()); return -1; } |
讓我們瀏覽一下這些:
·freq 前面所講的採樣率
·format 告訴SDL我們將要給的格式。在“S16SYS”中的S表示有符號的signed,16表示每個樣本是16位長的,SYS表示大小頭的順序是與使用的系統相同的。這些格式是由avcodec_decode_audio2爲我們給出來的輸入音頻的格式。
·channels 聲音的通道數
·silence 這是用來表示靜音的值。因爲聲音採樣是有符號的,所以0當然就是這個值。
·samples 這是當我們想要更多聲音的時候,我們想讓SDL給出來的聲音緩衝區的尺寸。一個比較合適的值在512到8192之間;ffplay使用1024。
·callback 這個是我們的回調函數。我們後面將會詳細討論。
·userdata 這個是SDL供給回調函數運行的參數。我們將讓回調函數得到整個編解碼的上下文;你將在後面知道原因。
最後,我們使用SDL_OpenAudio函數來打開聲音。
如果你還記得前面的指導,我們仍然需要打開聲音編解碼器本身。這是很顯然的。
AVCodec *aCodec; aCodec = avcodec_find_decoder(aCodecCtx->codec_id); if(!aCodec) { fprintf(stderr, "Unsupported codec!\n"); return -1; } avcodec_open(aCodecCtx, aCodec); |
隊列
嗯!現在我們已經準備好從流中取出聲音信息。但是我們如何來處理這些信息呢?我們將會不斷地從文件中得到這些包,但同時SDL也將調用回調函數。解決方法爲創建一個全局的結構體變量以便於我們從文件中得到的聲音包有地方存放同時也保證SDL中的聲音回調函數audio_callback能從這個地方得到聲音數據。所以我們要做的是創建一個包的隊列queue。在ffmpeg中有一個叫AVPacketList的結構體可以幫助我們,這個結構體實際是一串包的鏈表。下面就是我們的隊列結構體:
typedef struct PacketQueue { AVPacketList *first_pkt, *last_pkt; int nb_packets; int size; SDL_mutex *mutex; SDL_cond *cond; } PacketQueue; |
首先,我們應當指出nb_packets是與size不一樣的--size表示我們從packet->size中得到的字節數。你會注意到我們有一個互斥量mutex和一個條件變量cond在結構體裏面。這是因爲SDL是在一個獨立的線程中來進行音頻處理的。如果我們沒有正確的鎖定這個隊列,我們有可能把數據搞亂。我們將來看一個這個隊列是如何來運行的。每一個程序員應當知道如何來生成的一個隊列,但是我們將把這部分也來討論從而可以學習到SDL的函數。
一開始我們先創建一個函數來初始化隊列:
void packet_queue_init(PacketQueue *q) { memset(q, 0, sizeof(PacketQueue)); q->mutex = SDL_CreateMutex(); q->cond = SDL_CreateCond(); } |
接着我們再做一個函數來給隊列中填入東西:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) { AVPacketList *pkt1; if(av_dup_packet(pkt) < 0) { return -1; } pkt1 = av_malloc(sizeof(AVPacketList)); if (!pkt1) return -1; pkt1->pkt = *pkt; pkt1->next = NULL; SDL_LockMutex(q->mutex); if (!q->last_pkt) q->first_pkt = pkt1; else q->last_pkt->next = pkt1; q->last_pkt = pkt1; q->nb_packets++; q->size += pkt1->pkt.size; SDL_CondSignal(q->cond); SDL_UnlockMutex(q->mutex); return 0; } |
函數SDL_LockMutex()鎖定隊列的互斥量以便於我們向隊列中添加東西,然後函數SDL_CondSignal()通過我們的條件變量爲一個接收函數(如果它在等待)發出一個信號來告訴它現在已經有數據了,接着就會解鎖互斥量並讓隊列可以自由訪問。
下面是相應的接收函數。注意函數SDL_CondWait()是如何按照我們的要求讓函數阻塞block的(例如一直等到隊列中有數據)。
int quit = 0; static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) { AVPacketList *pkt1; int ret; SDL_LockMutex(q->mutex); for(;;) { if(quit) { ret = -1; break; } pkt1 = q->first_pkt; if (pkt1) { q->first_pkt = pkt1->next; if (!q->first_pkt) q->last_pkt = NULL; q->nb_packets--; q->size -= pkt1->pkt.size; *pkt = pkt1->pkt; av_free(pkt1); ret = 1; break; } else if (!block) { ret = 0; break; } else { SDL_CondWait(q->cond, q->mutex); } } SDL_UnlockMutex(q->mutex); return ret; } |
正如你所看到的,我們已經用一個無限循環包裝了這個函數以便於我們想用阻塞的方式來得到數據。我們通過使用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) { return quit; } ... main() { ... url_set_interrupt_cb(decode_interrupt_cb); ... SDL_PollEvent(&event); switch(event.type) { case SDL_QUIT: quit = 1; ... |
當然,這僅僅是用來給ffmpeg中的阻塞情況使用的,而不是SDL中的。我們還必需要設置quit標誌爲1。
爲隊列提供包
剩下的我們唯一需要爲隊列所做的事就是提供包了:
PacketQueue audioq; main() { ... avcodec_open(aCodecCtx, aCodec); packet_queue_init(&audioq); SDL_PauseAudio(0); |
函數SDL_PauseAudio()讓音頻設備最終開始工作。如果沒有立即供給足夠的數據,它會播放靜音。
我們已經建立好我們的隊列,現在我們準備爲它提供包。先看一下我們的讀取包的循環:
while(av_read_frame(pFormatCtx, &packet)>=0) { // Is this a packet from the video stream? if(packet.stream_index==videoStream) { // Decode video frame .... } } else if(packet.stream_index==audioStream) { packet_queue_put(&audioq, &packet); } else { av_free_packet(&packet); } |
注意:我們沒有在把包放到隊列裏的時候釋放它,我們將在解碼後來釋放它。
取出包
現在,讓我們最後讓聲音回調函數audio_callback來從隊列中取出包。回調函數的格式必需爲void callback(void *userdata, Uint8 *stream, int len),這裏的userdata就是我們給到SDL的指針,stream是我們要把聲音數據寫入的緩衝區指針,len是緩衝區的大小。下面就是代碼:
void audio_callback(void *userdata, Uint8 *stream, int len) { AVCodecContext *aCodecCtx = (AVCodecContext *)userdata; int len1, audio_size; static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2]; static unsigned int audio_buf_size = 0; static unsigned int audio_buf_index = 0; while(len > 0) { if(audio_buf_index >= audio_buf_size) { audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf)); if(audio_size < 0) { audio_buf_size = 1024; memset(audio_buf, 0, audio_buf_size); } else { audio_buf_size = audio_size; } audio_buf_index = 0; } len1 = audio_buf_size - audio_buf_index; if(len1 > len) len1 = len; memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1); len -= len1; stream += len1; audio_buf_index += len1; } } |
這基本上是一個簡單的從另外一個我們將要寫的audio_decode_frame()函數中獲取數據的循環,這個循環把結果寫入到中間緩衝區,嘗試着向流中寫入len字節並且在我們沒有足夠的數據的時候會獲取更多的數據或者當我們有多餘數據的時候保存下來爲後面使用。這個audio_buf的大小爲 1.5倍的聲音幀的大小以便於有一個比較好的緩衝,這個聲音幀的大小是ffmpeg給出的。
最後解碼音頻
讓我們看一下解碼器的真正部分:audio_decode_frame
int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) { static AVPacket pkt; static uint8_t *audio_pkt_data = NULL; static int audio_pkt_size = 0; int len1, data_size; for(;;) { while(audio_pkt_size > 0) { data_size = buf_size; len1 = avcodec_decode_audio2(aCodecCtx, (int16_t *)audio_buf, &data_size, audio_pkt_data, audio_pkt_size); if(len1 < 0) { audio_pkt_size = 0; break; } audio_pkt_data += len1; audio_pkt_size -= len1; if(data_size <= 0) { continue; } return data_size; } if(pkt.data) av_free_packet(&pkt); if(quit) { return -1; } if(packet_queue_get(&audioq, &pkt, 1) < 0) { return -1; } audio_pkt_data = pkt.data; audio_pkt_size = pkt.size; } } |
整個過程實際上從函數的尾部開始,在這裏我們調用了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` |
啊哈!視頻雖然還是像原來那樣快,但是聲音可以正常播放了。這是爲什麼呢?因爲聲音信息中的採樣率--雖然我們把聲音數據儘可能快的填充到聲卡緩衝中,但是聲音設備卻會按照原來指定的採樣率來進行播放。
我們幾乎已經準備好來開始同步音頻和視頻了,但是首先我們需要的是一點程序的組織。用隊列的方式來組織和播放音頻在一個獨立的線程中工作的很好:它使得程序更加更加易於控制和模塊化。在我們開始同步音視頻之前,我們需要讓我們的代碼更加容易處理。所以下次要講的是:創建一個線程。
--
ffmpeg文檔4
(2008-08-26 23:18:50)} else {
break;
}
}
// Is this a packet from the video stream?
if(packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if(packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_free_packet(packet);
}
}
這裏沒有什麼新東西,除了我們給音頻和視頻隊列限定了一個最大值並且我們添加一個檢測讀錯誤的函數。格式上下文裏面有一個叫做pb的 ByteIOContext類型結構體。這個結構體是用來保存一些低級的文件信息。函數url_ferror用來檢測結構體並發現是否有些讀取文件錯誤。
在循環以後,我們的代碼是用等待其餘的程序結束和提示我們已經結束的。這些代碼是有益的,因爲它指示出了如何驅動事件--後面我們將顯示影像。
while(!is->quit) { SDL_Delay(100); } fail: if(1){ SDL_Event event; event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); } return 0; |
我們使用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) { VideoState *is = (VideoState *)arg; AVPacket pkt1, *packet = &pkt1; int len1, frameFinished; AVFrame *pFrame; pFrame = avcodec_alloc_frame(); for(;;) { if(packet_queue_get(&is->videoq, packet, 1) < 0) { // means we quit getting packets break; } // Decode video frame len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, packet->data, packet->size); // Did we get a video frame? if(frameFinished) { if(queue_picture(is, pFrame) < 0) { break; } } av_free_packet(packet); } av_free(pFrame); return 0; } |
在這裏的很多函數應該很熟悉吧。我們把avcodec_decode_video函數移到了這裏,替換了一些參數,例如:我們把AVStream保存在我們自己的大結構體中,所以我們可以從那裏得到編解碼器的信息。我們僅僅是不斷的從視頻隊列中取包一直到有人告訴我們要停止或者出錯爲止。
把幀隊列化
讓我們看一下保存解碼後的幀pFrame到圖像隊列中去的函數。因爲我們的圖像隊列是SDL的覆蓋的集合(基本上不用讓視頻顯示函數再做計算了),我們需要把幀轉換成相應的格式。我們保存到圖像隊列中的數據是我們自己做的一個結構體。
typedef struct VideoPicture { SDL_Overlay *bmp; int width, height; int allocated; } VideoPicture; |
我們的大結構體有一個可以保存這些緩衝區。然而,我們需要自己來申請SDL_Overlay(注意:allocated標誌會指明我們是否已經做了這個申請的動作與否)。
爲了使用這個隊列,我們有兩個指針--寫入指針和讀取指針。我們也要保證一定數量的實際數據在緩衝中。要寫入到隊列中,我們先要等待緩衝清空以便於有位置來保存我們的VideoPicture。然後我們檢查看我們是否已經申請到了一個可以寫入覆蓋的索引號。如果沒有,我們要申請一段空間。我們也要重新申請緩衝如果窗口的大小已經改變。然而,爲了避免被鎖定,盡是避免在這裏申請(我現在還不太清楚原因;我相信是爲了避免在其它線程中調用SDL覆蓋函數的原因)。
int queue_picture(VideoState *is, AVFrame *pFrame) { VideoPicture *vp; int dst_pix_fmt; AVPicture pict; SDL_LockMutex(is->pictq_mutex); while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit) { SDL_CondWait(is->pictq_cond, is->pictq_mutex); } SDL_UnlockMutex(is->pictq_mutex); if(is->quit) return -1; // windex is set to 0 initially vp = &is->pictq[is->pictq_windex]; if(!vp->bmp || vp->width != is->video_st->codec->width || vp->height != is->video_st->codec->height) { SDL_Event event; vp->allocated = 0; event.type = FF_ALLOC_EVENT; event.user.data1 = is; SDL_PushEvent(&event); SDL_LockMutex(is->pictq_mutex); while(!vp->allocated && !is->quit) { SDL_CondWait(is->pictq_cond, is->pictq_mutex); } SDL_UnlockMutex(is->pictq_mutex); if(is->quit) { return -1; } } |
這裏的事件機制與前面我們想要退出的時候看到的一樣。我們已經定義了事件FF_ALLOC_EVENT作爲SDL_USEREVENT。我們把事件發到事件隊列中然後等待申請內存的函數設置好條件變量。
讓我們來看一看如何來修改事件循環:
for(;;) { SDL_WaitEvent(&event); switch(event.type) { case FF_ALLOC_EVENT: alloc_picture(event.user.data1); break; |
記住event.user.data1是我們的大結構體。就這麼簡單。讓我們看一下alloc_picture()函數:
void alloc_picture(void *userdata) { VideoState *is = (VideoState *)userdata; VideoPicture *vp; vp = &is->pictq[is->pictq_windex]; if(vp->bmp) { // we already have one make another, bigger/smaller SDL_FreeYUVOverlay(vp->bmp); } // Allocate a place to put our YUV image on that screen vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width, is->video_st->codec->height, SDL_YV12_OVERLAY, screen); vp->width = is->video_st->codec->width; vp->height = is->video_st->codec->height; SDL_LockMutex(is->pictq_mutex); vp->allocated = 1; SDL_CondSignal(is->pictq_cond); SDL_UnlockMutex(is->pictq_mutex); } |
你可以看到我們把SDL_CreateYUVOverlay函數從主循環中移到了這裏。這段代碼應該完全可以自我註釋。記住我們把高度和寬度保存到VideoPicture結構體中因爲我們需要保存我們的視頻的大小沒有因爲某些原因而改變。
好,我們幾乎已經全部解決並且可以申請到YUV覆蓋和準備好接收圖像。讓我們回顧一下queue_picture並看一個拷貝幀到覆蓋的代碼。你應該能認出其中的一部分:
int queue_picture(VideoState *is, AVFrame *pFrame) { if(vp->bmp) { SDL_LockYUVOverlay(vp->bmp); dst_pix_fmt = PIX_FMT_YUV420P; pict.data[0] = vp->bmp->pixels[0]; pict.data[1] = vp->bmp->pixels[2]; pict.data[2] = vp->bmp->pixels[1]; pict.linesize[0] = vp->bmp->pitches[0]; pict.linesize[1] = vp->bmp->pitches[2]; pict.linesize[2] = vp->bmp->pitches[1]; // Convert the image into YUV format that SDL uses img_convert(&pict, dst_pix_fmt, (AVPicture *)pFrame, is->video_st->codec->pix_fmt, is->video_st->codec->width, is->video_st->codec->height); SDL_UnlockYUVOverlay(vp->bmp); if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) { is->pictq_windex = 0; } SDL_LockMutex(is->pictq_mutex); is->pictq_size++; SDL_UnlockMutex(is->pictq_mutex); } return 0; } |
這部分代碼和前面用到的一樣,主要是簡單的用我們的幀來填充YUV覆蓋。最後一點只是簡單的給隊列加1。這個隊列在寫的時候會一直寫入到滿爲止,在讀的時候會一直讀空爲止。因此所有的都依賴於is->pictq_size值,這要求我們必需要鎖定它。這裏我們做的是增加寫指針(在必要的時候採用輪轉的方式),然後鎖定隊列並且增加尺寸。現在我們的讀者函數將會知道隊列中有了更多的信息,當隊列滿的時候,我們的寫入函數也會知道。
顯示視頻
這就是我們的視頻線程。現在我們看過了幾乎所有的線程除了一個--記得我們調用schedule_refresh()函數嗎?讓我們看一下實際中是如何做的:
static void schedule_refresh(VideoState *is, int delay) { SDL_AddTimer(delay, sdl_refresh_timer_cb, is); } |
函數SDL_AddTimer()是SDL中的一個定時(特定的毫秒)執行用戶定義的回調函數(可以帶一些參數user data)的簡單函數。我們將用這個函數來定時刷新視頻--每次我們調用這個函數的時候,它將設置一個定時器來觸發定時事件來把一幀從圖像隊列中顯示到屏幕上。
但是,讓我們先觸發那個事件。
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) { SDL_Event event; event.type = FF_REFRESH_EVENT; event.user.data1 = opaque; SDL_PushEvent(&event); return 0; } |
這裏向隊列中寫入了一個現在很熟悉的事件。FF_REFRESH_EVENT被定義成SDL_USEREVENT+1。要注意的一件事是當返回0的時候,SDL停止定時器,於是回調就不會再發生。
現在我們產生了一個FF_REFRESH_EVENT事件,我們需要在事件循環中處理它:
for(;;) { SDL_WaitEvent(&event); switch(event.type) { case FF_REFRESH_EVENT: video_refresh_timer(event.user.data1); break; |
於是我們就運行到了這個函數,在這個函數中會把數據從圖像隊列中取出:
void video_refresh_timer(void *userdata) { VideoState *is = (VideoState *)userdata; VideoPicture *vp; if(is->video_st) { if(is->pictq_size == 0) { schedule_refresh(is, 1); } else { vp = &is->pictq[is->pictq_rindex]; schedule_refresh(is, 80); video_display(is); if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) { is->pictq_rindex = 0; } SDL_LockMutex(is->pictq_mutex); is->pictq_size--; SDL_CondSignal(is->pictq_cond); SDL_UnlockMutex(is->pictq_mutex); } } else { schedule_refresh(is, 100); } } |
現在,這只是一個極其簡單的函數:當隊列中有數據的時候,他從其中獲得數據,爲下一幀設置定時器,調用video_display函數來真正顯示圖像到屏幕上,然後把隊列讀索引值加1,並且把隊列的尺寸size減1。你可能會注意到在這個函數中我們並沒有真正對vp做一些實際的動作,原因是這樣的:我們將在後面處理。我們將在後面同步音頻和視頻的時候用它來訪問時間信息。你會在這裏看到這個註釋信息“timing密碼here”。那裏我們將討論什麼時候顯示下一幀視頻,然後把相應的值寫入到schedule_refresh()函數中。現在我們只是隨便寫入一個值80。從技術上來講,你可以猜測並驗證這個值,並且爲每個電影重新編譯程序,但是:1)過一段時間它會漂移;2)這種方式是很笨的。我們將在後面來討論它。
我們幾乎做完了;我們僅僅剩了最後一件事:顯示視頻!下面就是video_display函數:
void video_display(VideoState *is) { SDL_Rect rect; VideoPicture *vp; AVPicture pict; float aspect_ratio; int w, h, x, y; int i; vp = &is->pictq[is->pictq_rindex]; if(vp->bmp) { if(is->video_st->codec->sample_aspect_ratio.num == 0) { aspect_ratio = 0; } else { aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) * is->video_st->codec->width / is->video_st->codec->height; } if(aspect_ratio <= 0.0) { aspect_ratio = (float)is->video_st->codec->width / (float)is->video_st->codec->height; } h = screen->h; w = ((int)rint(h * aspect_ratio)) & -3; if(w > screen->w) { w = screen->w; h = ((int)rint(w / aspect_ratio)) & -3; } x = (screen->w - w) / 2; y = (screen->h - h) / 2; rect.x = x; rect.y = y; rect.w = w; rect.h = h; SDL_DisplayYUVOverlay(vp->bmp, &rect); } } |
因爲我們的屏幕可以是任意尺寸(我們設置爲640x480並且用戶可以自己來改變尺寸),我們需要動態計算出我們顯示的圖像的矩形大小。所以一開始我們需要計算出電影的縱橫比aspect ratio,表示方式爲寬度除以高度。某些編解碼器會有奇數採樣縱橫比,只是簡單表示了一個像素或者一個採樣的寬度除以高度的比例。因爲寬度和高度在我們的編解碼器中是用像素爲單位的,所以實際的縱橫比與縱橫比乘以樣本縱橫比相同。某些編解碼器會顯示縱橫比爲0,這表示每個像素的縱橫比爲1x1。然後我們把電影縮放到適合屏幕的儘可能大的尺寸。這裏的& -3表示與-3做與運算,實際上是讓它們4字節對齊。然後我們把電影移到中心位置,接着調用SDL_DisplayYUVOverlay()函數。
結果是什麼?我們做完了嗎?嗯,我們仍然要重新改寫聲音部分的代碼來使用新的VideoStruct結構體,但是那些只是嘗試着改變,你可以看一下那些參考示例代碼。最後我們要做的是改變ffmpeg提供的默認退出回調函數爲我們的退出回調函數。
VideoState *global_video_state; int decode_interrupt_cb(void) { return (global_video_state && global_video_state->quit); } |
我們在主函數中爲大結構體設置了global_video_state。
這就是了!讓我們編譯它:
gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lz -lm \ `sdl-config --cflags --libs` |
請享受一下沒有經過同步的電影!下次我們將編譯一個可以最終工作的電影播放器
ffmpeg文檔5
(2008-08-26 23:21:41)標籤:
雜談 |
指導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。這就是爲什麼我們會有一個解碼時間戳和一個顯示時間戳的原因。解碼時間戳告訴我們什麼時候需要解碼,顯示時間戳告訴我們什麼時候需要顯示。所以,在這種情況下,我們的流可以是這樣的:
PTS: 1 4 2 3 DTS: 1 2 3 4 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,這是很容易的:
double pts; for(;;) { if(packet_queue_get(&is->videoq, packet, 1) < 0) { // means we quit getting packets break; } pts = 0; // Decode video frame len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, packet->data, packet->size); if(packet->dts != AV_NOPTS_VALUE) { pts = packet->dts; } else { pts = 0; } pts *= av_q2d(is->video_st->time_base); |
如果我們得不到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) { int ret = avcodec_default_get_buffer(c, pic); uint64_t *pts = av_malloc(sizeof(uint64_t)); *pts = global_video_pkt_pts; pic->opaque = pts; return ret; } void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) { if(pic) av_freep(&pic->opaque); avcodec_default_release_buffer(c, pic); } |
函數avcodec_default_get_buffer和avcodec_default_release_buffer是ffmpeg中默認的申請緩衝的函數。函數av_freep是一個內存管理函數,它不但把內存釋放而且把指針設置爲NULL。
現在到了我們流打開的函數(stream_component_open),我們添加這幾行來告訴ffmpeg如何去做:
codecCtx->get_buffer = our_get_buffer; codecCtx->release_buffer = our_release_buffer; |
現在我們必需添加代碼來保存PTS到全局變量中,然後在需要的時候來使用它。我們的代碼現在看起來應該是這樣子:
for(;;) { if(packet_queue_get(&is->videoq, packet, 1) < 0) { // means we quit getting packets break; } pts = 0; // Save global pts to be stored in pFrame in first call global_video_pkt_pts = packet->pts; // Decode video frame len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, packet->data, packet->size); if(packet->dts == AV_NOPTS_VALUE && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) { pts = *(uint64_t *)pFrame->opaque; } else if(packet->dts != AV_NOPTS_VALUE) { pts = packet->dts; } else { pts = 0; } pts *= av_q2d(is->video_st->time_base); |
技術提示:你可能已經注意到我們使用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 { double video_clock; /// |
下面的是函數synchronize_video,它可以很好的自我註釋:
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) { double frame_delay; if(pts != 0) { is->video_clock = pts; } else { pts = is->video_clock; } frame_delay = av_q2d(is->video_st->codec->time_base); frame_delay += src_frame->repeat_pict * (frame_delay * 0.5); is->video_clock += frame_delay; return pts; } |
你也會注意到我們也計算了重複的幀。
現在讓我們得到正確的PTS並且使用queue_picture來隊列化幀,添加一個新的時間戳參數pts:
// Did we get a video frame? if(frameFinished) { pts = synchronize_video(is, pFrame, pts); if(queue_picture(is, pFrame, pts) < 0) { break; } } |
對於queue_picture來說唯一改變的事情就是我們把時間戳值pts保存到VideoPicture結構體中,我們我們必需添加一個時間戳變量到結構體中並且添加一行代碼:
typedef struct VideoPicture { ... double pts; } int queue_picture(VideoState *is, AVFrame *pFrame, double pts) { ... stuff ... if(vp->bmp) { ... convert picture ... vp->pts = pts; ... alert queue ... } |
現在我們的圖像隊列中的所有圖像都有了正確的時間戳值,所以讓我們看一下視頻刷新函數。你會記得上次我們用80ms的刷新時間來欺騙它。那麼,現在我們將會算出實際的值。
我們的策略是通過簡單計算前一幀和現在這一幀的時間戳來預測出下一個時間戳的時間。同時,我們需要同步視頻到音頻。我們將設置一個音頻時間audio clock;一個內部值記錄了我們正在播放的音頻的位置。就像從任意的mp3播放器中讀出來的數字一樣。既然我們把視頻同步到音頻,視頻線程使用這個值來算出是否太快還是太慢。
我們將在後面來實現這些代碼;現在我們假設我們已經有一個可以給我們音頻時間的函數get_audio_clock。一旦我們有了這個值,我們在音頻和視頻失去同步的時候應該做些什麼呢?簡單而有點笨的辦法是試着用跳過正確幀或者其它的方式來解決。作爲一種替代的手段,我們會調整下次刷新的值;如果時間戳太落後於音頻時間,我們加倍計算延遲。如果時間戳太領先於音頻時間,我們將儘可能快的刷新。既然我們有了調整過的時間和延遲,我們將把它和我們通過 frame_timer計算出來的時間進行比較。這個幀時間frame_timer將會統計出電影播放中所有的延時。換句話說,這個 frame_timer就是指我們什麼時候來顯示下一幀。我們簡單的添加新的幀定時器延時,把它和電腦的系統時間進行比較,然後使用那個值來調度下一次刷新。這可能有點難以理解,所以請認真研究代碼:
void video_refresh_timer(void *userdata) { VideoState *is = (VideoState *)userdata; VideoPicture *vp; double actual_delay, delay, sync_threshold, ref_clock, diff; if(is->video_st) { if(is->pictq_size == 0) { schedule_refresh(is, 1); } else { vp = &is->pictq[is->pictq_rindex]; delay = vp->pts - is->frame_last_pts; if(delay <= 0 || delay >= 1.0) { delay = is->frame_last_delay; } is->frame_last_delay = delay; is->frame_last_pts = vp->pts; ref_clock = get_audio_clock(is); diff = vp->pts - ref_clock; sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD; if(fabs(diff) < AV_NOSYNC_THRESHOLD) { if(diff <= -sync_threshold) { delay = 0; } else if(diff >= sync_threshold) { delay = 2 * delay; } } is->frame_timer += delay; actual_delay = is->frame_timer - (av_gettime() / 1000000.0); if(actual_delay < 0.010) { actual_delay = 0.010; } schedule_refresh(is, (int)(actual_delay * 1000 + 0.5)); video_display(is); if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) { is->pictq_rindex = 0; } SDL_LockMutex(is->pictq_mutex); is->pictq_size--; SDL_CondSignal(is->pictq_cond); SDL_UnlockMutex(is->pictq_mutex); } } else { schedule_refresh(is, 100); } } |
我們在這裏做了很多檢查:首先,我們保證現在的時間戳和上一個時間戳之間的處以delay是有意義的。如果不是的話,我們就猜測着用上次的延遲。接着,我們有一個同步閾值,因爲在同步的時候事情並不總是那麼完美的。在ffplay中使用0.01作爲它的值。我們也保證閾值不會比時間戳之間的間隔短。最後,我們把最小的刷新值設置爲10毫秒。
(這句不知道應該放在哪裏)事實上這裏我們應該跳過這一幀,但是我們不想爲此而煩惱。 |
我們給大結構體添加了很多的變量,所以不要忘記檢查一下代碼。同時也不要忘記在函數streame_component_open中初始化幀時間frame_timer和前面的幀延遲frame delay:
is->frame_timer = (double)av_gettime() / 1000000.0; is->frame_last_delay = 40e-3; |
同步:聲音時鐘
現在讓我們看一下怎樣來得到聲音時鐘。我們可以在聲音解碼函數audio_decode_frame中更新時鐘時間。現在,請記住我們並不是每次調用這個函數的時候都在處理新的包,所以有我們要在兩個地方更新時鐘。第一個地方是我們得到新的包的時候:我們簡單的設置聲音時鐘爲這個包的時間戳。然後,如果一個包裏有許多幀,我們通過樣本數和採樣率來計算,所以當我們得到包的時候:
if(pkt->pts != AV_NOPTS_VALUE) { is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts; } |
然後當我們處理這個包的時候:
pts = is->audio_clock; *pts_ptr = pts; n = 2 * is->audio_st->codec->channels; is->audio_clock += (double)data_size / (double)(n * is->audio_st->codec->sample_rate); |
一點細節:臨時函數被改成包含pts_ptr,所以要保證你已經改了那些。這時的pts_ptr是一個用來通知audio_callback函數當前聲音包的時間戳的指針。這將在下次用來同步聲音和視頻。
現在我們可以最後來實現我們的get_audio_clock函數。它並不像得到is->audio_clock值那樣簡單。注意我們會在每次處理它的時候設置聲音時間戳,但是如果你看了audio_callback函數,它花費了時間來把數據從聲音包中移到我們的輸出緩衝區中。這意味着我們聲音時鐘中記錄的時間比實際的要早太多。所以我們必須要檢查一下我們還有多少沒有寫入。下面是完整的代碼:
double get_audio_clock(VideoState *is) { double pts; int hw_buf_size, bytes_per_sec, n; pts = is->audio_clock; hw_buf_size = is->audio_buf_size - is->audio_buf_index; bytes_per_sec = 0; n = is->audio_st->codec->channels * 2; if(is->audio_st) { bytes_per_sec = is->audio_st->codec->sample_rate * n; } if(bytes_per_sec) { pts -= (double)hw_buf_size / bytes_per_sec; } return pts; } |
你應該知道爲什麼這個函數可以正常工作了;)
這就是了!讓我們編譯它:
gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
最後,你可以使用我們自己的電影播放器來看電影了。下次我們將看一下聲音同步,然後接下來的指導我們會討論查詢
7
ffmpeg文檔6
(2008-08-27 18:44:22)指導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) { if(is->video_st) { if(is->pictq_size == 0) { schedule_refresh(is, 1); } else { vp = &is->pictq[is->pictq_rindex]; is->video_current_pts = vp->pts; is->video_current_pts_time = av_gettime(); |
不要忘記在stream_component_open函數中初始化它:
is->video_current_pts_time = av_gettime(); |
現在我們需要一種得到信息的方式:
double get_video_clock(VideoState *is) { double delta; delta = (av_gettime() - is->video_current_pts_time) / 1000000.0; return is->video_current_pts + delta; } |
提取時鐘
但是爲什麼要強制使用視頻時鐘呢?我們更改視頻同步代碼以致於音頻和視頻不會試着去相互同步。想像一下我們讓它像ffplay一樣有一個命令行參數。所以讓我們抽象一樣這件事情:我們將做一個新的封裝函數get_master_clock,用來檢測av_sync_type變量然後決定調用 get_audio_clock還是get_video_clock或者其它的想使用的獲得時鐘的函數。我們甚至可以使用電腦時鐘,這個函數我們叫做 get_external_clock:
enum { AV_SYNC_AUDIO_MASTER, AV_SYNC_VIDEO_MASTER, AV_SYNC_EXTERNAL_MASTER, }; #define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTER double get_master_clock(VideoState *is) { if(is->av_sync_type == AV_SYNC_VIDEO_MASTER) { return get_video_clock(is); } else if(is->av_sync_type == AV_SYNC_AUDIO_MASTER) { return get_audio_clock(is); } else { return get_external_clock(is); } } main() { ... is->av_sync_type = DEFAULT_AV_SYNC_TYPE; ... } |
同步音頻
現在是最難的部分:同步音頻到視頻時鐘。我們的策略是測量聲音的位置,把它與視頻時間比較然後算出我們需要修正多少的樣本數,也就是說:我們是否需要通過丟棄樣本的方式來加速播放還是需要通過插值樣本的方式來放慢播放?
我們將在每次處理聲音樣本的時候運行一個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, int samples_size, double pts) { int n; double ref_clock; n = 2 * is->audio_st->codec->channels; if(is->av_sync_type != AV_SYNC_AUDIO_MASTER) { double diff, avg_diff; int wanted_size, min_size, max_size, nb_samples; ref_clock = get_master_clock(is); diff = get_audio_clock(is) - ref_clock; if(diff < AV_NOSYNC_THRESHOLD) { // accumulate the diffs is->audio_diff_cum = diff + is->audio_diff_avg_coef * is->audio_diff_cum; if(is->audio_diff_avg_count < AUDIO_DIFF_AVG_NB) { is->audio_diff_avg_count++; } else { avg_diff = is->audio_diff_cum * (1.0 - is->audio_diff_avg_coef); } } else { is->audio_diff_avg_count = 0; is->audio_diff_cum = 0; } } return samples_size; } |
現在我們已經做得很好;我們已經近似的知道如何用視頻或者其它的時鐘來調整音頻了。所以讓我們來計算一下要在添加和砍掉多少樣本,並且如何在“Shrinking/expanding buffer code”部分來寫上代碼:
if(fabs(avg_diff) >= is->audio_diff_threshold) { wanted_size = samples_size + ((int)(diff * is->audio_st->codec->sample_rate) * n); min_size = samples_size * ((100 - SAMPLE_CORRECTION_PERCENT_MAX) / 100); max_size = samples_size * ((100 + SAMPLE_CORRECTION_PERCENT_MAX) / 100); if(wanted_size < min_size) { wanted_size = min_size; } else if (wanted_size > max_size) { wanted_size = max_size; } |
記住audio_length * (sample_rate * # of channels * 2)就是audio_length秒時間的聲音的樣本數。所以,我們想要的樣本數就是我們根據聲音偏移添加或者減少後的聲音樣本數。我們也可以設置一個範圍來限定我們一次進行修正的長度,因爲如果我們改變的太多,用戶會聽到刺耳的聲音。
修正樣本數
現在我們要真正的修正一下聲音。你可能會注意到我們的同步函數synchronize_audio返回了一個樣本數,這可以告訴我們有多少個字節被送到流中。所以我們只要調整樣本數爲wanted_size就可以了。這會讓樣本更小一些。但是如果我們想讓它變大,我們不能只是讓樣本大小變大,因爲在緩衝區中沒有多餘的數據!所以我們必需添加上去。但是我們怎樣來添加呢?最笨的辦法就是試着來推算聲音,所以讓我們用已有的數據在緩衝的末尾添加上最後的樣本。
if(wanted_size < samples_size) { samples_size = wanted_size; } else if(wanted_size > samples_size) { uint8_t *samples_end, *q; int nb; nb = (samples_size - wanted_size); samples_end = (uint8_t *)samples + samples_size - n; q = samples_end + n; while(nb > 0) { memcpy(q, samples_end, n); q += n; nb -= n; } samples_size = wanted_size; } |
現在我們通過這個函數返回的是樣本數。我們現在要做的是使用它:
void audio_callback(void *userdata, Uint8 *stream, int len) { VideoState *is = (VideoState *)userdata; int len1, audio_size; double pts; while(len > 0) { if(is->audio_buf_index >= is->audio_buf_size) { audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts); if(audio_size < 0) { is->audio_buf_size = 1024; memset(is->audio_buf, 0, is->audio_buf_size); } else { audio_size = synchronize_audio(is, (int16_t *)is->audio_buf, audio_size, pts); is->audio_buf_size = audio_size; |
我們要做的是把函數synchronize_audio插入進去。(同時,保證在初始化上面變量的時候檢查一下代碼,這些我沒有贅述)。
結束之前的最後一件事情:我們需要添加一個if語句來保證我們不會在視頻爲主時鐘的時候也來同步視頻。
if(is->av_sync_type != AV_SYNC_VIDEO_MASTER) { ref_clock = get_master_clock(is); diff = vp->pts - ref_clock; sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD; if(fabs(diff) < AV_NOSYNC_THRESHOLD) { if(diff <= -sync_threshold) { delay = 0; } else if(diff >= sync_threshold) { delay = 2 * delay; } } } |
添加後就可以了。要保證整個程序中我沒有贅述的變量都被初始化過了。然後編譯它:
gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs` |
然後你就可以運行它了。
下次我們要做的是讓你可以讓電影快退和快進。
--
ffmpeg文檔7
(2008-08-28 09:41:24)指導7:快進快退
處理快進快退命令
現在我們來爲我們的播放器加入一些快進和快退的功能,因爲如果你不能全局搜索一部電影是很讓人討厭的。同時,這將告訴你av_seek_frame函數是多麼容易使用。
我們將在電影播放中使用左方向鍵和右方向鍵來表示向後和向前一小段,使用向上和向下鍵來表示向前和向後一大段。這裏一小段是10秒,一大段是60秒。所以我們需要設置我們的主循環來捕捉鍵盤事件。然而當我們捕捉到鍵盤事件後我們不能直接調用av_seek_frame函數。我們要主要的解碼線程 decode_thread的循環中做這些。所以,我們要添加一些變量到大結構體中,用來包含新的跳轉位置和一些跳轉標誌:
int seek_req; int seek_flags; int64_t seek_pos; |
現在讓我們在主循環中捕捉按鍵:
for(;;) { double incr, pos; SDL_WaitEvent(&event); switch(event.type) { case SDL_KEYDOWN: switch(event.key.keysym.sym) { case SDLK_LEFT: incr = -10.0; goto do_seek; case SDLK_RIGHT: incr = 10.0; goto do_seek; case SDLK_UP: incr = 60.0; goto do_seek; case SDLK_DOWN: incr = -60.0; goto do_seek; do_seek: if(global_video_state) { pos = get_master_clock(global_video_state); pos += incr; stream_seek(global_video_state, (int64_t)(pos * AV_TIME_BASE), incr); } break; default: break; } break; |
爲了檢測按鍵,我們先查了一下是否有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) { if(!is->seek_req) { is->seek_pos = pos; is->seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0; is->seek_req = 1; } } |
現在讓我們看一下如果在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) { int stream_index= -1; int64_t seek_target = is->seek_pos; if (is->videoStream >= 0) stream_index = is->videoStream; else if(is->audioStream >= 0) stream_index = is->audioStream; if(stream_index>=0){ seek_target= av_rescale_q(seek_target, AV_TIME_BASE_Q, pFormatCtx->streams[stream_index]->time_base); } if(av_seek_frame(is->pFormatCtx, stream_index, seek_target, is->seek_flags) < 0) { fprintf(stderr, "%s: error while seeking\n", is->pFormatCtx->filename); } else { |
這裏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) { AVPacketList *pkt, *pkt1; SDL_LockMutex(q->mutex); for(pkt = q->first_pkt; pkt != NULL; pkt = pkt1) { pkt1 = pkt->next; av_free_packet(&pkt->pkt); av_freep(&pkt); } q->last_pkt = NULL; q->first_pkt = NULL; q->nb_packets = 0; q->size = 0; SDL_UnlockMutex(q->mutex); } |
既然隊列已經清空了,我們放入“清空包”。但是開始我們要定義和創建這個包:
AVPacket flush_pkt; main() { ... av_init_packet(&flush_pkt); flush_pkt.data = "FLUSH"; ... } |
現在我們把這個包放到隊列中:
} else { if(is->audioStream >= 0) { packet_queue_flush(&is->audioq); packet_queue_put(&is->audioq, &flush_pkt); } if(is->videoStream >= 0) { packet_queue_flush(&is->videoq); packet_queue_put(&is->videoq, &flush_pkt); } } is->seek_req = 0; } |
(這些代碼片段是接着前面decode_thread中的代碼片段的)我們也需要修改packet_queue_put函數才不至於直接簡單複製了這個包:
int packet_queue_put(PacketQueue *q, AVPacket *pkt) { AVPacketList *pkt1; if(pkt != &flush_pkt && av_dup_packet(pkt) < 0) { return -1; } |
然後在聲音線程和視頻線程中,我們在packet_queue_get後立即調用函數avcodec_flush_buffers:
if(packet_queue_get(&is->audioq, pkt, 1) < 0) { return -1; } if(packet->data == flush_pkt.data) { avcodec_flush_buffers(is->audio_st->codec); continue; } |
上面的代碼片段與視頻線程中的一樣,只要把“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) { static struct SwsContext *img_convert_ctx; ... if(vp->bmp) { SDL_LockYUVOverlay(vp->bmp); dst_pix_fmt = PIX_FMT_YUV420P; pict.data[0] = vp->bmp->pixels[0]; pict.data[1] = vp->bmp->pixels[2]; pict.data[2] = vp->bmp->pixels[1]; pict.linesize[0] = vp->bmp->pitches[0]; pict.linesize[1] = vp->bmp->pitches[2]; pict.linesize[2] = vp->bmp->pitches[1]; // Convert the image into YUV format that SDL uses if(img_convert_ctx == NULL) { int w = is->video_st->codec->width; int h = is->video_st->codec->height; img_convert_ctx = sws_getContext(w, h, is->video_st->codec->pix_fmt, w, h, dst_pix_fmt, SWS_BICUBIC, NULL, NULL, NULL); if(img_convert_ctx == NULL) { fprintf(stderr, "Cannot initialize the conversion context!\n"); exit(1); } } sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize, 0, is->video_st->codec->height, pict.data, pict.linesize); |
我們把新的縮放器放到了合適的位置。希望這會讓你知道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中的命令開關。
·其它事情,例如:在結構體中的音頻緩衝區應該對齊。