最近學(xué)習(xí)播放器的一些東西,所以接觸了ffmpeg,看源碼的過(guò)程中,就想了解一下ffplay是怎么處理音視頻同步的,之前只大概知道通過(guò)pts來(lái)進(jìn)行同步,但對(duì)于如何實(shí)現(xiàn)卻不甚了解,所以想借助這個(gè)機(jī)會(huì),從最直觀的代碼入手,詳細(xì)分析一下如何處理音視頻同步。在看代碼的時(shí)候,剛開(kāi)始腦袋一片混亂,對(duì)于ffplay.c里面的各種時(shí)間計(jì)算完全摸不著頭腦,在網(wǎng)上查找資料的過(guò)程中,發(fā)現(xiàn)關(guān)于分析ffplay音視頻同步的東西比較少,要么就是ffplay版本太過(guò)于老舊,代碼和現(xiàn)在最新版本已經(jīng)不一樣,要么就是簡(jiǎn)單的分析了一下,沒(méi)有詳細(xì)的講清楚為什么要這么做。遂決定,在自己學(xué)習(xí)的過(guò)程中,記錄下自己的分析思路,以供大家指正和參考。我用的ffmpeg版本是2.3, SDL版本為1.2.14,編譯環(huán)境是windos xp下使用MinGw+msys.
轉(zhuǎn)自:http://www.oooo.club/archives/646/
一、先簡(jiǎn)單介紹下ffplay的代碼結(jié)構(gòu)。如下:
1.??????Main函數(shù)中需要注意的有
(1)??????av_register_all接口,該接口的主要作用是注冊(cè)一些muxer、demuxer、coder、和decoder. 這些模塊將是我們后續(xù)編解碼的關(guān)鍵。每個(gè)demuxer和decoder都對(duì)應(yīng)不同的格式,負(fù)責(zé)不同格式的demux和decode
(2)??????stream_open接口,該接口主要負(fù)責(zé)一些隊(duì)列和時(shí)鐘的初始化工作,另外一個(gè)功能就是創(chuàng)建read_thread線程,該線程將負(fù)責(zé)文件格式的檢測(cè),文件的打開(kāi)以及frame的讀取工作,文件操作的主要工作都在這個(gè)線程里面完成
(3)??????event_loop:事件處理,event_loop->refresh_loop_wait_event-> video_refresh,通過(guò)這個(gè)順序進(jìn)行視頻的display
2.Read_thread線程
(1)? 該線程主要負(fù)責(zé)文件操作,包括文件格式的檢測(cè),音視頻流的打開(kāi)和讀取,它通過(guò)av_read_frame讀取完整的音視頻frame packet,并將它們放入對(duì)應(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注冊(cè)給SDL的回調(diào)函數(shù),其作用是進(jìn)行音頻的解碼,并在SDL需要數(shù)據(jù)的時(shí)候,將解碼后的音頻數(shù)據(jù)寫(xiě)入SDL的緩沖區(qū),SDL再調(diào)用audio驅(qū)動(dòng)的接口進(jìn)行播放。
5. video_refresh,該接口的作用是從picture隊(duì)列中獲取pic,并調(diào)用SDL進(jìn)行渲染,音視頻同步的關(guān)鍵就在這個(gè)接口中
二、音視頻的同步
要想了解音視頻的同步,首先得去了解一些基本的概念,video的frame_rate. Pts, audio的frequency之類(lèi)的東西,這些都是比較基礎(chǔ)的,網(wǎng)上資料很多,建議先搞清楚這些基本概念,這樣閱讀代碼才會(huì)做到心中有數(shù),好了,閑話(huà)少說(shuō),開(kāi)始最直觀的源碼分析吧,如下:
(1)??????首先來(lái)說(shuō)下video和audio 的輸出接口,video輸出是通過(guò)調(diào)用video_refresh-> video_display-> video_image_display-> SDL_DisplayYUVOverlay來(lái)實(shí)現(xiàn)的。Audio是通過(guò)SDL回調(diào)sdl_audio_callback(該接口在打開(kāi)音頻時(shí)注冊(cè)給SDL)來(lái)實(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ú)立的,那么它們是如何做到同步的,答案就是通過(guò)ffplay中音視頻流各自維護(hù)的clock來(lái)實(shí)現(xiàn),具體怎么做,我們還是來(lái)看代碼吧。
(4)??????代碼分析:
(a)??????先來(lái)看video_refresh的代碼, 去掉了一些無(wú)關(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í)候才會(huì)產(chǎn)生變化,更準(zhǔn)確的說(shuō),應(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;
/*通過(guò)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)在還沒(méi)到這一幀的播放時(shí)間,我們需要睡眠等待*/
*remaining_time =FFMIN(is->frame_timer + delay - time, ?*remaining_time);
return;
}
is->frame_timer += delay;
/*如果下一幀的播放時(shí)間已經(jīng)過(guò)了,并且其和當(dāng)前系統(tǒng)時(shí)間的差值超過(guò)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開(kāi)始,按照幀率往上增加的,此處pts是一個(gè)相對(duì)值,和系統(tǒng)時(shí)間沒(méi)有關(guān)系,對(duì)于固定fps,一般是按照1/frame_rate的速度往上增加,可變fps暫 ? ? ? ? ? ?時(shí)沒(méi)研究*/
if (!redisplay &&!isnan(vp->pts))
/*更新視頻的clock,將當(dāng)前幀的pts和當(dāng)前系統(tǒng)的時(shí)間保存起來(lái),這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í)間超過(guò)一幀,并且允許丟幀,則進(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í)際上是通過(guò)上一幀的播放時(shí)間加上一個(gè)延遲來(lái)計(jì)算下一幀的計(jì)算時(shí)間的,例如上一幀的播放時(shí)間pre_pts是0,延遲delay為33ms,那么下一幀的播放時(shí)間則為0+33ms,第一幀的播放時(shí)間我們可以輕松獲取,那么后續(xù)幀的播放時(shí)間的計(jì)算,起關(guān)鍵點(diǎn)就在于delay,我們就是更具delay來(lái)控制視頻播放的速度,從而達(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í)際上是:從處理最后一幀開(kāi)始到現(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í)間,通過(guò)已經(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, 則說(shuō)明視頻播放太慢了,如果diff>0,
則說(shuō)明視頻播放太快,此時(shí)需要通過(guò)計(jì)算delay來(lái)調(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)看了以上的分析,是不是對(duì)于如何將視頻同步到音頻有了一個(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é),該大小是我們?cè)诖蜷_(kāi)音頻設(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ù)是否滿(mǎn)足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í)間,如果不同過(guò)SDL,例如直接使用linux下的dsp設(shè)備進(jìn)行播放,那么我們可以通 ? ? ? ? 過(guò)ioctl接口獲取到驅(qū)動(dòng)的audiobuffer中還有多少數(shù)據(jù)沒(méi)播放,這樣,我們通過(guò)音頻的采樣率和位深,可以很精確的算出音頻播放到哪個(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í)間作比較,如果視頻播放過(guò)快,則通過(guò)加大延遲或者重復(fù)播放來(lái)使速度降下來(lái),如果慢了,則通過(guò)減小延遲或者丟幀來(lái)追趕音頻播放的時(shí)間點(diǎn),而且關(guān)鍵就在于音視頻時(shí)間的比較以及延遲的計(jì)算。
四、還存在的問(wèn)題
關(guān)于sdl_audio_callback中 set_clock_at第二個(gè)參數(shù)的計(jì)算,為什么要那么做,還不是很明白,也有可能那只是一種假設(shè)的算法,只是經(jīng)驗(yàn),并沒(méi)有什么為什么,但也有可能是其他,希望明白的人給解釋一下。大家互相學(xué)習(xí),互相進(jìn)步。
huzn