[ffmpeg] h264並行解碼


ffmpeg中的並行解碼分為兩種:

 

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解碼器傳入一幀的數據。

 

實現方案[4]

FFmpeg實現方案如下:

image

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

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

  • 向解碼線程提交解碼任務。FFmpeg中是以packet為單位進行解碼任務的提交的,按照前一小節的描述,FFmpeg就是以frame為單位進行解碼任務的提交的。
  • 從解碼線程獲取解碼所得的幀並進行返回。不過在第一輪進行任務提交的時候是不會去獲取幀,在第一輪任務提交完成后,此時所有解碼線程都已經開始進行了解碼作業,那么主線程就可以開始等待第一個線程解碼完成,然后嘗試去獲得解碼完成的幀(這里的“嘗試”,是因為就像單線程解碼時那樣,並不一定是每次調用解碼API都會返回一幀的。由於h264編碼的視頻中常常包含B幀,這會使得碼流的解碼順序並非幀的播放順序,但是解碼API必須按照幀的播放順序進行返回,因此在進行幀的返回時會進行相應的調整)。接下來每次向一個線程提交一個解碼任務后,都需要等待下一個線程空閑並嘗試返回幀。

image

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

 

隱式的幀間依賴

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

解碼所需的參考圖像列表依賴於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只要在打開解碼器之前進行如下設置就可以執行並行解碼。

1
2
3
4
5
6
7
8
9
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_workerrun_jobsworker

9. h264_slice_init


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM