溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎樣進(jìn)行ffplay播放器的源代碼分析

發(fā)布時(shí)間:2021-12-03 17:09:10 來(lái)源:億速云 閱讀:153 作者:柒染 欄目:云計(jì)算

今天就跟大家聊聊有關(guān)怎樣進(jìn)行ffplay播放器的源代碼分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

視頻播放器原理其實(shí)大抵相同,都是對(duì)音視頻幀序列的控制。只是一些播放器在音視頻同步上可能做了更為復(fù)雜的幀預(yù)測(cè)技術(shù),來(lái)保證音頻和視頻有更好的同步性。

ffplay是FFMpeg自帶的播放器,使用了 ffmpeg解碼庫(kù)和用于視頻渲染顯示的sdl 庫(kù),也是業(yè)界播放器最初參考的設(shè)計(jì)標(biāo)準(zhǔn)。本文對(duì)ffplay源碼進(jìn)行分析,試圖用更基礎(chǔ)而系統(tǒng)的方法,來(lái)嘗試解開(kāi)播放器的音視頻同步,以及播放/暫停、快進(jìn)/后退的控制原理。

由于FFMpeg本身的跨平臺(tái)特性,相比在移動(dòng)端看音視頻代碼,在PC端利用VS查看和調(diào)試代碼,分析播放器原理,要高效迅速很多。

由于FFMpeg官方提供的ffmplay在console中進(jìn)行使用不夠直觀,本文直接分析CSDN上將ffplay移植到VC的代碼(ffplay for MFC)進(jìn)行分析。

文章目錄:
一、初探mp4文件
二、以最簡(jiǎn)單播放器開(kāi)始:FFmpeg解碼 + SDL顯示
三、先拋五個(gè)問(wèn)題
四、ffplay代碼總體結(jié)構(gòu)
五、視頻播放器的操作控制
5.1 ffplay所定義的關(guān)鍵結(jié)構(gòu)體VideoState
5.2 補(bǔ)充基礎(chǔ)知識(shí)——PTS和DTS
5.2 如何控制音視頻同步
5.4 如何控制視頻的播放和暫停?
5.5 逐幀播放是如何做的?
5.6 快進(jìn)和后退
六、 這次分析ffplay代碼的反省總結(jié)

一、初探mp4文件

為了讓大家對(duì)視頻文件有一個(gè)初步認(rèn)識(shí),首先來(lái)看對(duì)一個(gè)MP4文件的簡(jiǎn)單分析,如圖1。

怎樣進(jìn)行ffplay播放器的源代碼分析
圖1 對(duì)MP4文件解參

從圖一我們知道,每個(gè)視頻文件都會(huì)有特定的封裝格式、比特率、時(shí)長(zhǎng)等信息。視頻解復(fù)用之后,就劃分為video_stream和audio_stream,分別對(duì)應(yīng)視頻流和音頻流。

解復(fù)用之后的音視頻有自己獨(dú)立的參數(shù),視頻參數(shù)包括編碼方式、采樣率、畫(huà)面大小等,音頻參數(shù)包括采樣率、編碼方式和聲道數(shù)等。

對(duì)解復(fù)用之后的音頻和視頻Packet進(jìn)行解碼之后,就變成原始的音頻(PWM)和視頻(YUV/RGB)數(shù)據(jù),才可以在進(jìn)行顯示和播放。

其實(shí)這已經(jīng)差不多涉及到了,視頻解碼播放的大部分流程,整個(gè)視頻播放的流程如圖2所示。

怎樣進(jìn)行ffplay播放器的源代碼分析
圖3 播放器流程圖(圖源見(jiàn)水?。?/p>

流程圖說(shuō)明如下:

1.FFmpeg初始化的代碼比較固定,主要目的就是為了設(shè)置AVFormatContext實(shí)例中相關(guān)成員變量的值,調(diào)用av_register_all、avformat_open_input av_find_stream_info和avcodec_find_decoder等函數(shù)。

  如圖4所示,初始化之后的AVFormatContext實(shí)例里面具體的值,調(diào)用av_find_stream_info就是找到文件中的音視頻流數(shù)據(jù),對(duì)其中的streams(包含音頻、視頻流)變量進(jìn)行初始化。


圖4 AVFormatContext初始化實(shí)例

2.av_read_frame不斷讀取stream中的下一幀,對(duì)其進(jìn)行解復(fù)用得到視頻的AVPacket,隨后調(diào)用avcodec_decode_video2是視頻幀AVPacket進(jìn)行解碼,得到圖像幀AVFrame。

3.得到AVFrame之后,接下來(lái)就是放到SDL中進(jìn)行渲染顯示了,也很簡(jiǎn)單,流程見(jiàn)下面代碼注釋?zhuān)?/p>

SDL_Overlay *bmp;
//將解析得到的AVFrame的數(shù)據(jù)拷貝到SDL_Overlay實(shí)例當(dāng)中
SDL_LockYUVOverlay(bmp);
bmp->pixels[0]=pFrameYUV->data[0];
bmp->pixels[2]=pFrameYUV->data[1];
bmp->pixels[1]=pFrameYUV->data[2];    
bmp->pitches[0]=pFrameYUV->linesize[0];
bmp->pitches[2]=pFrameYUV->linesize[1];  
bmp->pitches[1]=pFrameYUV->linesize[2];

SDL_UnlockYUVOverlay(bmp);
//設(shè)置SDL_Rect,因?yàn)樯婕暗狡鹗键c(diǎn)和顯示大小,用rect進(jìn)行表示。
SDL_Rect rect;
rect.x = 0;   
rect.y = 0;   
rect.w = pCodecCtx->width; 
rect.h = pCodecCtx->height;   
//將SDL_Overlay數(shù)據(jù)顯示到SDL_Surface當(dāng)中。
SDL_DisplayYUVOverlay(bmp, &rect);
//延時(shí)40ms,留足ffmpeg取到下一幀并解碼該幀的時(shí)間,隨后繼續(xù)讀取下一幀
SDL_Delay(40);

由上面的原理可知,從幀流中獲取到AVPacket,并且解碼得到AVFrame,渲染到SDL窗口中。

怎樣進(jìn)行ffplay播放器的源代碼分析
圖5 視頻播放狀態(tài)圖

對(duì)視頻播放的流程總結(jié)一下就是:讀取下一幀——>解碼——>播放——>不斷往復(fù),狀態(tài)圖如圖5所示。

三、先拋五個(gè)問(wèn)題

本文還是以問(wèn)題拋問(wèn)題的思路,以逐步對(duì)每個(gè)問(wèn)題進(jìn)行原理性分析,加深對(duì)音視頻解碼和播放的認(rèn)識(shí)。以下這些問(wèn)題也是每一個(gè)播放器所需要面對(duì)的基礎(chǔ)問(wèn)題和原理:

1.我們?cè)谟^看電影時(shí)發(fā)現(xiàn),電影可以更換不同字幕,甚至不同音頻,比如中英文字幕和配音,最后在同一個(gè)畫(huà)面中進(jìn)行顯示,視頻關(guān)于畫(huà)面、字幕和聲音是如何組合的? 
其實(shí)每一個(gè)視頻文件,讀取出來(lái)之后發(fā)現(xiàn),都會(huì)被區(qū)分不同的流。為了讓大家有更具體的理解,以FFMpeg中的代碼為例,AVMediaType定義了具體的流類(lèi)型:

enum AVMediaType {

    AVMEDIA_TYPE_VIDEO,  //視頻流

    AVMEDIA_TYPE_AUDIO, //音頻流

    AVMEDIA_TYPE_SUBTITLE, //字幕流

};

利用av_read_frame讀取出音視頻幀之后,隨后就利用avcodec_decode_video2對(duì)視頻捷星解碼,或者調(diào)用avcodec_decode_audio4對(duì)音頻進(jìn)行解碼,得到可以供渲染和顯示的音視頻原始數(shù)據(jù)。

圖像和字幕都將會(huì)以Surface或者texture的形式,就像Android中的SurfaceFlinger,將畫(huà)面不同模塊的顯示進(jìn)行組合,生成一幅新的圖像,顯示在視頻畫(huà)面中。

2.既然視頻有幀率的概念,音頻有采樣率的概念,是否直接利用幀率就可以控制音視頻的同步了呢? 
每一個(gè)視頻幀和音頻幀在時(shí)域上都對(duì)應(yīng)于一個(gè)時(shí)間點(diǎn),按道理來(lái)說(shuō)只要控制每一個(gè)音視頻幀的播放時(shí)間,就可以實(shí)現(xiàn)同步。

但實(shí)際上,對(duì)每一幀顯示的時(shí)間上的精確控制是很難的,更何況音頻和視頻的解碼所需時(shí)間不同,極容易引起音視頻在時(shí)間上的不同步。

所以,播放器具體是如何做音視頻同步的呢?

3.視頻的音頻流、視頻流和字幕流,他們?cè)跁r(shí)間上是連續(xù)的還是離散的?不同流的幀數(shù)相同嗎? 
由于計(jì)算機(jī)只能數(shù)字模擬離散的世界,所以在時(shí)間上肯定是離散的。那既然是離散的,他們的幀數(shù)是否相同呢?

視頻可以理解為諸多音頻幀、視頻幀和字幕幀在時(shí)間上的序列,他們?cè)跁r(shí)間上的時(shí)長(zhǎng),跟視頻總時(shí)長(zhǎng)是相同的,但是由于每個(gè)幀解碼時(shí)間不同,必然會(huì)導(dǎo)致他們?cè)诿繋臅r(shí)間間隔不相同。

音頻原始數(shù)據(jù)本身就是采樣數(shù)據(jù),所以是有固定時(shí)鐘周期。但是視頻假如想跟音頻進(jìn)行同步的話,可能會(huì)出現(xiàn)跳幀的情況,每個(gè)視頻幀播放時(shí)間差,都會(huì)起伏不定,不是恒定周期。

所以結(jié)論是,三者在視頻總時(shí)長(zhǎng)上播放的幀數(shù)肯定是不一樣的。

4.視頻播放就是一系列的連續(xù)幀不停渲染。對(duì)視頻的控制操作包括:暫停和播放、快進(jìn)和后退。那有沒(méi)有想過(guò),每次快進(jìn)/后退的幅度,以時(shí)間為量度好,還是以每次跳躍的幀數(shù),就是每次快進(jìn)是前進(jìn)多長(zhǎng)時(shí)間,還是前進(jìn)多少幀。 時(shí)間 VS 幀數(shù)? 
由上面問(wèn)題分析,我們知道,視頻是以音頻流、視頻流和字幕流進(jìn)行分流的,假如以幀數(shù)為基礎(chǔ),由于不同流的幀數(shù)量不一定相同,以幀數(shù)為單位,很容易導(dǎo)致三個(gè)流播放的不一致。

因此以時(shí)間為量度,相對(duì)更好,直接搜尋mp4文件流,當(dāng)前播放時(shí)間的前進(jìn)或后退時(shí)長(zhǎng)的seek時(shí)間點(diǎn),隨后重新對(duì)文件流進(jìn)行分流解析,就可以達(dá)到快進(jìn)和后退之后的音視頻同步效果。

我們可以看到絕大部分播放器,快進(jìn)/倒退都是以時(shí)長(zhǎng)為步進(jìn)的,我們可以看看ffplay是怎么樣的,以及是如何實(shí)現(xiàn)的。

5.上一節(jié)中,實(shí)現(xiàn)的簡(jiǎn)單播放器,解碼和播放都是在同一個(gè)線程中,解碼速度直接影響播放速度,從而將直接造成播放不流暢的問(wèn)題。那如何在解碼可能出現(xiàn)速度不均勻的情況下,進(jìn)行流暢的視頻播放呢?

很容易想到,引入緩沖隊(duì)列,將視頻圖像渲染顯示和視頻解碼作為兩個(gè)線程,視頻解碼線程往隊(duì)列中寫(xiě)數(shù)據(jù),視頻渲染線程從隊(duì)列中讀取數(shù)據(jù)進(jìn)行顯示,這樣就可以保證視頻是可以流程播放的。

因此需要采用音頻幀、視頻幀和字幕幀的三個(gè)緩沖隊(duì)列,那如何保證音視頻播放的同步呢?

PTS是視頻幀或者音頻幀的顯示時(shí)間戳,究竟是如何利用起來(lái)的,從而控制視頻幀、音頻幀以及字幕幀的顯示時(shí)刻呢?

那我們就可以探尋ffplay,究竟是如何去做緩沖隊(duì)列控制的。

