ffmpeg 2.3版本號, 關於ffplay音視頻同步的分析


近期學習播放器的一些東西。所以接觸了ffmpeg,看源代碼的過程中。就想了解一下ffplay是怎么處理音視頻同步的,之前僅僅大概知道通過pts來進行同步,但對於怎樣實現卻不甚了解,所以想借助這個機會,從最直觀的代碼入手。具體分析一下怎樣處理音視頻同步。

在看代碼的時候。剛開始腦袋一片混亂,對於ffplay.c里面的各種時間計算全然摸不着頭腦,在網上查找資料的過程中,發現關於分析ffplay音視頻同步的東西比較少。要么就是ffplay版本號太過於老舊。代碼和如今最新版本號已經不一樣,要么就是簡單的分析了一下,沒有具體的講清楚為什么要這么做。

遂決定,在自己學習的過程中。記錄下自己的分析思路,以供大家指正和參考。

我用的ffmpeg版本號是2.3。 SDL版本號為1.2.14,編譯環境是windos xp下使用MinGw+msys. 

一、先簡介下ffplay的代碼結構。

例如以下:

1.      Main函數中須要注意的有

(1)      av_register_all接口,該接口的主要作用是注冊一些muxer、demuxer、coder、和decoder. 這些模塊將是我們興許編解碼的關鍵。每一個demuxer和decoder都相應不同的格式。負責不同格式的demux和decode

(2)      stream_open接口,該接口主要負責一些隊列和時鍾的初始化工作。另外一個功能就是創建read_thread線程,該線程將負責文件格式的檢測。文件的打開以及frame的讀取工作,文件操作的主要工作都在這個線程里面完畢

(3)      event_loop:事件處理。event_loop->refresh_loop_wait_event-> video_refresh,通過這個順序進行視頻的display

2.Read_thread線程

(1)  該線程主要負責文件操作,包含文件格式的檢測。音視頻流的打開和讀取,它通過av_read_frame讀取完整的音視頻frame packet。並將它們放入相應的隊列中,等待相應的解碼線程進行解碼

3. video_thread線程。該線程主要負責將packet隊列中的數據取出並進行解碼,然將解碼完后的picture放入picture隊列中,等待SDL進行渲染

4. sdl_audio_callback。這是ffplay注冊給SDL的回調函數,其作用是進行音頻的解碼。並在SDL須要數據的時候。將解碼后的音頻數據寫入SDL的緩沖區。SDL再調用audio驅動的接口進行播放。

5. video_refresh,該接口的作用是從picture隊列中獲取pic,並調用SDL進行渲染。音視頻同步的關鍵就在這個接口中

        

二、音視頻的同步

         要想了解音視頻的同步,首先得去了解一些主要的概念,video的frame_rate. Pts, audio的frequency之類的東西,這些都是比較基礎的。網上資料非常多,建議先搞清楚這些基本概念,這樣閱讀代碼才會做到心中有數。好了,閑話少說,開始最直觀的源代碼分析吧,例如以下:

(1)      首先來說下video和audio 的輸出接口,video輸出是通過調用video_refresh-> video_display-> video_image_display-> SDL_DisplayYUVOverlay來實現的。Audio是通過SDL回調sdl_audio_callback(該接口在打開音頻時注冊給SDL)來實現的。

(2)      音視頻同步的機制,據我所知有3種,(a)以音頻為基准進行同步(b)以視頻為基准進行同步(c)以外部時鍾為基准進行同步。

Ffplay中默認以音頻為基准進行同步,我們的分析也是基於此。其他兩種暫不分析。

(3)      既然視頻和音頻的播放是獨立的,那么它們是怎樣做到同步的,答案就是通過ffplay中音視頻流各自維護的clock來實現,詳細怎么做。我們還是來看代碼吧。

(4)      代碼分析:

(a)      先來看video_refresh的代碼, 去掉了一些無關的代碼,像subtitle和狀態顯示

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操作的時候才會產生變化,更准確的說。應該是packet 隊列發生flush操作時*/

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

            {

                is->frame_timer =av_gettime_relative() / 1000000.0;

            }

            if (is->paused)

                goto display;

 

            /*通過pts計算duration,duration是一個videoframe的持續時間,當前幀的pts 減去上一幀的pts*/

            /* compute nominal last_duration */

            last_duration = vp_duration(is,lastvp, vp);

            if (redisplay)

            {

                delay = 0.0;

            }

           

            /*音視頻同步的關鍵點*/

            else

                delay =compute_target_delay(last_duration, is);

 

            /*time 為系統當前時間。av_gettime_relative拿到的是1970年1月1日到如今的時間,也就是格林威治時間*/

            time=av_gettime_relative()/1000000.0;

 

            /*frame_timer實際上就是上一幀的播放時間。該時間是一個系統時間,而 frame_timer + delay 實際上就是當前這一幀的播放時間*/

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

                                               /*remaining 就是在refresh_loop_wait_event 中還須要睡眠的時間,事實上就是如今還沒到這一幀的播放時間,我們須要睡眠等待*/

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

                return;

            }

            is->frame_timer += delay;

            /*假設下一幀的播放時間已經過了,而且其和當前系統時間的差值超過AV_SYNC_THRESHOLD_MAX。則將下一幀的播放時間改為當前系統時間,並在興許推斷是否需               要丟幀。其目的是立馬處理?

*/

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

            {

                is->frame_timer = time;

            }

           

            SDL_LockMutex(is->pictq_mutex);

            /*視頻幀的pts通常是從0開始。依照幀率往上添加的,此處pts是一個相對值,和系統時間沒有關系。對於固定fps,通常是依照1/frame_rate的速度往上添加,可變fps暫            時沒研究*/

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

                /*更新視頻的clock,將當前幀的pts和當前系統的時間保存起來,這2個數據將和audio  clock的pts 和系統時間一起計算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);

                /*假設延遲時間超過一幀。而且同意丟幀。則進行丟幀處理*/

                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)      視頻的播放實際上是通過上一幀的播放時間加上一個延遲來計算下一幀的計算時間的,比如上一幀的播放時間pre_pts是0。延遲delay為33ms,那么下一幀的播放時間則為0+33ms,第一幀的播放時間我們能夠輕松獲取。那么興許幀的播放時間的計算。起關鍵點就在於delay,我們就是更具delay來控制視頻播放的速度,從而達到與音頻同步的目的,那么怎樣計算delay?接着看代碼。compute_target_delay接口:

 static doublecompute_target_delay(double delay, VideoState *is)

{

    double sync_threshold,diff;

 

    /* update delay to followmaster synchronisation source */

    /*假設主同步方式不是以視頻為主。默認是以audio為主進行同步*/

    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)獲取到的實際上是:從處理最后一幀開始到如今的時間加上最后一幀的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實際上就是已經播放的近期一個視頻幀和音頻幀pts的差值+ 雙方系統的一個差值,用公式表達例如以下:

        pre_video_pts: 近期的一個視頻幀的pts

        video_system_time_diff: 記錄近期一個視頻pts 到如今的時間,即av_gettime_relative()/ 1000000.0 - is->vidclk.last_updated

        pre_audio_pts: 音頻已經播放到的時間點,即已經播放的數據所代表的時間,通過已經播放的samples能夠計算出已經播放的時間。在sdl_audio_callback中被設置

        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,

        則說明視頻播放太快。此時須要通過計算delay來調整視頻的播放速度假設

        diff<AV_SYNC_THRESHOLD_MIN || diff>AV_SYNC_THRESHOLD_MAX 則不用調整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)看了以上的分析,是不是對於怎樣將視頻同步到音頻有了一個了解,那么音頻clock是在哪里設置的呢?繼續看代碼。sdl_audio_callback 分析

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

{

       VideoState *is = opaque;

        int audio_size, len1;

        /*當前系統時間*/

        audio_callback_time =av_gettime_relative();

        /*len為SDL中audio buffer的大小,單位是字節。該大小是我們在打開音頻設備時設置*/

        while (len > 0) {

        /*假設audiobuffer中的數據少於SDL須要的數據,則進行解碼*/

        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;

        }

        /*推斷解碼后的數據是否滿足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第二個參數是計算音頻已經播放的時間,相當於video中的上一幀的播放時間,假設不同過SDL。比如直接使用linux下的dsp設備進行播放,那么我們能夠通         過ioctl接口獲取到驅動的audiobuffer中還有多少數據沒播放,這樣,我們通過音頻的採樣率和位深,能夠非常精確的算出音頻播放到哪個點了,可是此處的計算方法有點讓人         看不懂*/

        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);

    }

}

 

三、總結

         音視頻同步。拿以音頻為基准為例,事實上就是將視頻當前的播放時間和音頻當前的播放時間作比較,假設視頻播放過快,則通過加大延遲或者反復播放來使速度降下來,假設慢了。則通過減小延遲或者丟幀來追趕音頻播放的時間點,並且關鍵就在於音視頻時間的比較以及延遲的計算。

 

四、還存在的問題

         關於sdl_audio_callback中 set_clock_at第二個參數的計算。為什么要那么做。還不是非常明確,也有可能那僅僅是一種如果的算法。僅僅是經驗,並沒有什么為什么。但也有可能是其它。希望明確的人給解釋一下。大家互相學習。互相進步。

                                                                                                                                           

                                                                                                                                                                                                                                                                   鄧旭光 於2015年3月17日

Ps:轉摘請注明出處        


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM