暫停也是播放器非常常見的功能。對于 FFplay 播放器,可以通過
p
鍵 或者空格鍵 來切換暫停狀态。
先來看一下處理
p
鍵 的代碼,如下:
從上圖可以看到,調了
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()
函數的實作,如下:
從 啟動狀态 切換到 暫停狀态的時候,
is->paused
等于 0,是以是不會跑進去
if (is->paused) {...}
裡面的。
上圖中用
set_clock()
更新了外部時鐘,
set_clock()
函數會把
Clock::pts
更新到目前最新的播放時刻。
通常情況下,
get_clock()
函數擷取目前的最新播放時刻,是用
Clock::pts
+ 消逝的時間。
但是消逝的時間可以是 0 的,什麼情況下消逝的時間是 0 呢?就是暫停的時候,當播放器暫停的時候,他外部時鐘也會暫停,是以消逝的時間為 0 ,如下:
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
視窗大小,
is->forece_refresh
機會變成 1,就會調
video_refresh()
。
是以 main 主線程,在暫停狀态下,隻會不斷檢測,處理鍵盤事件以及一些視窗事件,大部分時候并不會調
video_refresh()
播放視訊畫面。
再來看一下 read_thread解複用線程,在暫停狀态下,它在幹什麼?
上圖的是為了相容一些網絡流的播放,有些流媒體協定支援暫停跟播放操作,當暫停的時候,伺服器端就不會再推流過來了。對于本地播放,上面的代碼是沒用的。
我翻了一圈
read_thread()
函數的代碼,發現它并不會因為 pause 變成 1 而停下,
read_thread()
線程即使在暫停狀态下,也是不斷運作,不斷讀取資料,直到 塞滿隊列,塞滿隊列就會休眠 10 ms,如下:
感興趣的讀者可以在暫停狀态下往
SDL_CondWaitTimeout()
那裡打個斷點,會不斷跑進去那裡的邏輯。
再來看一下 video_thread視訊解碼線程,在暫停狀态下,它在幹什麼?
研究
video_thread()
函數的代碼,我們發現了第一個在暫停狀态下,擷取外部時鐘的地方,也就是
video_thread()
裡面的
get_video_frame()
函數
當外部時鐘是 主時鐘的時候,這裡的
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 的影響的,如下:
暫停狀态下,
audio_decode_frame()
函數會直接傳回 -1,就會導緻 輸出靜音資料,隻要直接把 stream 指向的記憶體資料設定為 0 就是輸出靜音資料了。
memset(stream, 0, len1);
雖然是輸出靜音資料,但是音頻播放線程還是在跑的,沒有阻塞,她在跑,就會更新音頻時鐘,如下:
雖然會更新音頻時鐘,但是因為在暫停狀态下沒有跑進去 audio_decode_frame(),是以
is->audio_clock
沒有更新。是以即便更新音頻時鐘,也是用原來的值來更新的。
是以音頻時鐘相當于沒有更新。
讀者可以在
sdl_audio_callback()
入口加個日志,如下:
會發現雖然音頻時鐘一直在跑,但是這個
get_clock()
傳回的值是沒有增長的。
總結一下,從啟動狀态切換到暫停狀态,影響的地方。
1,導緻視訊播放函數沒有調用,導緻
FrameQueue
堆積,是以視訊解碼線程會阻塞在
frame_queue_peek_writable()
函數裡面
2,導緻音頻播放線程直接輸出靜音資料,沒有從
FrameQueue
讀資料,導緻
FrameQueue
堆積,是以音頻解碼線程會阻塞在
frame_queue_peek_writable()
函數裡面。
3,音頻解碼線程,視訊解碼線程阻塞,一直不從 PacketQueue 拿資料去解碼,導緻
PacketQueue
堆積,進而導緻
read_thread
解複用線程不再從檔案讀取資料了。
回到
stream_toggle_pause()
函數,當從暫停狀态切換到啟動狀态的時候,有一段邏輯非常奇怪,如下:
可以看到,它會會更新
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
來判斷是否已經超過預定的播放時刻,超過了就會丢幀,如下:
上圖中,隻有超過 10 秒才會糾正
is->frame_timer
,如果你隻暫停了 8 秒就不會糾正。
是以,如果在 暫停狀态切換到啟動狀态的時候,不更新
is->frame_timer
,就會導緻 8 秒的視訊幀被丢棄,因為會誤判它們都已經過了預定的播放時刻。
再來看一下,從暫停狀态切換到啟動狀态的時候,為啥需要更新視訊時鐘,如下:
注意,他是先把視訊時鐘的暫停狀态恢複,然後再更新時鐘的,是以
get_clock()
擷取的是 pts + 消逝的時間,消逝的時間其實就等于暫停了多久。
其實我也不太清楚 這裡更新 視訊時鐘的 pts 的具體作用,因為暫停狀态已經恢複了,随意即使這裡不執行
set_clock()
,
get_clock()
擷取到的也是正确的播放時刻。
個人猜測是字幕的場景用的,因為那裡用了 clock 裡面的 pts 字段。
至此,
ffplay
播放器暫停功能分析完畢。
推薦一個零聲學院免費公開課程,個人覺得老師講得不錯,分享給大家:
Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等技術内容,立即學習