所有以上五個(gè)問(wèn)題,我們都將在對(duì)ffplay源代碼的探尋中,逐步找到更具體的解答。

四、ffplay代碼總體結(jié)構(gòu)


圖6 ffplay代碼總體流程

網(wǎng)上有人做了ffplay的總體流程圖,如圖6。有了這幅圖,代碼看起來(lái),就會(huì)輕松了很多。流程中具體包含的細(xì)節(jié)如下:

1.啟動(dòng)定時(shí)器Timer,計(jì)時(shí)器40ms刷新一次,利用SDL事件機(jī)制,觸發(fā)從圖像幀隊(duì)列中讀取數(shù)據(jù),進(jìn)行渲染顯示;

2.stream_componet_open函數(shù)中,av_read_frame()讀取到AVPacket,隨后放入到音頻、視頻或字幕Packet隊(duì)列中;

3.video_thread,從視頻packet隊(duì)列中獲取AVPacket并進(jìn)行解碼,得到AVFrame圖像幀,放到VideoPicture隊(duì)列中。

4..audio_thread線程,同video_thread,對(duì)音頻Packet進(jìn)行解碼;

5.subtitle_thread線程,同video_thread,對(duì)字幕Packet進(jìn)行解碼。

五、視頻播放器的操作控制

視頻播放器的操作包括播放/暫停、快進(jìn)/倒退、逐幀播放等,這些操作的實(shí)現(xiàn)原理是什么呢,下面對(duì)其從代碼層面逐個(gè)進(jìn)行分析。

5.1 ffplay所定義的關(guān)鍵結(jié)構(gòu)體VideoState

與FFmpeg解碼類(lèi)似,定義了一個(gè)AVFormatContext結(jié)構(gòu)體,用于存儲(chǔ)文件名、音視頻流、解碼器等字段,供全局進(jìn)行訪問(wèn)。

ffplay也定義了一個(gè)結(jié)構(gòu)體VideoState,通過(guò)對(duì)VideoState的分析,就可以大體知道播放器基本實(shí)現(xiàn)原理。

typedef struct VideoState {
       // Demux解復(fù)用線程,讀視頻文件stream線程,得到AVPacket,并對(duì)packet入棧
       SDL_Thread *read_tid;  
       //視頻解碼線程,讀取AVPacket,decode 爬出可以成AVFrame并入隊(duì)
       SDL_Thread *video_tid;
       //視頻播放刷新線程,定時(shí)播放下一幀
       SDL_Thread *refresh_tid;
       int paused;  //控制視頻暫?;虿シ艠?biāo)志位
       int seek_req;  //進(jìn)度控制標(biāo)志
       int seek_flags;

       AVStream *audio_st;   //音頻流
       PacketQueue audioq;  //音頻packet隊(duì)列
       double audio_current_pts;  //當(dāng)前音頻幀顯示時(shí)間

       AVStream *subtitle_st; //字幕流
       PacketQueue subtitleq;//字幕packet隊(duì)列 

       AVStream *video_st; //視頻流

       PacketQueue videoq;//視頻packet隊(duì)列
       double video_current_pts; ///當(dāng)前視頻幀pts
       double video_current_pts_drift;  

       VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];  //解碼后的圖像幀隊(duì)列
}

從VideoState結(jié)構(gòu)體中可以看出:

1.解復(fù)用、視頻解碼和視頻刷新播放,分屬三個(gè)線程中,并行控制;

2.音頻流、視頻流、字幕流,都有自己的緩沖隊(duì)列,供不同線程讀寫(xiě),并且有自己的當(dāng)前幀的PTS;

3.解碼后的圖像幀單獨(dú)放在pictq隊(duì)列當(dāng)中,SDL利用其進(jìn)行顯示。

其中PTS是什么呢,這在音視頻中是一個(gè)很重要的概念,直接決定視頻幀或音頻幀的顯示時(shí)間,下面具體介紹一下。

5.2 補(bǔ)充基礎(chǔ)知識(shí)——PTS和DTS

怎樣進(jìn)行ffplay播放器的源代碼分析
圖7 音視頻解碼分析

圖7為輸出的音頻幀和視頻幀序列,每一幀都有PTS和DTS標(biāo)簽,這兩個(gè)標(biāo)簽究竟是什么意思呢?
DTS(Decode Time Stamp)和PTS(Presentation Time Stamp)都是時(shí)間戳,前者是解碼時(shí)間,后者是顯示時(shí)間,都是為視頻幀、音頻幀打上的時(shí)間標(biāo)簽,以更有效地支持上層應(yīng)用的同步機(jī)制。

也就是說(shuō),視頻幀或者音頻在解碼時(shí),會(huì)記錄其解碼時(shí)間,視頻幀的播放時(shí)間依賴(lài)于PTS。

對(duì)于聲音來(lái)說(shuō) ,這兩個(gè)時(shí)間標(biāo)簽是相同的;但對(duì)于某些視頻編碼格式,由于采用了雙向預(yù)測(cè)技術(shù),DTS會(huì)設(shè)置一定的超時(shí)或延時(shí),保證音視頻的同步,會(huì)造成DTS和PTS的不一致。

5.3 如何控制音視頻同步

我們已經(jīng)知道,視頻幀的播放時(shí)間其實(shí)依賴(lài)pts字段的,音頻和視頻都有自己?jiǎn)为?dú)的pts。但pts究竟是如何生成的呢,假如音視頻不同步時(shí),pts是否需要?jiǎng)討B(tài)調(diào)整,以保證音視頻的同步?

下面先來(lái)分析,如何控制視頻幀的顯示時(shí)間的:

static void video_refresh(void *opaque){ 

  //根據(jù)索引獲取當(dāng)前需要顯示的VideoPicture
  VideoPicture *vp = &is->pictq[is->pictq_rindex];

  if (is->paused)
      goto display; //只有在paused的情況下,才播放圖像

  // 將當(dāng)前幀的pts減去上一幀的pts,得到中間時(shí)間差

  last_duration = vp->pts - is->frame_last_pts;

  //檢查差值是否在合理范圍內(nèi),因?yàn)閮蓚€(gè)連續(xù)幀pts的時(shí)間差,不應(yīng)該太大或太小

  if (last_duration > 0 && last_duration < 10.0) {
    /* if duration of the last frame was sane, update last_duration in video state */
    is->frame_last_duration = last_duration;
  }

  //既然要音視頻同步,肯定要以視頻或音頻為參考標(biāo)準(zhǔn),然后控制延時(shí)來(lái)保證音視頻的同步,
  //這個(gè)函數(shù)就做這個(gè)事情了,下面會(huì)有分析,具體是如何做到的。
  delay = compute_target_delay(is->frame_last_duration, is);

  //獲取當(dāng)前時(shí)間
  time= av_gettime()/1000000.0;

   //假如當(dāng)前時(shí)間小于frame_timer + delay,也就是這幀改顯示的時(shí)間超前,還沒(méi)到,就直接返回
  if (time < is->frame_timer + delay) 
      return;

  //根據(jù)音頻時(shí)鐘,只要需要延時(shí),即delay大于0,就需要更新累加到frame_timer當(dāng)中。
  if (delay > 0)
       /更新frame_timer,frame_time是delay的累加值
       is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay));

  SDL_LockMutex(is->pictq_mutex);

  //更新is當(dāng)中當(dāng)前幀的pts,比如video_current_pts、video_current_pos 等變量
  update_video_pts(is, vp->pts, vp->pos);

  SDL_UnlockMutex(is->pictq_mutex);

display:
  /* display picture */
  if (!display_disable)
    video_display(is);
}

函數(shù)compute_target_delay根據(jù)音頻的時(shí)鐘信號(hào),重新計(jì)算了延時(shí),從而達(dá)到了根據(jù)音頻來(lái)調(diào)整視頻的顯示時(shí)間,從而實(shí)現(xiàn)音視頻同步的效果。

static double compute_target_delay(double delay, VideoState *is)
{
    double sync_threshold, diff;
   //因?yàn)橐纛l是采樣數(shù)據(jù),有固定的采用周期并且依賴(lài)于主系統(tǒng)時(shí)鐘,要調(diào)整音頻的延時(shí)播放較難控制。所以實(shí)際場(chǎng)合中視頻同步音頻相比音頻同步視頻實(shí)現(xiàn)起來(lái)更容易。
   if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) ||
     is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) {

       //獲取當(dāng)前視頻幀播放的時(shí)間,與系統(tǒng)主時(shí)鐘時(shí)間相減得到差值
       diff = get_video_clock(is) - get_master_clock(is);
       sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay);

      //假如當(dāng)前幀的播放時(shí)間,也就是pts,滯后于主時(shí)鐘
      if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
         if (diff <= -sync_threshold)
             delay = 0;
      //假如當(dāng)前幀的播放時(shí)間,也就是pts,超前于主時(shí)鐘,那就需要加大延時(shí)
      else if (diff >= sync_threshold)
        delay = 2 * delay;
      }

   }
   return delay;
}


圖8 音視頻幀顯示序列

所以這里的流程就很簡(jiǎn)單了,圖8簡(jiǎn)單畫(huà)了一個(gè)音視頻幀序列,想表達(dá)的意思是,音頻幀數(shù)量和視頻幀數(shù)量不一定對(duì)等,另外每個(gè)音頻幀的顯示時(shí)間在時(shí)間上幾乎對(duì)等,每個(gè)視頻幀的顯示時(shí)間,會(huì)根據(jù)具體情況有延時(shí)顯示,這個(gè)延時(shí)就是有上面的compute_target_delay函數(shù)計(jì)算出來(lái)的。

計(jì)算延遲后,更新pts的代碼如下:

static void update_video_pts(VideoState *is, double pts, int64_t pos) {

    double time = av_gettime() / 1000000.0;
    /* update current video pts */
    is->video_current_pts = pts;
    is->video_current_pts_drift = is->video_current_pts - time;
    is->video_current_pos = pos;
    is->frame_last_pts = pts;
}

整個(gè)流程可以概括為:
顯示第一幀視頻圖像;
根據(jù)音頻信號(hào),計(jì)算出第二幀的delay時(shí)間,更新該幀的pts。
當(dāng)pts到達(dá)后,顯示第二幀視頻圖像。
重復(fù)以上步驟,到最后一幀
也許在這里仍然會(huì)讓人很困惑,為什么單單根據(jù)主時(shí)鐘,就可以播放下一幀所需要的延時(shí)呢? 
其實(shí)視頻是具備一定長(zhǎng)度的播放流,具體可以分為音頻流、視頻流和字幕流,三者同時(shí)在一起播放形成了視頻,當(dāng)然他們總的播放時(shí)間是跟視頻文件的播放時(shí)長(zhǎng)是一樣的。

由于音頻流本身是pwm采樣數(shù)據(jù),以固定的頻率播放,這個(gè)頻率是跟主時(shí)鐘相同或是它的分頻,從時(shí)間的角度來(lái)看,每個(gè)音頻幀是自然均勻流逝。

所以音頻的話,直接按照主時(shí)鐘或其分頻走就可以了。

視頻,要根據(jù)自己的顯示時(shí)間即pts,跟主時(shí)鐘當(dāng)前的時(shí)間進(jìn)行對(duì)比,確定是超前還是滯后于系統(tǒng)時(shí)鐘,從而確定延時(shí),隨后進(jìn)行準(zhǔn)確的播放,這樣就可以保證音視頻的同步了。

那接下來(lái),還有一個(gè)問(wèn)題,計(jì)算出延時(shí)之后,難道需要sleep一下做延遲顯示嗎? 
其實(shí)并不是如此,上面分析我們知道delay會(huì)更新到當(dāng)前需要更新視頻幀的pts (video_current_pts),對(duì)當(dāng)前AVFrame進(jìn)行顯示前,先檢測(cè)其pts時(shí)間,假如還沒(méi)到,就不進(jìn)行顯示了,直接return。直到下一次刷新,重新進(jìn)行檢測(cè)(ffplay采用的40ms定時(shí)刷新)。

代碼如下,未到更新后的pts時(shí)間( is->frame_timer + dela),直接return:

if (av_gettime()/1000000.0 < is->frame_timer + delay)  
    return;

那接下來(lái)就是分析如何播放視頻幀,就很簡(jiǎn)單了,只是這里多加了一個(gè)字幕流的處理:

static void video_image_display(VideoState *is)
{
    VideoPicture *vp;
   SubPicture *sp;
   AVPicture pict;
   SDL_Rect rect;
   int i;
   vp = &is->pictq[is->pictq_rindex];
   if (vp->bmp) {
       //字幕處理
       if (is->subtitle_st) {}                  
   }

   //計(jì)算圖像的顯示區(qū)域
   calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp);

   //顯示圖像
   SDL_DisplayYUVOverlay(vp->bmp, &rect);

   //將pic隊(duì)列的指針向前移動(dòng)一個(gè)位置
   pictq_next_picture(is);

}

VIDEO_PICTURE_QUEUE_SIZE 只設(shè)置為4,很快就會(huì)用完了。數(shù)據(jù)滿了如何重新更新呢? 
一旦檢測(cè)到超出隊(duì)列大小限制,就處于等待狀態(tài),直到pictq被取出消費(fèi),從而避免開(kāi)啟播放器,就把整個(gè)文件全部解碼完,這樣會(huì)代碼會(huì)很吃?xún)?nèi)存。

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts1, int64_t pos){

/* keep the last already displayed picture in the queue */
while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE - 2 &&
      !is->videoq.abort_request) {

    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
   }
   SDL_UnlockMutex(is->pictq_mutex);
}

5.4 如何控制視頻的播放和暫停?

static void stream_toggle_pause(VideoState *is)
{

    if (is->paused) {
       //由于frame_timer記下來(lái)視頻從開(kāi)始播放到當(dāng)前幀播放的時(shí)間,所以暫停后,必須要將暫停的時(shí)間( is->video_current_pts_drift - is->video_current_pts)一起累加起來(lái),并加上drift時(shí)間。

     is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts;

     if (is->read_pause_return != AVERROR(ENOSYS)) {
     //并更新video_current_pts
        is->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0;

       }
    //drift其實(shí)就是當(dāng)前幀的pts和當(dāng)前時(shí)間的時(shí)間差
    is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0;
    }

    //paused取反,paused標(biāo)志位也會(huì)控制到圖像幀的展示,按一次空格鍵實(shí)現(xiàn)暫停,再按一次就實(shí)現(xiàn)播放了。
    is->paused = !is->paused;
}

特別說(shuō)明:paused標(biāo)志位控制著視頻是否播放,當(dāng)需要繼續(xù)播放的時(shí)候,一定要重新更新當(dāng)前所需要播放幀的pts時(shí)間,因?yàn)檫@里面要加上已經(jīng)暫停的時(shí)間。

5.5 逐幀播放是如何做的?

在視頻解碼線程中,不斷通過(guò)stream_toggle_paused,控制對(duì)視頻的暫停和顯示,從而實(shí)現(xiàn)逐幀播放:

static void step_to_next_frame(VideoState *is)
{
   //逐幀播放時(shí),一定要先繼續(xù)播放,然后再設(shè)置step變量,控制逐幀播放
   if (is->paused)
      stream_toggle_pause(is);//會(huì)不斷將paused進(jìn)行取反
   is->step = 1;
}

其原理就是不斷的播放,然后暫停,從而實(shí)現(xiàn)逐幀播放:

static int video_thread(void *arg)
{
  if (is->step)
    stream_toggle_pause(is);
      ……………………
  if (is->paused)
    goto display;//顯示視頻
  }
}

5.6 快進(jìn)和后退

關(guān)于快進(jìn)/后退,首先拋出兩個(gè)問(wèn)題:

1. 快進(jìn)以時(shí)間為維度還是以幀數(shù)為維度來(lái)對(duì)播放進(jìn)度進(jìn)行控制呢? 
2.一旦進(jìn)度發(fā)生了變化,那么當(dāng)前幀,以及AVFrame隊(duì)列是否需要清零,整個(gè)對(duì)stream的流是否需要重新來(lái)進(jìn)行控制呢? 
ffplay中采用以時(shí)間為維度的控制方法。對(duì)于快進(jìn)和后退的控制,都是通過(guò)設(shè)置VideoState的seek_req、seek_pos等變量進(jìn)行控制

do_seek:
//實(shí)際上是計(jì)算is->audio_current_pts_drift + av_gettime() / 1000000.0,確定當(dāng)前需要播放幀的時(shí)間值
pos = get_master_clock(cur_stream);
pos += incr; //incr為每次快進(jìn)的步進(jìn)值,相加即可得到快進(jìn)后的時(shí)間點(diǎn)
stream_seek(cur_stream, (int64_t)(pos AV_TIME_BASE), (int64_t)(incr AV_TIME_BASE), 0);
關(guān)于stream_seek的代碼如下,其實(shí)就是設(shè)置VideoState的相關(guān)變量,以控制read_tread中的快進(jìn)或后退的流程:

