ffmpeg 2.3版本, 關(guān)于ffplay音視頻同步的分析

最近學(xué)習(xí)播放器的一些東西,所以接觸了ffmpeg,看源碼的過程中仓蛆,就想了解一下ffplay是怎么處理音視頻同步的睁冬,之前只大概知道通過pts來進(jìn)行同步倒彰,但對于如何實(shí)現(xiàn)卻不甚了解福铅,所以想借助這個(gè)機(jī)會,從最直觀的代碼入手,詳細(xì)分析一下如何處理音視頻同步誊酌。在看代碼的時(shí)候,剛開始腦袋一片混亂祷嘶,對于ffplay.c里面的各種時(shí)間計(jì)算完全摸不著頭腦宣蠕,在網(wǎng)上查找資料的過程中,發(fā)現(xiàn)關(guān)于分析ffplay音視頻同步的東西比較少拾积,要么就是ffplay版本太過于老舊殉挽,代碼和現(xiàn)在最新版本已經(jīng)不一樣,要么就是簡單的分析了一下拓巧,沒有詳細(xì)的講清楚為什么要這么做斯碌。遂決定,在自己學(xué)習(xí)的過程中肛度,記錄下自己的分析思路傻唾,以供大家指正和參考。我用的ffmpeg版本是2.3承耿, SDL版本為1.2.14冠骄,編譯環(huán)境是windos xp下使用MinGw+msys.

轉(zhuǎn)自:http://www.oooo.club/archives/646/

一、先簡單介紹下ffplay的代碼結(jié)構(gòu)加袋。如下:

1.??????Main函數(shù)中需要注意的有

(1)??????av_register_all接口凛辣,該接口的主要作用是注冊一些muxer、demuxer职烧、coder扁誓、和decoder. 這些模塊將是我們后續(xù)編解碼的關(guān)鍵。每個(gè)demuxer和decoder都對應(yīng)不同的格式蚀之,負(fù)責(zé)不同格式的demux和decode

(2)??????stream_open接口蝗敢,該接口主要負(fù)責(zé)一些隊(duì)列和時(shí)鐘的初始化工作,另外一個(gè)功能就是創(chuàng)建read_thread線程足删,該線程將負(fù)責(zé)文件格式的檢測寿谴,文件的打開以及frame的讀取工作,文件操作的主要工作都在這個(gè)線程里面完成

(3)??????event_loop:事件處理失受,event_loop->refresh_loop_wait_event-> video_refresh,通過這個(gè)順序進(jìn)行視頻的display

2.Read_thread線程

(1)? 該線程主要負(fù)責(zé)文件操作讶泰,包括文件格式的檢測,音視頻流的打開和讀取拂到,它通過av_read_frame讀取完整的音視頻frame packet峻厚,并將它們放入對應(yīng)的隊(duì)列中,等待相應(yīng)的解碼線程進(jìn)行解碼

3. video_thread線程谆焊,該線程主要負(fù)責(zé)將packet隊(duì)列中的數(shù)據(jù)取出并進(jìn)行解碼惠桃,然將解碼完后的picture放入picture隊(duì)列中,等待SDL進(jìn)行渲染

4. sdl_audio_callback,這是ffplay注冊給SDL的回調(diào)函數(shù)辜王,其作用是進(jìn)行音頻的解碼劈狐,并在SDL需要數(shù)據(jù)的時(shí)候,將解碼后的音頻數(shù)據(jù)寫入SDL的緩沖區(qū)呐馆,SDL再調(diào)用audio驅(qū)動的接口進(jìn)行播放肥缔。

5. video_refresh,該接口的作用是從picture隊(duì)列中獲取pic,并調(diào)用SDL進(jìn)行渲染汹来,音視頻同步的關(guān)鍵就在這個(gè)接口中

二续膳、音視頻的同步

要想了解音視頻的同步,首先得去了解一些基本的概念收班,video的frame_rate. Pts, audio的frequency之類的東西坟岔,這些都是比較基礎(chǔ)的,網(wǎng)上資料很多摔桦,建議先搞清楚這些基本概念社付,這樣閱讀代碼才會做到心中有數(shù),好了邻耕,閑話少說鸥咖,開始最直觀的源碼分析吧,如下:

(1)??????首先來說下video和audio 的輸出接口兄世,video輸出是通過調(diào)用video_refresh-> video_display-> video_image_display-> SDL_DisplayYUVOverlay來實(shí)現(xiàn)的啼辣。Audio是通過SDL回調(diào)sdl_audio_callback(該接口在打開音頻時(shí)注冊給SDL)來實(shí)現(xiàn)的。

(2)??????音視頻同步的機(jī)制御滩,據(jù)我所知有3種熙兔,(a)以音頻為基準(zhǔn)進(jìn)行同步(b)以視頻為基準(zhǔn)進(jìn)行同步(c)以外部時(shí)鐘為基準(zhǔn)進(jìn)行同步。Ffplay中默認(rèn)以音頻為基準(zhǔn)進(jìn)行同步艾恼,我們的分析也是基于此,其它兩種暫不分析麸锉。

(3)??????既然視頻和音頻的播放是獨(dú)立的钠绍,那么它們是如何做到同步的,答案就是通過ffplay中音視頻流各自維護(hù)的clock來實(shí)現(xiàn)花沉,具體怎么做柳爽,我們還是來看代碼吧。

(4)??????代碼分析:

(a)??????先來看video_refresh的代碼, 去掉了一些無關(guān)的代碼碱屁,像subtitle和狀態(tài)顯示

static voidvideo_refresh(void *opaque, double *remaining_time)

{

VideoState *is = opaque;

double time;

SubPicture *sp, *sp2;

if (!is->paused &&get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)

check_external_clock_speed(is);

if(!display_disable && is->show_mode != SHOW_MODE_VIDEO &&is->audio_st)

{

time = av_gettime_relative() /1000000.0;

if (is->force_refresh ||is->last_vis_time + rdftspeed < time) {

video_display(is);

is->last_vis_time = time;

}

*remaining_time =FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);

}

if (is->video_st) {

int redisplay = 0;

if (is->force_refresh)

redisplay = pictq_prev_picture(is);

retry:

if (pictq_nb_remaining(is) == 0) {

// nothing to do, no picture todisplay in the queue

} else {

double last_duration, duration, delay;

VideoPicture *vp, *lastvp;

/* dequeue the picture */

lastvp =&is->pictq[is->pictq_rindex];

vp =&is->pictq[(is->pictq_rindex + is->pictq_rindex_shown) % VIDEO_PICTURE_QUEUE_SIZE];

if (vp->serial !=is->videoq.serial) {

pictq_next_picture(is);

is->video_current_pos = -1;

redisplay = 0;

goto retry;

}

/*不管是vp的serial還是queue的serial, 在seek操作的時(shí)候才會產(chǎn)生變化磷脯,更準(zhǔn)確的說,應(yīng)該是packet 隊(duì)列發(fā)生flush操作時(shí)*/

if (lastvp->serial !=vp->serial && !redisplay)

{

is->frame_timer =av_gettime_relative() / 1000000.0;

}

if (is->paused)

goto display;

/*通過pts計(jì)算duration娩脾,duration是一個(gè)videoframe的持續(xù)時(shí)間赵誓,當(dāng)前幀的pts 減去上一幀的pts*/

/* compute nominal last_duration */

last_duration = vp_duration(is,lastvp, vp);

if (redisplay)

{

delay = 0.0;

}

/*音視頻同步的關(guān)鍵點(diǎn)*/

else

delay =compute_target_delay(last_duration, is);

/*time 為系統(tǒng)當(dāng)前時(shí)間,av_gettime_relative拿到的是1970年1月1日到現(xiàn)在的時(shí)間,也就是格林威治時(shí)間*/

time=av_gettime_relative()/1000000.0;

/*frame_timer實(shí)際上就是上一幀的播放時(shí)間俩功,該時(shí)間是一個(gè)系統(tǒng)時(shí)間幻枉,而 frame_timer + delay 實(shí)際上就是當(dāng)前這一幀的播放時(shí)間*/

if (time < is->frame_timer +delay && !redisplay) {

/*remaining 就是在refresh_loop_wait_event 中還需要睡眠的時(shí)間,其實(shí)就是現(xiàn)在還沒到這一幀的播放時(shí)間诡蜓,我們需要睡眠等待*/

*remaining_time =FFMIN(is->frame_timer + delay - time, ?*remaining_time);

return;

}

is->frame_timer += delay;

/*如果下一幀的播放時(shí)間已經(jīng)過了熬甫,并且其和當(dāng)前系統(tǒng)時(shí)間的差值超過AV_SYNC_THRESHOLD_MAX,則將下一幀的播放時(shí)間改為當(dāng)前系統(tǒng)時(shí)間蔓罚,并在后續(xù)判斷是否需 ? ? ? ? ? ? ? 要丟幀椿肩,其目的是立刻處理?*/

if (delay > 0 && time -is->frame_timer > AV_SYNC_THRESHOLD_MAX)

{

is->frame_timer = time;

}

SDL_LockMutex(is->pictq_mutex);

/*視頻幀的pts一般是從0開始,按照幀率往上增加的豺谈,此處pts是一個(gè)相對值郑象,和系統(tǒng)時(shí)間沒有關(guān)系,對于固定fps核无,一般是按照1/frame_rate的速度往上增加扣唱,可變fps暫 ? ? ? ? ? ?時(shí)沒研究*/

if (!redisplay &&!isnan(vp->pts))

/*更新視頻的clock,將當(dāng)前幀的pts和當(dāng)前系統(tǒng)的時(shí)間保存起來团南,這2個(gè)數(shù)據(jù)將和audio? clock的pts 和系統(tǒng)時(shí)間一起計(jì)算delay*/

update_video_pts(is,vp->pts, vp->pos, vp->serial);

SDL_UnlockMutex(is->pictq_mutex);

if (pictq_nb_remaining(is) > 1){

VideoPicture *nextvp =&is->pictq[(is->pictq_rindex + is->pictq_rindex_shown + 1) %VIDEO_PICTURE_QUEUE_SIZE];

duration = vp_duration(is, vp,nextvp);

/*如果延遲時(shí)間超過一幀噪沙,并且允許丟幀,則進(jìn)行丟幀處理*/

if(!is->step &&(redisplay || framedrop>0 || (framedrop && get_master_sync_type(is)!= AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){

if (!redisplay)

is->frame_drops_late++;

/*丟掉延遲的幀吐根,取下一幀*/

pictq_next_picture(is);

redisplay = 0;

goto retry;

}

}

display:

/* display picture */

/*刷新視頻幀*/

if (!display_disable &&is->show_mode == SHOW_MODE_VIDEO)

video_display(is);

pictq_next_picture(is);

if (is->step &&!is->paused)

stream_toggle_pause(is);

}

}

}

(b)??????視頻的播放實(shí)際上是通過上一幀的播放時(shí)間加上一個(gè)延遲來計(jì)算下一幀的計(jì)算時(shí)間的正歼,例如上一幀的播放時(shí)間pre_pts是0,延遲delay為33ms拷橘,那么下一幀的播放時(shí)間則為0+33ms,第一幀的播放時(shí)間我們可以輕松獲取局义,那么后續(xù)幀的播放時(shí)間的計(jì)算,起關(guān)鍵點(diǎn)就在于delay冗疮,我們就是更具delay來控制視頻播放的速度萄唇,從而達(dá)到與音頻同步的目的,那么如何計(jì)算delay术幔?接著看代碼另萤,compute_target_delay接口:

static doublecompute_target_delay(double delay, VideoState *is)

{

double sync_threshold,diff;

/* update delay to followmaster synchronisation source */

/*如果主同步方式不是以視頻為主,默認(rèn)是以audio為主進(jìn)行同步*/

if(get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER) {

/* if video is slave,we try to correct big delays by

duplicating ordeleting a frame */

/*get_clock(&is->vidclk)獲取到的實(shí)際上是:從處理最后一幀開始到現(xiàn)在的時(shí)間加上最后一幀的pts,具體參考set_clock_at 和get_clock的代碼

get_clock(&is->vidclk) ==is->vidclk.pts, av_gettime_relative() / 1000000.0 -is->vidclk.last_updated? +is->vidclk.pts*/

/*driff實(shí)際上就是已經(jīng)播放的最近一個(gè)視頻幀和音頻幀pts的差值+ 兩方系統(tǒng)的一個(gè)差值诅挑,用公式表達(dá)如下:

pre_video_pts: 最近的一個(gè)視頻幀的pts

video_system_time_diff: 記錄最近一個(gè)視頻pts 到現(xiàn)在的時(shí)間四敞,即av_gettime_relative()/ 1000000.0 - is->vidclk.last_updated

pre_audio_pts: 音頻已經(jīng)播放到的時(shí)間點(diǎn),即已經(jīng)播放的數(shù)據(jù)所代表的時(shí)間拔妥,通過已經(jīng)播放的samples可以計(jì)算出已經(jīng)播放的時(shí)間忿危,在sdl_audio_callback中被設(shè)置

audio_system_time_diff: 同video_system_time_diff

最終視頻和音頻的diff可以用下面的公式表示:

diff = (pre_video_pts-pre_audio_pts) +(video_system_time_diff - ?audio_system_time_diff)

如果diff<0, 則說明視頻播放太慢了,如果diff>0,

則說明視頻播放太快没龙,此時(shí)需要通過計(jì)算delay來調(diào)整視頻的播放速度如果

diffAV_SYNC_THRESHOLD_MAX 則不用調(diào)整delay?*/

diff =get_clock(&is->vidclk) - get_master_clock(is);

/* skip or repeatframe. We take into account the

delay to computethe threshold. I still don't know

if it is the bestguess */

sync_threshold=FFMAX(AV_SYNC_THRESHOLD_MIN,FFMIN(AV_SYNC_THRESHOLD_MAX,delay));

if (!isnan(diff)&& fabs(diff) < is->max_frame_duration) {

if (diff <=-sync_threshold)

delay =FFMAX(0, delay + diff);

else if (diff >= sync_threshold&& delay > AV_SYNC_FRAMEDUP_THRESHOLD)

delay = delay+ diff;

else if (diff>= sync_threshold)

delay = 2 *delay;

}

}

av_dlog(NULL, "video:delay=%0.3f A-V=%f\n",

delay, -diff);

return delay;

}

(c)看了以上的分析铺厨,是不是對于如何將視頻同步到音頻有了一個(gè)了解,那么音頻clock是在哪里設(shè)置的呢缎玫?繼續(xù)看代碼,sdl_audio_callback 分析

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)

