FFmpeg如何同步音視頻的解決方案_第1頁(yè)
FFmpeg如何同步音視頻的解決方案_第2頁(yè)
FFmpeg如何同步音視頻的解決方案_第3頁(yè)
FFmpeg如何同步音視頻的解決方案_第4頁(yè)
FFmpeg如何同步音視頻的解決方案_第5頁(yè)
已閱讀5頁(yè),還剩45頁(yè)未讀, 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

1、【轉(zhuǎn)】如何同步視頻2010-07-1610:18轉(zhuǎn)載自fandy586最終編輯fandy586如何同步視頻PTS 和 DTS幸運(yùn)的是,音頻和視頻流都有一些關(guān)于以多快速度和什么時(shí)間來(lái)播放它們的信息在里面。音頻流有采樣,視頻流有每秒的幀率。然而,如 果我們只是簡(jiǎn)單的通過(guò)數(shù)幀和乘以幀率的方式來(lái)同步視頻,那么就很有可能會(huì)失去同步。于是作為一種補(bǔ)充,在流中的包有種叫做 DTS (解碼時(shí)間戳)和PTS (顯示時(shí)間戳)的機(jī)制。為了這兩個(gè)參數(shù),你需要了解電影存放的方式。像MPEG等格式,使用被叫做 B幀(B表示雙向bidrectional )的方式。另外兩種幀被叫做I幀和P幀(I表示關(guān)鍵幀,P表示預(yù)測(cè)幀)。I

2、幀包含了某個(gè)特定的完整圖像。P幀依賴于前面的I幀和P幀并且使用比較或者差分的方式來(lái)編碼。B幀與P幀有點(diǎn)類似,但是它是依賴于前面和后面的幀的信息的。這也就解釋了為什么我們可能在調(diào)用avcodec_decode_video 以后會(huì)得不到一幀圖像。所以對(duì)于一個(gè)電影,幀是這樣來(lái)顯示的:I BBP?,F(xiàn)在我們需要在顯示 B幀之前知道P幀中的信息。因此,幀可能會(huì)按照這樣的方式來(lái)存儲(chǔ):IPBB。這就是為什么我們會(huì)有一個(gè)解碼時(shí)間戳和一個(gè)顯示時(shí)間戳的原因。解碼時(shí)間戳告訴我們什么時(shí)候需要解碼,顯示時(shí)間戳告訴我 們什么時(shí)候需要顯示。所以,在這種情況下,我們的流可以是這樣的:PTS: 1 4 2 3DTS: 1 2 3

3、 4Stream: I P B B通常PTS和DTS只有在流中有 B幀的時(shí)候會(huì)不同。當(dāng)我們調(diào)用av_read_frame()得到一個(gè)包的時(shí)候, PTS和DTS的信息也會(huì)保存在包中。但是我們真正想要的PTS是我們剛剛解碼出來(lái)的原始幀的PTS,這樣我們才能知道什么時(shí)候來(lái)顯示它。然而,我們從 avcodec_decode_video() 函數(shù)中得到的幀只是一個(gè) AVFrame ,其中 并沒(méi)有包含有用的 PTS值(注意:AVFrame并沒(méi)有包含時(shí)間戳信息,但當(dāng)我們等到幀的時(shí)候并不是我們想要的樣子)。然而,ffmpeg重新排序包以便于被 avcodec_decode_video() 函數(shù)處理的包的 DT

4、S可以總是與其返回的PTS相同。但是,另外的一個(gè)警告是:我們也并不是總能得到這個(gè)信息。不用擔(dān)心,因?yàn)橛辛硗庖环N辦法可以找到幀的PTS,我們可以讓程序自己來(lái)重新排序包。我們保存一幀的第一個(gè)包的PTS:這將作為整個(gè)這一幀的 PTS。我們可以通過(guò)函數(shù) avcodec_decode_video() 來(lái)計(jì)算出哪個(gè)包是一幀的第一個(gè)包。怎樣實(shí)現(xiàn)呢?任何時(shí)候當(dāng)一個(gè)包開(kāi)始一 幀的時(shí)候,avcodec_decode_video() 將調(diào)用一個(gè)函數(shù)來(lái)為一幀申請(qǐng)一個(gè)緩沖。當(dāng)然, ffmpeg允許我們重新定義那個(gè)分配內(nèi)存的函數(shù)。所以 我們制作了一個(gè)新的函數(shù)來(lái)保存一個(gè)包的時(shí)間戳。當(dāng)然,盡管那樣,我們可能還是得不到一個(gè)正確