/* seek in the stream */
static void stream_seek(VideoState *is, int64_t pos, int64_t rel, int seek_by_bytes)
{

  if (!is->seek_req) {
  is->seek_pos = pos;
  is->seek_rel = rel;
  is->seek_flags &= ~AVSEEK_FLAG_BYTE;
  if (seek_by_bytes)
    is->seek_flags |= AVSEEK_FLAG_BYTE;
  is->seek_req = 1;
}
}

stream_seek中設(shè)置了seek_req標(biāo)志,就直接進(jìn)入前進(jìn)/后退控制流程了,其原理是調(diào)用avformat_seek_file函數(shù),根據(jù)時(shí)間戳控制索引點(diǎn),從而控制需要顯示的下一幀:

static int read_thread(void *arg){
//當(dāng)調(diào)整播放進(jìn)度以后
if (is->seek_req) {
   int64_t seek_target = is->seek_pos;
   int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;
   int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;
  //根據(jù)時(shí)間抽查找索引點(diǎn)位置,定位到索引點(diǎn)之后,下一幀的讀取直接從這里開(kāi)始,就實(shí)現(xiàn)了快進(jìn)/后退操作
  ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
  if (ret < 0) {
     fprintf(stderr, "s: error while seeking\n", is->ic->filename);
  } else {
  //查找成功之后,就需要清空當(dāng)前的PAcket隊(duì)列,包括音頻、視頻和字幕
     if (is->audio_stream >= 0) {
        packet_queue_flush(&is->audioq);
        packet_queue_put(&is->audioq, &flush_pkt);
     }
     if (is->subtitle_stream >= 0) {//處理字幕stream
        packet_queue_flush(&is->subtitleq);
        packet_queue_put(&is->subtitleq, &flush_pkt);
    }
    if (is->video_stream >= 0) {
       packet_queue_flush(&is->videoq);
       packet_queue_put(&is->videoq, &flush_pkt);
    }
  }
  is->seek_req = 0;
  eof = 0;
  }
}

另外從上面代碼中發(fā)現(xiàn),每次快進(jìn)后退之后都會(huì)對(duì)audioq、videoq和subtitleq進(jìn)行flush清零,也是相當(dāng)于重新開(kāi)始,保證緩沖隊(duì)列中的數(shù)據(jù)的正確性。

對(duì)于音頻,開(kāi)始仍然有些困惑,因?yàn)樵跁和5臅r(shí)候,沒(méi)有看到對(duì)音頻的控制,是如何控制的呢?

后來(lái)發(fā)現(xiàn),其實(shí)暫停的時(shí)候設(shè)置了is->paused變量,解復(fù)用和音頻解碼和播放都依賴(lài)于is->paused變量,所以音頻和視頻播放都隨之停止了。

六、 這次分析ffplay代碼的反省總結(jié):

1.基礎(chǔ)概念和原理積累,最開(kāi)始接觸FFmpeg,因?yàn)槠渖婕暗母拍詈芏啵雌饋?lái)有種無(wú)從下手的感覺(jué)。這時(shí)候必須從基本模塊入手,逐步理解更多,一定的量積累,就會(huì)產(chǎn)生一些質(zhì)變,更好的理解視頻編解碼機(jī)制;

2.一定要首先看懂代碼總體架構(gòu)和流程,隨后針對(duì)每個(gè)細(xì)節(jié)點(diǎn)進(jìn)行深入分析,會(huì)極大提高看代碼效率。會(huì)畫(huà)一些框圖是非常重要的,比如下面這張,所以簡(jiǎn)要的流程圖要比注重細(xì)節(jié)的uml圖要方便得多;

怎樣進(jìn)行ffplay播放器的源代碼分析
3.看FFmpeg代碼,在PC端上調(diào)試,會(huì)快捷很多。假如要在Android上,調(diào)用jni來(lái)看代碼,效率就會(huì)很低。

看完上述內(nèi)容,你們對(duì)怎樣進(jìn)行ffplay播放器的源代碼分析有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI