[ffmpeg] h264並行解碼

ffmpeg中的並行解碼分爲兩種:html

 

Frame-level Parallelism

幀間依賴

咱們以前討論過Frame-level Parallelism。在以前的文章中,咱們說過在進行幀級的並行處理時,因爲I、P幀是做爲參考幀(B幀也能做爲參考幀),所以不能對其進行並行處理,只有非參考B幀纔是最適宜進行並行處理的幀。不過其實若是咱們能正確地處理好各個幀之間的依賴關係,不管是I、P仍是B幀都能進行並行處理。FFmpeg爲了達到這一目的,對咱們以前所討論的Frame-level Parallelism進行了改進:添加幀間依賴。多線程

h264的幀間依賴主要有兩個:函數

  • 進行運動補償時須要參考幀提供參考區域的像素數據。在解碼一個宏塊的過程當中,獲得宏塊中每一個分塊的運動向量與參考幀後,就能肯定宏塊的各個分塊的參考區域,而後就能夠從這些區域提取像素數據進行運動補償。
  • 編碼模式爲B Direct的宏塊在計算運動向量時,須要位於第一個後向參考幀的co-located塊的運動向量,用來計算出當前幀的前向以及後向運動向量。

image

FFmpeg對於上述依賴的解決方案是:編碼

  • 對於每一個分塊,在進行運動補償以前,等待該分塊所依賴的參考區域的最後一行(以像素爲單位,表示這一行的值row是相對於整幅圖像來講的,下同)的就緒消息[1]
  • 對於編碼模式爲B Direct的宏塊,在計算運動向量以前,等待co-located塊的第一行的就緒消息(因爲咱們只須要該co-located塊的運動向量,既然第一行像素已就緒,那麼運動向量確定是已經就緒了)[2]
  • 解碼器每解碼完成一行宏塊,就通知發送該宏塊的最後一行的就緒消息(若是當前視頻指定了deblocking,因爲當前行宏塊的deblocking須要用到下一行宏塊的上方4行像素,所以就緒的row值須要進行相應的調整)[3]

 

Packet與Frame

在討論FFmpeg的實現以前,咱們須要先了解packet(AVPacket)與frame(AVFrame)之間的關係。不一樣的編碼格式也許會有所不一樣,不過h264在FFmpeg中的一個packet中所包含的數據就是一個frame(一幀)。通常狀況下一幀就是一個slice,這樣的話一個packet中只有一個slice;固然,一幀也有可能會分爲多個slice,若是是這種狀況的話,一個packet中會包含這一幀全部的slice。線程

咱們之因此在這裏討論這二者之間的關係,是由於FFmpeg每次都是以一個packet爲單位向解碼器傳入須要解碼的數據,也就是說每次會向h264解碼器傳入一幀的數據。3d

 

實現方案[4]

FFmpeg實現方案以下:code

image

Thread List,線程列表,線程列表中的每一項都映射一個解碼線程。主線程會從線程列表中按照序號由小到大(循環)提取解碼線程,並把解碼任務提交到該解碼線程。同時主線程在提交完解碼任務後也會從線程列表中按照序號由小到大(循環)提取解碼線程,並嘗試從該解碼線程獲取解碼完成的幀。orm

M,主線程,主要目的有兩個:視頻

  • 向解碼線程提交解碼任務。FFmpeg中是以packet爲單位進行解碼任務的提交的,按照前一小節的描述,FFmpeg就是以frame爲單位進行解碼任務的提交的。
  • 從解碼線程獲取解碼所得的幀並進行返回。不過在第一輪進行任務提交的時候是不會去獲取幀,在第一輪任務提交完成後,此時全部解碼線程都已經開始進行了解碼做業,那麼主線程就能夠開始等待第一個線程解碼完成,而後嘗試去得到解碼完成的幀(這裏的「嘗試」,是由於就像單線程解碼時那樣,並不必定是每次調用解碼API都會返回一幀的。因爲h264編碼的視頻中經常包含B幀,這會使得碼流的解碼順序並不是幀的播放順序,可是解碼API必須按照幀的播放順序進行返回,所以在進行幀的返回時會進行相應的調整)。接下來每次向一個線程提交一個解碼任務後,都須要等待下一個線程空閒並嘗試返回幀。