5、的時(shí)間戳。我們將在后面處理這個(gè)問(wèn)題。同步現(xiàn)在,知道了什么時(shí)候來(lái)顯示一個(gè)視頻幀真好,但是我們?cè)鯓觼?lái)實(shí)際操作呢?這里有個(gè)主意:當(dāng)我們顯示了一幀以后,我們計(jì)算出下一幀 顯示的時(shí)間。然后我們簡(jiǎn)單的設(shè)置一個(gè)新的定時(shí)器來(lái)。你可能會(huì)想,我們檢查下一幀的PTS值而不是系統(tǒng)時(shí)鐘來(lái)看超時(shí)是否會(huì)到。這種方式可以工作,但是有兩種情況要處理。首先,要知道下一個(gè) PTS是什么?,F(xiàn)在我們能添加視頻速率到我們的PTS中-太對(duì)了!然而,有些電影需要幀重復(fù)。這意味著我們重復(fù)播放當(dāng)前的幀。這將導(dǎo)致程序顯示下一幀太快了。所以我們需要計(jì)算它們。第二,正如程序現(xiàn)在這樣,視頻和音頻播放很歡快,一點(diǎn)也不受同步的影響。如果一切都工作得很好的話

6、,我們不必?fù)?dān)心。但是,你的電 腦并不是最好的,很多視頻文件也不是完好的。所以,我們有三種選擇:同步音頻到視頻,同步視頻到音頻,或者都同步到外部時(shí)鐘(例 如你的電腦時(shí)鐘)。從現(xiàn)在開(kāi)始,我們將同步視頻到音頻。寫(xiě)代碼:獲得幀的時(shí)間戳現(xiàn)在讓我們到代碼中來(lái)做這些事情。我們將需要為我們的大結(jié)構(gòu)體添加一些成員,但是我們會(huì)根據(jù)需要來(lái)做。首先,讓我們看一下視頻線程。記住,在這里我們得到了解碼線程輸出到隊(duì)列中的包。這里我們需要的是從avcodec_decode_video 函數(shù)中得到幀的時(shí)間戳。我們討論的第一種方式是從上次處理的包中得到DTS,這是很容易的: double pts;for(;) (if(packe

7、t_queue_get(&is->videoq, packet, 1) < 0) / means we quit getting packetsbreak;)pts = 0;/ Decode video frameIen1 = avcodec_decode_video(is->video_st->codec,pFrame, &frameFinished,packet->data, packet->size);if(packet->dts != AV_NOPTS_VALUE) pts = packet->dts; else pts

8、= 0;)pts *= av_q2d(is->video_st->time_base);如果我們得不到 PTS就把它設(shè)置為0o好,那是很容易的。但是我們所說(shuō)的如果包的DTS不能幫到我們,我們需要使用這一幀的第一個(gè)包的PTS。我們通過(guò)讓ffmpeg使用我們自己的申請(qǐng)幀程序來(lái)實(shí)現(xiàn)。下面的是函數(shù)的格式:intget_buffer(structAVCodecContext *c, AVFrame *pic);void release_buffer(structAVCodecContext *c, AVFrame *pic);申請(qǐng)函數(shù)沒(méi)有告訴我們關(guān)于包的任何事情,所以我們要自己每次在得到一個(gè)

9、包的時(shí)候把PTS保存到一個(gè)全局變量中去。我們自己以讀到它。然后,我們把值保存到AVFrame結(jié)構(gòu)體難理解的變量中去。所以一開(kāi)始,這就是我們的函數(shù):uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;intour_get_buffer(structAVCodecContext *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->

10、;opaque = pts;return ret;void our_release_buffer(structAVCodecContext *c, AVFrame *pic) if(pic) av_freep(&pic->opaque);avcodec_default_release_buffer(c, pic);函數(shù) avcodec_default_get_buffer 和 avcodec_default_release_buffer 是 ffmpeg 中默認(rèn)的申請(qǐng)緩沖的函數(shù)。函數(shù) av_freep 是一個(gè)內(nèi)存管理函數(shù),它不但把內(nèi)存釋放而且把指針設(shè)置為NULL?,F(xiàn)在到了我們流打

11、開(kāi)的函數(shù)(stream_component_open ),我們添加這幾行來(lái)告訴ffmpeg如何去做:codecCtx->get_buffer = our_get_buffer;codecCtx->release_buffer = our_release_buffer;現(xiàn)在我們必需添加代碼來(lái)保存PTS到全局變量中,然后在需要的時(shí)候來(lái)使用它。我們的代碼現(xiàn)在看起來(lái)應(yīng)該是這樣子:for(;) if(packet_queue_get(&is->videoq, packet, 1) < 0) / means we quit getting packetsbreak;pts

12、= 0;/ Save global pts to be stored in pFrame in first callglobal_video_pkt_pts = packet->pts;/ Decode video frame lenl = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished, packet->data, packet->size);if(packet->dts = AV_NOPTS_VALUE&&pFrame->opaque &

13、;& *(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);技術(shù)提示:你可能已經(jīng)注意到我們使用int64來(lái)表示PTS。這是因?yàn)镻TS是以整型來(lái)保存的。這個(gè)值是一個(gè)時(shí)間戳相當(dāng)于時(shí)間的度量,用來(lái)以流的time_base為單位進(jìn)行時(shí)間