{

VideoState *is = opaque;

int audio_size, len1;

/*當(dāng)前系統(tǒng)時(shí)間*/

audio_callback_time =av_gettime_relative();

/*len為SDL中audio buffer的大小努释,單位是字節(jié)碘梢,該大小是我們在打開音頻設(shè)備時(shí)設(shè)置*/

while (len > 0) {

/*如果audiobuffer中的數(shù)據(jù)少于SDL需要的數(shù)據(jù),則進(jìn)行解碼*/

if(is->audio_buf_index >= is->audio_buf_size) {

audio_size = audio_decode_frame(is);

if (audio_size <0) {

/* if error,just output silence */

is->audio_buf????? =is->silence_buf;

is->audio_buf_size =sizeof(is->silence_buf) / is->audio_tgt.frame_size *is->audio_tgt.frame_size;

}

else

{

if(is->show_mode != SHOW_MODE_VIDEO)

update_sample_display(is, (int16_t *)is->audio_buf, audio_size);

is->audio_buf_size = audio_size;

}

is->audio_buf_index = 0;

}

/*判斷解碼后的數(shù)據(jù)是否滿足SDL需要*/

len1 =is->audio_buf_size - is->audio_buf_index;

if (len1 > len)

len1 = len;

memcpy(stream,(uint8_t *)is->audio_buf + is->audio_buf_index, len1);

len -= len1;

stream += len1;

is->audio_buf_index+= len1;

}

is->audio_write_buf_size = is->audio_buf_size -is->audio_buf_index;

/* Let's assume the audiodriver that is used by SDL has two periods. */

if(!isnan(is->audio_clock))

{

/*set_clock_at第二個(gè)參數(shù)是計(jì)算音頻已經(jīng)播放的時(shí)間伐蒂,相當(dāng)于video中的上一幀的播放時(shí)間煞躬,如果不同過SDL,例如直接使用linux下的dsp設(shè)備進(jìn)行播放逸邦,那么我們可以通 ? ? ? ? 過ioctl接口獲取到驅(qū)動的audiobuffer中還有多少數(shù)據(jù)沒播放恩沛,這樣,我們通過音頻的采樣率和位深缕减,可以很精確的算出音頻播放到哪個(gè)點(diǎn)了雷客,但是此處的計(jì)算方法有點(diǎn)讓人 ? ? ? ? 看不懂*/

set_clock_at(&is->audclk,is->audio_clock - (double)(2 * is->audio_hw_buf_size +is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec,is->audio_clock_serial, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?audio_callback_time / 1000000.0);

sync_clock_to_slave(&is->extclk, &is->audclk);

}

}

三、總結(jié)

音視頻同步桥狡,拿以音頻為基準(zhǔn)為例搅裙,其實(shí)就是將視頻當(dāng)前的播放時(shí)間和音頻當(dāng)前的播放時(shí)間作比較,如果視頻播放過快裹芝,則通過加大延遲或者重復(fù)播放來使速度降下來部逮,如果慢了,則通過減小延遲或者丟幀來追趕音頻播放的時(shí)間點(diǎn)嫂易,而且關(guān)鍵就在于音視頻時(shí)間的比較以及延遲的計(jì)算兄朋。

四、還存在的問題

關(guān)于sdl_audio_callback中 set_clock_at第二個(gè)參數(shù)的計(jì)算怜械,為什么要那么做颅和,還不是很明白,也有可能那只是一種假設(shè)的算法缕允,只是經(jīng)驗(yàn)峡扩,并沒有什么為什么,但也有可能是其他障本,希望明白的人給解釋一下教届。大家互相學(xué)習(xí),互相進(jìn)步彼绷。

huzn

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市茴迁,隨后出現(xiàn)的幾起案子寄悯,更是在濱河造成了極大的恐慌,老刑警劉巖堕义,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件猜旬,死亡現(xiàn)場離奇詭異脆栋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)洒擦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門椿争,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人熟嫩,你說我怎么就攤上這事秦踪。” “怎么了掸茅?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵椅邓,是天一觀的道長。 經(jīng)常有香客問我昧狮,道長景馁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任逗鸣,我火速辦了婚禮合住,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撒璧。我一直安慰自己透葛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布沪悲。 她就那樣靜靜地躺著获洲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪殿如。 梳的紋絲不亂的頭發(fā)上贡珊,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音涉馁,去河邊找鬼门岔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛烤送,可吹牛的內(nèi)容都是我干的寒随。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼帮坚,長吁一口氣:“原來是場噩夢啊……” “哼妻往!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起试和,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤讯泣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后阅悍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體好渠,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昨稼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拳锚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片假栓。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖霍掺,靈堂內(nèi)的尸體忽然破棺而出匾荆,到底是詐尸還是另有隱情,我是刑警寧澤抗楔,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布棋凳,位于F島的核電站,受9級特大地震影響连躏,放射性物質(zhì)發(fā)生泄漏剩岳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一入热、第九天 我趴在偏房一處隱蔽的房頂上張望拍棕。 院中可真熱鬧,春花似錦勺良、人聲如沸绰播。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蠢箩。三九已至,卻和暖如春事甜,著一層夾襖步出監(jiān)牢的瞬間谬泌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工逻谦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掌实,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓邦马,卻偏偏與公主長得像贱鼻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子滋将,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,700評論 0 3
  • 在上一篇筆記中我們已經(jīng)完成了使用SDL播放聲音和視頻邻悬,聲音播放沒有什么問題,而視頻播放太快随闽,很明顯視頻沒有同步父丰。在...
    762683ff5d3d閱讀 1,329評論 0 1
  • 本文轉(zhuǎn)自:[FFmpeg 入門(5):視頻同步 | www.samirchen.com][2] 視頻如何同步 在之...
    SamirChen閱讀 2,789評論 0 5
  • 根據(jù)ffmpeg官方網(wǎng)站上的例子程序開始學(xué)習(xí)ffmpeg和SDL編程。 SDL是一個(gè)跨平臺的多媒體開發(fā)包橱脸。適用于游...
    762683ff5d3d閱讀 1,801評論 0 2
  • 這是一個(gè)跨平臺的播放器ijkplayer础米,iOS上集成看【如何快速的開發(fā)一個(gè)完整的iOS直播app】(原理篇)。 ...
    FindCrt閱讀 7,068評論 2 46