1. 程式人生 > >[SimplePlayer] 7. 多執行緒處理

[SimplePlayer] 7. 多執行緒處理

在前面的文章中,我們分別實現了視訊影象解碼、播放,音訊解碼、播放,現在則需要把這些功能組合起來。總體上來說,整個程式的功能可以分為兩條線路:視訊以及音訊,兩條線之間除了後續的同步操作之外基本沒有任何關聯。而線上路當中,各個模組之間並沒有太緊密的耦合,只要上游模組提供了原料,下游模組就可以執行處理。因此,我們可以為各個模組建立獨立的執行緒,這會使得程式結構更加清晰,並且在多核的平臺下能更好地發揮平臺效能。

所需的各執行緒及其功能:

  • 主執行緒,除了進行各個模組的初始化之外,所要承擔的任務是整個程式的事件處理,如關閉程式。
  • 視訊執行緒,進行視訊影象解碼,把video packet解碼成frame。
  • 音訊執行緒,進行音訊解碼,把audio packet解碼成frame。
  • 讀取執行緒,讀取視訊檔案,demux後分別向視訊執行緒與音訊執行緒提供packet。
  • 視訊顯示執行緒,進行視訊影象顯示,這部分並非繁重的任務,因此可以被合併到主執行緒當中。
  • 音訊播放執行緒,進行音訊播放,通過SDL的callback實現(SDL會自動為音訊輸出建立一個執行緒)。

 

上述各個執行緒的處理效率各不相同。例如,讀取執行緒僅需要從磁碟讀取視訊檔案,然後進行復雜度較簡單的demux,也就是說很短時間只能就能輸出一幀的packet;而視訊解碼執行緒則由於其中流程繁雜,需要大量運算,因此通常需要相對較長的時間才能解碼出一幀影象。對於這種上下游模組資料處理的效率差異,如果不採取一些應對措施,則會導致執行緒的頻繁切換(每demux、decode、play一幀都需要進行一次執行緒切換,而執行緒的上下文切換也會消耗cpu資源),從而降低程式的處理效率。

在上下游執行緒之間新增一個緩衝就可以很好地改善這一問題。為上下游執行緒之間新增緩衝後,只要緩衝區還有空間,那麼上游的執行緒就可以繼續執行下一幀的處理,並把處理結果輸出到該緩衝區內。

本文所用到的緩衝區如下:

  • video packet list,儲存read thread所輸出的video packet。
  • audio packet list,儲存read thread所輸出的audio packet。
  • frame queue,儲存video thread解碼後所輸出的視訊幀。
  • audio ring buffer,儲存audio thread解碼後所輸出的音訊資料。

 

image

 

 

Packet List

Packet list作為demuxer與decoder之間的緩衝區,目的是實現一個packet佇列,該佇列中的packet先進先出。FFmpeg提供了一個AVPacketList結構體,我們可以用這個結構體來進行佇列的構建。

typedef struct AVPacketList {
    AVPacket pkt;
    struct AVPacketList *next;
} AVPacketList;

AVPacketList當作連結串列的節點,其中pkt用於維護packet,next用於連線相鄰的節點。由於是用連結串列來實現佇列,因此需要一個指向連結串列頭的指標first_pkt以及一個指向連結串列尾的指標last_pkt。當需要把packet入佇列時,把packet加入到連結串列末尾,而當要取出packet時,則從連結串列頭部取出。

image

 

 

Frame Queue

Video Thread解碼出來的視訊幀會被快取在Frame Queue中,顯示模組在需要進行影象顯示的時候從Frame Queue中取出影象進行顯示。由於通常frame所佔用的空間都比較大,因此快取的frame的數量會有所限制,那麼我們就可以用一個指向frame的指標陣列來進行佇列的維護。

image

為了實現佇列的效果,需要分別有兩個數字指示佇列的頭與尾,其中read_index標記的是佇列頭部,write_index指示的是佇列尾部。當要從佇列中取出frame時,去獲取read_index的陣列元素所指示的frame,然後read_index++;當要把frame加入到佇列中時,令write_index的陣列元素指向需要加入佇列的frame,然後write_index++。

 

 

Ring Buffer

前面的章節中我們手動把fltp格式的音訊轉換為s16的音訊,s16的音訊格式是把左右聲道的音訊樣本交叉排列的序列資料,ring buffer是一種比較時候用於儲存序列資料的資料結構。

Ring buffer,環形緩衝區,原型為一塊連續的緩衝區,通過運用指向資料頭部(rIndex)以及資料尾部(wIndex)的指標來維護資料的存取,當資料尾部的指標到達緩衝區末尾,就會把尾部指標指向緩衝區開頭,同理資料頭部的指標也會進行迴圈移動,如此實現環形緩衝區。

image

當需要儲存資料時,首先需要保證有足夠的空間來進行儲存,然後從wIndex處開始寫入資料,並根據寫入資料的長度更新wIndex;當要讀取資料時,從rIndex處讀取資料,並根據讀取的資料長度更新rIndex。

 

 

執行緒安全

對於上面描述的3種佇列,為了執行緒安全(使得對佇列的操作能在多執行緒上安全使用),我們需要保證出/入佇列的操作為原子操作。實現則可以採用SDL提供的mutex。

 

 

中途退出

視訊播放可以進行中途退出的操作,那麼我們也有必要提供能在中途終止佇列的功能。我們這裡所說的終止佇列,就是使得再次調用出/入佇列的函式時,會返回-1,以表示佇列已被終止。我們可以通過設定一個變數abort_request來進行判斷,當abort_request為1時佇列終止,為0則佇列正常執行。

佇列的abort函式需要實現:

  1. 把abort_request設定為1。
  2. 由於出/入佇列函式可能此時會處於等待狀態(如:此時已經滿佇列,入佇列函式在等待佇列騰出空間),因此abort函式還需要解除出/入佇列函式的等待狀態。

那麼在實現出/入佇列的函式時

  1. 在函式的開頭加入對變數abort_request的判斷來決定是否返回-1。
  2. 由於出/入佇列函式可能此時會處於等待狀態(如:此時已經滿佇列,入佇列函式在等待佇列騰出空間),那麼在abort函式解除當前函式的等待狀態後,應該再次進行abort_request變數的判斷,如果abort_request為1則應該直接返回-1,而不應該繼續執行後續操作。