14、度量。例如,如果一個(gè)流是 24幀每秒,值為42的PTS表示這一幀應(yīng)該排在第42個(gè)幀的位置如果我們每秒有24幀(這里并不完全正確)。我們可以通過(guò)除以幀率來(lái)把這個(gè)值轉(zhuǎn)化為秒。流中的time_base值表示1/framerate (對(duì)于固定幀率來(lái)說(shuō)),所以得到了以秒為單位的PTS ,我們需要乘以time_base 。寫(xiě)代碼:使用PTS來(lái)同步現(xiàn)在我們得到了 PTS。我們要注意前面討論到的兩個(gè)同步問(wèn)題。我們將定義一個(gè)函數(shù)叫做synchronize_video ,它可以更新同步的 PTS。我們可以使用內(nèi)部的反映當(dāng)前視這個(gè)函數(shù)也能最終處理我們得不到PTS的情況。同時(shí)我們要知道下一幀的時(shí)間以便于正確設(shè)置刷新速

15、率。頻已經(jīng)播放時(shí)間的時(shí)鐘 video_clock來(lái)完成這個(gè)功能。我們把這些值添加到大結(jié)構(gòu)體中。typedefstructVideoState (double video_clock; /下面的是函數(shù)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_del

16、ay = 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;你也會(huì)注意到我們也計(jì)算了重復(fù)的幀?,F(xiàn)在讓我們得到正確的PTS并且使用queue_picture來(lái)隊(duì)列化幀,添加一個(gè)新的時(shí)間戳參數(shù)pts :/ Did we get a video frame?if(frameFinished) pts = synchronize_video(i

17、s, pFrame, pts);if(queue_picture(is, pFrame, pts) < 0) break;對(duì)于queue_picture 來(lái)說(shuō)唯一改變的事情就是我們把時(shí)間戳值pts保存到VideoPicture結(jié)構(gòu)體中,我們必需添加一個(gè)時(shí)間戳變量到結(jié)構(gòu)體中并且添加一行代碼:typedefstructVideoPicture .double pts; intqueue_picture(VideoState *is, AVFrame *pFrame, double pts) . stuff .if(vp->bmp) (. convert picture .vp->

18、pts = pts;. alert queue .現(xiàn)在我們的圖像隊(duì)列中的所有圖像都有了正確的時(shí)間戳值,所以讓我們看一下視頻刷新函數(shù)。你會(huì)記得上次我們用80ms的刷新時(shí)間來(lái)欺騙它。那么,現(xiàn)在我們將會(huì)算出實(shí)際的值。我們的策略是通過(guò)簡(jiǎn)單計(jì)算前一幀和現(xiàn)在這一幀的時(shí)間戳來(lái)預(yù)測(cè)出下一個(gè)時(shí)間戳的時(shí)間。同時(shí),我們需要同步視頻到音頻。我們將設(shè)置一個(gè)音頻時(shí)間audio clock ; 一個(gè)內(nèi)部值記錄了我們正在播放的音頻的位置。就像從任意的mp3播放器中讀出來(lái)的數(shù)字一樣。既然我們把視頻同步到音頻,視頻線程使用這個(gè)值來(lái)算出是否太快還是太慢。我們將在后面來(lái)實(shí)現(xiàn)這些代碼;現(xiàn)在我們假設(shè)我們已經(jīng)有一個(gè)可以給我們音頻時(shí)間的函數(shù)

19、get_audio_clock。一旦我們有了這個(gè)值,我們?cè)谝纛l和視頻失去同步的時(shí)候應(yīng)該做些什么呢?簡(jiǎn)單而有點(diǎn)笨的辦法是試著用跳過(guò)正確幀或者其它的方式來(lái)解決。作為一種替代的手段,我們會(huì)調(diào)整下次刷新的值;如果時(shí)間戳太落后于音頻時(shí)間,我們加倍計(jì)算延遲。如果時(shí)間戳太領(lǐng)先于音頻時(shí)間,我們將盡可能快的刷新。既然我們有了調(diào)整過(guò)的時(shí)間和延遲,我們將把它和我們通過(guò)frame_timer計(jì)算出來(lái)的時(shí)間進(jìn)行比較。這個(gè)幀時(shí)間frame_timer將會(huì)統(tǒng)計(jì)出電影播放中所有的延時(shí)。換句話說(shuō),這個(gè) frame_timer就是指我們什么時(shí)候來(lái)顯示下一幀。我們簡(jiǎn)單的添加新的幀定時(shí)器延時(shí),把它和電腦 的系統(tǒng)時(shí)間進(jìn)行比較,然后使用

20、那個(gè)值來(lái)調(diào)度下一次刷新。這可能有點(diǎn)難以理解,所以請(qǐng)認(rèn)真研究代碼:void video_refresh_timer(void *userdata) (VideoState *is = (VideoState *)userdata;VideoPicture *vp;double actual_delay, delay, sync_threshold, rejclock, diff;if(is->video_st) if(is->pictq_size = 0) schedule_refresh(is, 1); else vp = &is->pictqis->pictq

21、_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;rejclock = get_audio_clock(is);diff = vp->pts - rejclock;sync_threshold = (delay > AV_SYNC_THRESHOLD) ? del

22、ay : 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;)sc

23、hedule_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

24、);)我們?cè)谶@里做了很多檢查:首先,我們保證現(xiàn)在的時(shí)間戳和上一個(gè)時(shí)間戳之間的處以 上次的延遲。接著,我們有一個(gè)同步閾值,因?yàn)樵谕降臅r(shí)候事情并不總是那么完美的。在 值不會(huì)比時(shí)間戳之間的間隔短。最后,我們把最小的刷新值設(shè)置為10毫秒。(這句不知道應(yīng)該放在哪里)事實(shí)上這里我們應(yīng)該跳過(guò)這一幀,但是我們不想為此而煩惱。我們給大結(jié)構(gòu)體添加了很多的變量,所以不要忘記檢查一下代碼。同時(shí)也不要忘記在函數(shù) frame_timer 和前面的幀延遲 frame delay :is->frame_timer = (double)av_gettime() /1000000.0;is->frame_last_