image

T,解碼線程,接收解碼任務並進行解碼。解碼線程是以frame爲單位進行處理的。解碼線程解碼主線程所提交的packet,執行與單線程時同樣的解碼做業,固然在解碼做業期間會碰到咱們上面所述的幀間依賴並進行處理。htm

 

隱式的幀間依賴

幀間依賴除了上面所述的明顯存在的幀間依賴以外,還有一處較爲隱蔽的幀間依賴。

解碼所需的參考圖像列表依賴於POC,而在計算圖像POC時,須要對相鄰兩個frame(或者說slice)頭部的pic_order_cnt_lsb或者frame_num進行比較。這就代表在開始一個frame的解碼以前,須要把上一個frame的這些參數傳入當前frame。有了上一個frame頭部的這些參數,當前的frame就能按照單線程解碼那樣準確地計算出當前frame的POC。

FFmpeg把這參數傳入操做實如今了ff_h264_update_thread_context當中,該函數會在提交解碼任務前被調用[5]

 

 

Slice-level Parallelism

如咱們以前討論過的Slice-level Parallelism,ffmpeg的slice級並行只能在幀內並行。所以,若是在某個視頻在編碼時,一幀圖像分爲多個slice進行編碼的話,那麼在使用ffmpeg解碼時調用slice級並行解碼就會獲得不錯的效果。而在實際應用中,大多數h264編碼的視頻都是一幀只有一個slice,對於這種視頻,就算採用了slice級並行,也只有一個線程在進行解碼做業。

 

實現方案

若是一幀,即一個packet分爲幾個slice時,會先把這一幀前面的slice加入隊列,到最後一個slice時統一對這一幀的全部slice進行並行解碼[6]。其中涉及到的關鍵要素以下:

image

Slice Context List,slice的上下文是slice context(FFmpeg中的變量爲slice_ctx),若是一幀中有多個slice,那麼會把slice上下文組成一個列表。前面所說的入隊列操做會對該列表進行填充以供後續解碼使用。

M,主線程,如單線程同樣的流程,從用戶調用解碼API一直執行到咱們前面所說的入隊列,到最後一個slice時會調用一個入口函數啓動多線程解碼操做。在調用入口函數後,主線程參與的多線程解碼過程一共包含三個步驟[7]

  1. 經過發送啓動消息激活其它正在等待的解碼線程。
  2. 在啓動多線程解碼後,主線程也會一同做爲其中一個線程進行slice的解碼。
  3. 最後等待全部線程完成任務後返回。

T,解碼線程,接收到主線程所發起的啓動消息後,解碼線程會到Slice Context List去提取其中一個slice context(原子操做),而後進行slice解碼[8]

 

※在進行slice並行解碼時deblocking是沒法超越slice邊界的,若是視頻指定了超越邊界的deblocking,那麼deblocking須要要留到全部slice解碼完成後再作。與此同時,若是指定ffmpeg進行快速解碼,也會在解碼線程內進行deblocking,不過此時的deblocking就是對本來進行超越邊界的deblocking進行了非超越邊界的deblocking,會影響視頻圖像質量[9]

 

Example

ffmpeg只要在打開解碼器以前進行以下設置就能夠執行並行解碼。

    avcodec_alloc_context3(NULL);
    avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[Stream]->codecpar);
    Codec = avcodec_find_decoder(pCodecCtx->codec_id);

    pCodecCtx->thread_count = 4;
    pCodecCtx->thread_type = FF_THREAD_FRAME;
    //pCodecCtx->thread_type = FF_THREAD_SLICE;

    avcodec_open2(pCodecCtx, pCodec, NULL);

兩行分別爲:

設置並行解碼數目,即解碼線程數。

設置並行解碼類型爲FF_THREAD_FRAME或者FF_THREAD_SLICE,分別對應Frame-level Parallelism以及Slice-level Parallelism。

 

Reference:

1. await_references

2. await_reference_mb_row

3. decode_finish_row

4. ff_thread_decode_frame

5. submit_packet

6. decode_nal_units

7. avpriv_slicethread_execute

8. thread_worker, run_jobs, worker

9. h264_slice_init

相關文章
相關標籤/搜索