天天看點

FFplay暫停分析

暫停也是播放器非常常見的功能。對于 FFplay 播放器,可以通過 

p

 鍵 或者空格鍵 來切換暫停狀态。

先來看一下處理 

p

 鍵 的代碼,如下:

FFplay暫停分析

從上圖可以看到,調了 

toggle_pause()

 函數,注意這個 

cur_stream

 參數,其實這個參數不是視訊流或者音頻流,而是 FFplay 的全局管理器 

VideoState

toggle_pause()

 函數的實作如下:

static void toggle_pause(VideoState *is)
{
    stream_toggle_pause(is);
    is->step = 0;
}
           

後面為什麼會把 

is->step

 置為 0 ?

is->step

 變量是給逐幀播放用的,為 0 代表退出逐幀播放模式。

這是因為逐幀播放實際上就是用切換暫停狀态來實作的,每播放一幀就立即暫停。具體請看《FFplay逐幀播放分析》

再來看一下 

stream_toggle_pause()

 函數的實作,如下:

FFplay暫停分析

從 啟動狀态 切換到 暫停狀态的時候,

is->paused

 等于 0,是以是不會跑進去 

if (is->paused) {...}

 裡面的。

上圖中用 

set_clock()

 更新了外部時鐘,

set_clock()

 函數會把 

Clock::pts

 更新到目前最新的播放時刻。

通常情況下,

get_clock()

 函數擷取目前的最新播放時刻,是用 

Clock::pts

 + 消逝的時間。

但是消逝的時間可以是 0 的,什麼情況下消逝的時間是 0 呢?就是暫停的時候,當播放器暫停的時候,他外部時鐘也會暫停,是以消逝的時間為 0 ,如下:

FFplay暫停分析

stream_toggle_pause()

 函數才需要更新外部時鐘的 pts 成最新的值,如果不更新,在暫停狀态下擷取到的外部時鐘播放時刻就不準。

在暫停狀态下,哪段代碼還會調 

get_clock()

 擷取外部時鐘的播放時刻呢?這個後面揭曉。

stream_toggle_pause()

 函數最後還會把 4 個暫停變量都被設定成 1,或者 從 1 切換成 0 。如下:

is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = !is->paused;
           

我們來看一下這 4 個暫停變量會影響哪些代碼邏輯?

首先是 main 主線程,主線程主要是處理鍵盤事件 跟 播放視訊畫面,鍵盤事件不會受到暫停狀态影響,該處理還是繼續處理。

播放視訊畫面 的函數是 

video_refresh()

 ,但是暫停狀态下也不會調 

video_refresh()

 ,如下:

FFplay暫停分析

但是暫停狀态下,如果改變了 

ffplay

 視窗大小,

is->forece_refresh

 機會變成 1,就會調 

video_refresh()

 。

是以 main 主線程,在暫停狀态下,隻會不斷檢測,處理鍵盤事件以及一些視窗事件,大部分時候并不會調 

video_refresh()

 播放視訊畫面。

再來看一下 read_thread解複用線程,在暫停狀态下,它在幹什麼?

FFplay暫停分析

上圖的是為了相容一些網絡流的播放,有些流媒體協定支援暫停跟播放操作,當暫停的時候,伺服器端就不會再推流過來了。對于本地播放,上面的代碼是沒用的。

我翻了一圈 

read_thread()

 函數的代碼,發現它并不會因為 pause 變成 1 而停下,

read_thread()

 線程即使在暫停狀态下,也是不斷運作,不斷讀取資料,直到 塞滿隊列,塞滿隊列就會休眠 10 ms,如下:

FFplay暫停分析

感興趣的讀者可以在暫停狀态下往 

SDL_CondWaitTimeout()

 那裡打個斷點,會不斷跑進去那裡的邏輯。

再來看一下 video_thread視訊解碼線程,在暫停狀态下,它在幹什麼?

研究 

video_thread()

 函數的代碼,我們發現了第一個在暫停狀态下,擷取外部時鐘的地方,也就是 

video_thread()

 裡面的 

get_video_frame()

 函數

FFplay暫停分析

當外部時鐘是 主時鐘的時候,這裡的 

get_master_clock()

 擷取的就是外部時鐘的播放時刻。是以如果在 

stream_toggle_pause()

 裡面不更新外部時鐘,這裡擷取到的時間就是錯誤的,會導緻誤判,丢幀。

翻了一圈 

video_thread()

 解碼線程的代碼,發現也不會受到 paused 的影響,還是會正常解碼,但是如果塞滿 

FrameQueue

 隊列的時候,就會一直阻塞在 

frame_queue_peek_writable()

 函數裡面。

再來看一下 audio_thread音頻解碼線程,在暫停狀态下,它在幹什麼?

研究發現,跟 

video_thread()

 視訊解碼類似,也不會受到 paused 的影響,還是會正常解碼,但是如果塞滿 

FrameQueue

 隊列的時候,就會一直阻塞在 

frame_queue_peek_writable()

 函數裡面。

最後來看一下 sdl_audio_callback音頻播放線程分析,在暫停狀态下,它在幹什麼?

首先,

sdl_audio_callback()

 是回調函數,是以你可以猜到,肯定不會阻塞,肯定會傳回去給 SDL 。

首先,

sdl_audio_callback()

 是會受到 paused 的影響的,如下:

FFplay暫停分析
FFplay暫停分析

暫停狀态下,

audio_decode_frame()

 函數會直接傳回 -1,就會導緻 輸出靜音資料,隻要直接把 stream 指向的記憶體資料設定為 0 就是輸出靜音資料了。

memset(stream, 0, len1);
           

雖然是輸出靜音資料,但是音頻播放線程還是在跑的,沒有阻塞,她在跑,就會更新音頻時鐘,如下:

FFplay暫停分析

雖然會更新音頻時鐘,但是因為在暫停狀态下沒有跑進去 audio_decode_frame(),是以 

is->audio_clock

 沒有更新。是以即便更新音頻時鐘,也是用原來的值來更新的。

是以音頻時鐘相當于沒有更新。

讀者可以在 

sdl_audio_callback()

 入口加個日志,如下:

FFplay暫停分析

會發現雖然音頻時鐘一直在跑,但是這個 

get_clock()

 傳回的值是沒有增長的。

FFplay暫停分析

總結一下,從啟動狀态切換到暫停狀态,影響的地方。

1,導緻視訊播放函數沒有調用,導緻 

FrameQueue

 堆積,是以視訊解碼線程會阻塞在 

frame_queue_peek_writable()

 函數裡面

2,導緻音頻播放線程直接輸出靜音資料,沒有從 

FrameQueue

 讀資料,導緻 

FrameQueue

 堆積,是以音頻解碼線程會阻塞在 

frame_queue_peek_writable()

 函數裡面。

3,音頻解碼線程,視訊解碼線程阻塞,一直不從 PacketQueue 拿資料去解碼,導緻 

PacketQueue

 堆積,進而導緻 

read_thread

解複用線程不再從檔案讀取資料了。

回到 

stream_toggle_pause()

 函數,當從暫停狀态切換到啟動狀态的時候,有一段邏輯非常奇怪,如下:

FFplay暫停分析

可以看到,它會會更新 

is->frame_timer

 跟 視訊時鐘,為什麼要這麼做呢?

因為 

is->frame_timer

 的機關是系統時間,代表視窗現在這一幀畫面是在什麼系統時間開始播放的。更新 

is->frame_timer

 之後代表視窗目前畫面是從此刻開始播放的。

暫停之後,系統時間是一直在跑的。例如,暫停 8 秒鐘之後,再調 

av_gettime_relative()

 會發現比之前多了 8 秒。

是以切換回來的時候,要及時更新 

is->frame_timer

 ,這個變量的含義不能變的。如果你不更新,就代表視訊播放線程卡頓,沒有排程過來,導緻的這幀畫面播放了 8 秒,雖然他确實是播放了 8秒,但是不能展現在 

is->frame_timer

 變量上面。

後面在 

video_refresh()

 裡面會用 

is->frame_timer

 來判斷是否已經超過預定的播放時刻,超過了就會丢幀,如下:

FFplay暫停分析

上圖中,隻有超過 10 秒才會糾正 

is->frame_timer

 ,如果你隻暫停了 8 秒就不會糾正。

是以,如果在 暫停狀态切換到啟動狀态的時候,不更新 

is->frame_timer

 ,就會導緻 8 秒的視訊幀被丢棄,因為會誤判它們都已經過了預定的播放時刻。

再來看一下,從暫停狀态切換到啟動狀态的時候,為啥需要更新視訊時鐘,如下:

FFplay暫停分析

注意,他是先把視訊時鐘的暫停狀态恢複,然後再更新時鐘的,是以 

get_clock()

 擷取的是 pts + 消逝的時間,消逝的時間其實就等于暫停了多久。

其實我也不太清楚 這裡更新 視訊時鐘的 pts 的具體作用,因為暫停狀态已經恢複了,随意即使這裡不執行 

set_clock()

get_clock()

 擷取到的也是正确的播放時刻。

個人猜測是字幕的場景用的,因為那裡用了 clock 裡面的 pts 字段。

FFplay暫停分析

至此,

ffplay

 播放器暫停功能分析完畢。

推薦一個零聲學院免費公開課程,個人覺得老師講得不錯,分享給大家:

Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等技術内容,立即學習

繼續閱讀