25、delay = 40e-3;同步:聲音時(shí)鐘delay是有意義的。如果不是的話,我們就猜測(cè)著用ffplay中使用0.01作為它的值。我們也保證閾streame_component_open中初始化幀時(shí)間現(xiàn)在讓我們看一下怎樣來(lái)得到聲音時(shí)鐘。我們可以在聲音解碼函數(shù)audio_decode_frame中更新時(shí)鐘時(shí)間?,F(xiàn)在,請(qǐng)記住我們并不是每次調(diào)用這個(gè)函數(shù)的時(shí)候都在處理新的包,所以有我們要在兩個(gè)地方更新時(shí)鐘。第一個(gè)地方是我們得到新的包的時(shí)候:我們簡(jiǎn)單的設(shè)置聲音時(shí) 鐘為這個(gè)包的時(shí)間戳。然后,如果一個(gè)包里有許多幀,我們通過(guò)樣本數(shù)和采樣率來(lái)計(jì)算,所以當(dāng)我們得到包的時(shí)候:if(pkt->pts != AV

26、_NOPTS_VALUE) (is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;然后當(dāng)我們處理這個(gè)包的時(shí)候: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);一點(diǎn)細(xì)節(jié):臨時(shí)函數(shù)被改成包含pt

27、s_ptr ,所以要保證你已經(jīng)改了那些。這時(shí)的 pts_ptr是一個(gè)用來(lái)通知 audio_callback函數(shù)當(dāng)前聲音包的時(shí)間戳的指針。這將在下次用來(lái)同步聲音和視頻?,F(xiàn)在我們可以最后來(lái)實(shí)現(xiàn)我們的get_audio_clock函數(shù)。它并不像得到is->audio_clock值那樣簡(jiǎn)單。注意我們會(huì)在每次處理它的時(shí)候設(shè)置聲音時(shí)間戳,但是如果你看了audio_callback函數(shù),它花費(fèi)了時(shí)間來(lái)把數(shù)據(jù)從聲音包中移到我們的輸出緩沖區(qū)中。這意味著我們聲音時(shí)鐘中記錄的時(shí)間比實(shí)際的要早太多。所以我們必須要檢查一下我們還有多少?zèng)]有寫(xiě)入。下面是完整的代碼:double get_audio_clock(Vid

28、eoState *is) (double pts;inthw_buLsize, bytes_per_sec, n;pts = is->audio_clock;hw_buLsize = is->audio_bu1size - is->audio_buMndex;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_

29、sec) pts -= (double)hw_bujsize / bytes_per_sec;)return pts;)你應(yīng)該知道為什么這個(gè)函數(shù)可以正常工作了;)這就是了!讓我們編譯它:gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -Iz -Im'sdl-config -cflags -libs'最后,你可以使用我們自己的電影播放器來(lái)看電影了。下次我們將看一下聲音同步,然后接下來(lái)的指導(dǎo)我們會(huì)討論查詢。同步音頻現(xiàn)在我們已經(jīng)有了一個(gè)比較像樣的播放器。所以讓我們看一下還有哪些零碎的東西沒(méi)處理。上次,我們掩飾了

30、一點(diǎn)同步問(wèn)題,也就是同步 音頻到視頻而不是其它的同步方式。我們將采用和視頻一樣的方式:做一個(gè)內(nèi)部視頻時(shí)鐘來(lái)記錄視頻線程播放了多久,然后同步音頻到上 面去。后面我們也來(lái)看一下如何推而廣之把音頻和視頻都同步到外部時(shí)鐘。生成一個(gè)視頻時(shí)鐘現(xiàn)在我們要生成一個(gè)類似于上次我們的聲音時(shí)鐘的視頻時(shí)鐘:一個(gè)給出當(dāng)前視頻播放時(shí)間的內(nèi)部值。開(kāi)始,你可能會(huì)想這和使用上一幀的 時(shí)間戳來(lái)更新定時(shí)器一樣簡(jiǎn)單。但是,不要忘了視頻幀之間的時(shí)間間隔是很長(zhǎng)的,以毫秒為計(jì)量的。解決辦法是跟蹤另外一個(gè)值:我們?cè)?設(shè)置上一幀時(shí)間戳的時(shí)候的時(shí)間值。于是當(dāng)前視頻時(shí)間值就是PTS_of_last_frame + (current_time -t

31、ime_elapsed_since_PTS_value_was_set)。這種解決方式與我們?cè)诤瘮?shù)get_audio_clock 中的方式很類似。所在在我們的大結(jié)構(gòu)體中,我們將放上一個(gè)雙精度浮點(diǎn)變量video_current_pts 和一個(gè)64位寬整型變量 video_current_pts_time 。時(shí)鐘更新將被放在 video_refresh_timer 函數(shù)中。void video_refresh_timer(void *userdata) if(is->video_st) if(is->pictq_size = 0) schedule_refresh(is, 1); el

32、se vp = &is->pictqis->pictq_rindex; is->video_current_pts = vp->pts;is->video_current_pts_time = av_gettime();不要忘記在 stream_component_open函數(shù)中初始化它:is->video_current_pts_time = av_gettime();現(xiàn)在我們需要一種得到信息的方式:double get_video_clock(VideoState *is) double delta;delta = (av_gettime() -

33、 is->video_current_pts_time) / 1000000.0;return is->video_current_pts + delta;提取時(shí)鐘但是為什么要強(qiáng)制使用視頻時(shí)鐘呢?我們更改視頻同步代碼以致于音頻和視頻不會(huì)試著去相互同步。想像一下我們讓它像ffplay 一樣有-個(gè)命令行參數(shù)。所以讓我們抽象一樣這件事情:我們將做一個(gè)新的封裝函數(shù)get_master_clock,用來(lái)檢測(cè)av_sync_type變量然后決定調(diào)用get_audio_clock還是get_video_clock 或者其它的想使用的獲得時(shí)鐘的函數(shù)。我們甚至可以使用電腦時(shí)鐘,這個(gè)函數(shù)我們叫做get

34、_external_clockenum AV_SYNC_AUDIO_MASTER,AV_SYNC_VIDEO_MASTER,AV_SYNC_EXTERNAL_MASTER, );#define DEFAULT_AV_SYNC_TYPE AV_SYNC_VIDEO_MASTERdouble 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_MAS

35、TER) return get_audio_clock(is); else return get_external_clock(is);)main() is->av_sync_type = D E F AU LT_AV_S YN C_T YP E:同步音頻現(xiàn)在是最難的部分:同步音頻到視頻時(shí)鐘。我們的策略是測(cè)量聲音的位置,把它與視頻時(shí)間比較然后算出我們需要修正多少的樣本數(shù),也 就是說(shuō):我們是否需要通過(guò)丟棄樣本的方式來(lái)加速播放還是需要通過(guò)插值樣本的方式來(lái)放慢播放?我們將在每次處理聲音樣本的時(shí)候運(yùn)行一個(gè)synchronize_audio 的函數(shù)來(lái)正確的收縮或者擴(kuò)展聲音樣本。然而,我們不想在每次

36、發(fā)現(xiàn)有偏差的時(shí)候都進(jìn)行同步,因?yàn)檫@樣會(huì)使同步音頻多于視頻包。所以我們?yōu)楹瘮?shù)synchronize_audio 設(shè)置一個(gè)最小連續(xù)值來(lái)限定需要同步的時(shí)刻,這樣我們就不會(huì)總是在調(diào)整了。當(dāng)然,就像上次那樣,失去同步”意味著聲音時(shí)鐘和視頻時(shí)鐘的差異大于我們的閾值。所以我們將使用一個(gè)分?jǐn)?shù)系數(shù),叫c,所以現(xiàn)在可以說(shuō)我們得到了N個(gè)失去同步的聲音樣本。失去同步的數(shù)量可能會(huì)有很多變化,所以我們要計(jì)算一下失去同步的長(zhǎng)度的均值。例如,第一次調(diào)用的時(shí)候,顯示出來(lái)我們失去同步的長(zhǎng)度為40ms,下次變?yōu)?0ms等等。但是我們不會(huì)使用一個(gè)簡(jiǎn)單的均值,因?yàn)榫嚯x現(xiàn)在最近的值比靠前的值要重要的多。所以我們將使用一個(gè)分?jǐn)?shù)系統(tǒng),叫c,

37、然后用這樣的公式來(lái)計(jì)算差異:diff_sum = new_diff + diff_sum*c。當(dāng)我們準(zhǔn)備好去找平均差異的時(shí)候,我們用簡(jiǎn)單的計(jì)算方式:avg_diff = diff_sum * (1-c)。注意:為什么會(huì)在這里?這個(gè)公式看來(lái)很神奇!嗯,它基本上是一個(gè)使用等比級(jí)數(shù)的加權(quán)平均值。我不知道這是否有名字(我甚至查過(guò)維 基百科!),但是如果想要更多的信息,這里是一個(gè)解釋 或者在 里。下面是我們的函數(shù):intsynchronize_audio(VideoState *is, short *samples, intsamples_size, double pts) int n;double r

38、ef_clock;n = 2 * is->audio_st->codec->channels;if(is->av_sync_type != AV_SYNC_AUDIO_MASTER) double diff, avg_diff;intwanted_size, min_size, max_size, nb_samples;rejclock = get_master_clock(is);diff = get_audio_clock(is) - rejclock;if(diff < AV_NOSYNC_THRESHOLD) / accumulate the diffsi

39、s->audio diff cum = diff + is->audio diff avg coef* is->audio_diffcum;if(is->audio_difLavg_count< AUDIO_DIFF_AVG_NB) is->audio_dif1avg_count+; else avg_diff = is->audio_dif1cum * (1.0 - is->audio_diffavg_coef);) else is->audio_dif1avg_count = 0;is->audio_dif1cum = 0;)re

40、turn samples_size;)現(xiàn)在我們已經(jīng)做得很好;我們已經(jīng)近似的知道如何用視頻或者其它的時(shí)鐘來(lái)調(diào)整音頻了 C并且如何在aShrinking/expanding buffer code 部分來(lái)寫(xiě)上代碼:if(fabs(avg_diff) >= is->audio_diffthreshold) wanted_size = samples_size +(int)(diff * is->audio_st->codec->sample_rate) * n);min_size = samples_size * (100 - SAMPLE_CORRECTION_PER

41、CENT_MAX)/100);max_size = samples_size * (100 + SAMPLE_CORRECTION_PERCENT_MAX)/100);if(wanted_size<min_size) wanted_size = min_size;所以讓我們來(lái)計(jì)算一下要在添加和砍掉多少樣本, else if (wanted_size>max_size) ( wanted_size = max_size;記住audio_length * (sample_rate * # of channels * 2) 就是audio_length 秒時(shí)間的聲音的樣本數(shù)。所以,我們想

42、要的樣本數(shù)就是我們根據(jù) 聲音偏移添加或者減少后的聲音樣本數(shù)。我們也可以設(shè)置一個(gè)范圍來(lái)限定我們一次進(jìn)行修正的長(zhǎng)度,因?yàn)槿绻覀兏淖兊奶?,用戶?huì)聽(tīng) 到刺耳的聲音。修正樣本數(shù)現(xiàn)在我們要真正的修正一下聲音。你可能會(huì)注意到我們的同步函數(shù)synchronize_audio 返回了一個(gè)樣本數(shù),這可以告訴我們有多少個(gè)字節(jié)被送到流中。所以我們只要調(diào)整樣本數(shù)為wanted_size就可以了。這會(huì)讓樣本更小一些。但是如果我們想讓它變大,我們不能只是讓樣本大小變大,因?yàn)樵诰彌_區(qū)中沒(méi)有多余的數(shù)據(jù)!所以我們必需添加上去。但是我們?cè)鯓觼?lái)添加呢?最笨的辦法就是試著來(lái)推算聲音,所以讓 我們用已有的數(shù)據(jù)在緩沖的末尾添加上最后的

43、樣本。if(wanted_size<samples_size) (samples_size = wanted_size; else if(wanted_size>samples_size) (uint8_t *samples_end, *q;intnb;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;)sam

44、ples_size = wanted_size;)現(xiàn)在我們通過(guò)這個(gè)函數(shù)返回的是樣本數(shù)。我們現(xiàn)在要做的是使用它:void audio_callback(void *userdata, Uint8 *stream, intlen) VideoState *is = (VideoState *)userdata;int Ien1, audio_size;double pts;while(len> 0) if(is->audio_buMndex>= is->audio_bu1size) audio_size = audio_decode_frame(is, is->aud

45、io_buf, sizeof(is->audio_buf), &pts);if(audio_size< 0) else if(diff >= sync_threshold) (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_siz

46、e;我們要做的是把函數(shù) synchronize_audio 插入進(jìn)去。(同時(shí),保證在初始化上面變量的時(shí)候檢查一下代碼,這些我沒(méi)有贅述)。結(jié)束之前的最后一件事情:我們需要添加一個(gè)if語(yǔ)句來(lái)保證我們不會(huì)在視頻為主時(shí)鐘的時(shí)候也來(lái)同步視頻。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_THRESHOL

47、D;if(fabs(diff) < AV_NOSYNC_THRESHOLD) if(diff <= -sync_threshold) delay = 0;delay = 2 * delay;添加后就可以了。要保證整個(gè)程序中我沒(méi)有贅述的變量都被初始化過(guò)了。然后編譯它:gcc -o tutorial06 tutorial06.c -lavutil -lavformat -lavcodec -lz -lm'sdl-config -cflags -libs'然后你就可以運(yùn)行它了??爝M(jìn)快退 處理快進(jìn)快退命令現(xiàn)在我們來(lái)為我們的播放器加入一些快進(jìn)和快退的功能,因?yàn)槿绻悴荒苋炙?/p>

48、索一部電影是很讓人討厭的。同時(shí),這將告訴你 av_seek_frame 函數(shù)是多么容易使用。我們將在電影播放中使用左方向鍵和右方向鍵來(lái)表示向后和向前一小段,使用向上和向下鍵來(lái)表示向前和向后一大段。這里一小段是10秒,一大段是60秒。所以我們需要設(shè)置我們的主循環(huán)來(lái)捕捉鍵盤(pán)事件。然而當(dāng)我們捕捉到鍵盤(pán)事件后我們不能直接調(diào)用av_seek_frame函數(shù)。我們要主要的解碼線程decode_thread的循環(huán)中做這些。所以,我們要添加一些變量到大結(jié)構(gòu)體中,用來(lái)包含新的跳轉(zhuǎn)位置和一些跳轉(zhuǎn)標(biāo)志:intseek_req;intseek_flags;int64_t seek_pos;現(xiàn)在讓我們?cè)谥餮h(huán)中捕捉按鍵

49、:for(;)double incr, pos;SDL_WaitEvent(&event);switch(event.type) case SDL_KEYDOWN:switch (event, key. keysym .sym) case SDLK_LEFT:incr = -10.0;gotodo_seek;case SDLK_RIGHT:incr = 10.0;gotodo_seek;case SDLK UP:incr = 60.0;gotodo_seek;case SDLK_DOWN:incr = -60.0;gotodo_seek;do_seek:if(global_video_

50、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;為了檢測(cè)按鍵,我們先查了一下是否有SDL_KEYDOWN 事件。然后我們使用event.key.keysym.sym 來(lái)判斷哪個(gè)按鍵被按下。一旦我們知道了如何來(lái)跳轉(zhuǎn),我們就來(lái)計(jì)算新的時(shí)間,方法為把增加的時(shí)間值加到從函數(shù)get_master_clock中得到的時(shí)間值上。然后我們調(diào)用str

51、eam_seek函數(shù)來(lái)設(shè)置seek_pos等變量。我們把新的時(shí)間轉(zhuǎn)換成為avcodec中的內(nèi)部時(shí)間戳單位。在流中調(diào)用那個(gè)時(shí)間戳將使用幀而不是用秒來(lái)計(jì)算, 公式為seconds = frames * time_base(fps)。默認(rèn)的avcodec值為1,000,000fps (所以2秒的內(nèi)部時(shí)間戳為 2,000,000 )。在后面我們來(lái)看一下為什么要把這個(gè)值進(jìn)行一下轉(zhuǎn)換。這就是我們的stream_seek函數(shù)。請(qǐng)注意我們?cè)O(shè)置了一個(gè)標(biāo)志為后退服務(wù):void stream_seek(VideoState *is, int64_t pos, intrel) (if(!is->seek_req

52、) (is->seek_pos = pos;is->seek_flags = rel< 0 ? AVSEEK_FLAG_BACKWARD : 0;is->seek_req = 1;現(xiàn)在讓我們看一下如果在decode_thread 中實(shí)現(xiàn)跳轉(zhuǎn)。你會(huì)注意到我們已經(jīng)在源文件中標(biāo)記了一個(gè)叫做"seek stuff goes here的部分?,F(xiàn)在我們將把代碼寫(xiě)在這里。跳轉(zhuǎn)是圍繞著av_seek_frame函數(shù)的。這個(gè)函數(shù)用到了一個(gè)格式上下文,一個(gè)流,一個(gè)時(shí)間戳和一組標(biāo)記來(lái)作為它的參數(shù)。這個(gè)函數(shù)將會(huì) 跳轉(zhuǎn)到你所給的時(shí)間戳的位置。時(shí)間戳的單位是你傳遞給函數(shù)的流的時(shí)基time

53、_base。然而,你并不是必需要傳給它一個(gè)流(流可以用-1來(lái)代替)。如果你這樣做了,時(shí)基time_base將會(huì)是avcodec中的內(nèi)部時(shí)間戳單位, 或者是1000000fps。這就是為什么我們?cè)谠O(shè)置seek_pos的時(shí)候會(huì)把位置乘以 AV_TIME_BASER 的原因。但是,如果給av_seek_frame函數(shù)的stream參數(shù)傳遞傳-1,你有時(shí)會(huì)在播放某些文件的時(shí)候遇到問(wèn)題(比較少見(jiàn)),所以我們會(huì)取文件中 的第一個(gè)流并且把它傳遞到av_seek_frame 函數(shù)。不要忘記我們也要把時(shí)間戳timestamp的單位進(jìn)行轉(zhuǎn)化。if(is->seek_req) (intstream_index

54、= -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->streamsstream_index->time_ba

55、se);if(av_seek_frame(is->pFormatCtx, stream_index,seek_target, is->seek_flags) < 0) (fprintf(stderr, "%s: error while seekingn",is->pFormatCtx->filename); else (它基本的動(dòng)作是計(jì)算a*b/c ,但是這個(gè)函數(shù)還是必這里av_rescale_q(a,b,c)是用來(lái)把時(shí)間戳從一個(gè)時(shí)基調(diào)整到另外一個(gè)時(shí)基時(shí)候用的函數(shù)。需的,因?yàn)橹苯佑?jì)算會(huì)有溢出的情況發(fā)生。AV_TIME_BASE_Q 是AV_TI

56、ME_BASE 作為分母后的版本。它們是很不相同的:AV_TIME_BASE * time_in_seconds = avcodec_timestamp 而 AV_TIME_BASE_Q * avcodec_timestamp = time_in_seconds (注意 AV_TIME_BASE_Q 實(shí)際上是一個(gè) AVRational對(duì)象,所以你必需使用avcodec中特定的q函數(shù)來(lái)處理它)。清空我們的緩沖我們已經(jīng)正確設(shè)定了跳轉(zhuǎn)位置,但是我們還沒(méi)有結(jié)束。記住我們有一個(gè)堆放了很多包的隊(duì)列。既然我們跳到了不同的位置,我們必需把隊(duì) 列中的內(nèi)容清空否則電影是不會(huì)跳轉(zhuǎn)的。不僅如此,avcodec也有它自

57、己的內(nèi)部緩沖,也需要每次被清空。要實(shí)現(xiàn)這個(gè),我們需要首先寫(xiě)一個(gè)函數(shù)來(lái)清空我們的包隊(duì)列。然后我們需要一種命令聲音和視頻線程來(lái)清空avcodec內(nèi)部緩沖的辦法。我們可以在清空隊(duì)列后把特定的包放入到隊(duì)列中,然后當(dāng)它們檢測(cè)到特定的包的時(shí)候,它們就會(huì)把自己的內(nèi)部緩沖清空。讓我們開(kāi)始寫(xiě)清空函數(shù)。其實(shí)很簡(jiǎn)單的,所以我直接把代碼寫(xiě)在下面: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);)既然隊(duì)列已經(jīng)清空了,我們放入 滑空包”。但是開(kāi)始我們要定義和創(chuàng)建這個(gè)包:AVPacketflush_pkt;main() av_in it_packet(&fl ush_p k

溫馨提示

  • 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

最新文檔

評(píng)論

0/150

提交評(píng)論