1. 程式人生 > >新手學習FFmpeg - 呼叫API完成兩個視訊的任意合併

新手學習FFmpeg - 呼叫API完成兩個視訊的任意合併

本次嘗試在視訊A中的任意位置插入視訊B.

在上一篇中,我們通過調整PTS可以實現視訊的加減速。這只是對同一個視訊的調轉,本次我們嘗試對多個視訊進行合併處理。

Concat如何執行

ffmpeg提供了一個concat濾鏡來合併多個視訊,例如:要合併視訊Video A和Video B,通過呼叫

ffmpeg -i va.mp4 -i vb.mp4 -filter_complex "[0][1]concat[out]" -map '[out]' -y output.mp4

concat支援多個Input Source,上面的命令只合並了兩個視訊,通過生成concat流程圖可以看到一些細節:

echo "movie=va.mp4[0];movie=vb.mp4[1];[0][1]concat,nullsink" | graph2dot -o graph.tmp
dot -Tpng graph.tmp -o graph.png

這是concat典型用法,迴圈讀取輸入源,然後通過修改pts完成合並。

concat是順序修改,如果需要在video A中某個時間點插入video B,那麼concat就無法完成了。 順序合併是通過修改PTS實現,那麼變序合併也可以通過修改PTS來實現,下面藉助concat的邏輯來看看如何實現變序合併。

變序合併

為了方便說明問題,我們來看一下順序和變序不同點到底在哪裡。

  • 問題分析

我們仍然假設需要合併的兩個視訊分別是Video A和Video B, 需要將Video B插入在Video A中。AF表示Video A的幀, BF表示Video B的幀。

順序合併

        +---------------------------------------------------------------------------------------------------------------+
        |       AF1    AF2    AF3     AF4     AF5    AF6     AF7    BF1    BF2     BF3    BF4     BF5    BF6            |
        |       |--------------|--------------|--------------|--------------|--------------|--------------|--->         |
        |Time   0              10             20             30            40              50            60             |
        |PTS    0      100     200     250    300     350    400    500    600     650    700    750     800            |
        +---------------------------------------------------------------------------------------------------------------+

順序合併就是讀取Video B的幀,然後將pts以Video A結束時的PTS為基準進行修改。

變序合併

        +---------------------------------------------------------------------------------------------------------------+
        |       AF1    AF2    AF3     AF4     BF1    BF2     BF3    BF4     BF5    BF6    AF5    AF6     AF7            |
        |       |--------------|--------------|--------------|--------------|--------------|--------------|--->         |
        |Time   0              10             20             30            40              50            60             |
        |PTS    0      100     200     250    300     350    400    500    600     650    700    750     800            |
        +---------------------------------------------------------------------------------------------------------------+

變序合併時先讀取Video A的幀,當達到規定的PTS時,開始讀取Video B的幀,然後以A截斷時的PTS為基準重新計算PTS。當Video B所有的幀都處理完畢之後,在從截斷處開始重新處理Video A的幀。

從上面兩個圖來看,問題好像不是很難解決。 只要達到截斷的條件,就去處理另外一個視訊,等待視訊處理完畢之後。再返回來處理被截斷的視訊。

但在實現的道路上有如下三個問題需要解決:

  1. 如何判斷到達插入時間點
  2. 如何判斷視訊處理完畢
  3. 如何從斷點處重新讀取Frame

下面就需要逐個問題解決了。

  • 如何判斷到達插入時間點

因為我們是需要在視訊A中插入視訊B,所以需要首先找到插入點。 而根據時間來判斷插入點無疑是最簡單的一種形式,計算時間就可以依靠前幾篇中介紹的PTS知識了。

當從視訊源中讀取到每幀後,我們通過幀的PTS和Time-Base根據pts * av_q2d(time_base)轉換成播放時間。 這樣第一個問題就順利解決。

當找到插入點後,我們需要暫存當前的位置,等待插入結束後,需要從斷點處重新載入幀。

  • 如何判斷視訊處理完畢

執行插入本質就是讀取視訊B的資料幀,然後修改PTS值。但我們需要得知視訊B已經處理完畢,這樣才能返回到視訊A的斷點處繼續處理。 所以如何獲取到視訊處理完畢就是第二個問題。

如果拋開ffmpeg來說,處理視訊本質也是一個IO流(從視訊檔案中讀取的IO流),當判斷到IO流結束時(通過seek來判斷EOF)時就是視訊處理完畢的時候。 但ffmpeg將這一層遮蔽掉了,也就是在filter中是無法直接獲取到IO流狀態的。

ffmpeg在遮蔽的同時,也提供了一種判斷方式。filter在處理完每一幀之後,需要確認下一幀的狀態(有下一幀/無下一幀),所以如果ffmpeg在讀取到下一幀時返回了無下一幀,那就表示當前視訊處理完畢。

通過ff_inlink_acknowledge_status(AVFilterLink *link, int *rstatus, int64_t *rpts)來獲取下一幀的狀態,當返回的ret>0表示沒有下一幀,這個時候就可以通過判斷當前處理狀態來決定是否關閉輸出流。

        if 當前處理視訊B
                切換到視訊A的斷點
        else 當前處理視訊A
                關閉所有的輸入流
                關閉輸出流
  • 如何從斷點處重新讀取Frame

這是最後一個待解決的問題了,當視訊B的資料都處理完之後,就需要從視訊A的斷點處重新讀取資料幀。上面說到對視訊流的讀取,本質就是對一個檔案的IO流處理,而在IO時都會有一個指標來表示當前位置。

ff_inlink_acknowledge_status有兩個作用,一方面獲取下一幀,另一方面是確認當前幀處理結束。 換言之,當呼叫ff_inlink_acknowledge_status之後,ffmpeg會將IO流的指標向後移動到下一幀的起始位置,如果移動失敗,則表示沒有下一幀了。 如果移動成功,那麼下次ff_inlink_consume_frame讀取幀時,就從這個位置開始讀取。

因此如何從斷點處重新讀取Frame其實不是問題,只要斷點處的幀被確認處理結束了,ffmpeg會自動的移到下一幀位置。當我們將輸入源切換到視訊A時,就自動從斷點處開始讀取幀了。

  • 虛擬碼實現

通過下面的虛擬碼簡要描述上述的過程:

        通過ff_outlink_get_status判斷輸出流狀態
        if 輸出流已關閉
                退出

        for {
                通過ff_inlink_consume_frame 獲取下一幀

                通過frame->pts * av_q2d(time_base)計算時間

                if 時間達到插入點
                        修改當前狀態, 進入暫存狀態。

                通過push_frame處理每一幀
        }

        通過ff_inlink_acknowledge_status確認幀狀態

        if 當前是暫存狀態
                切換到視訊B

        if 沒有下一幀
                if 當前是視訊B && 當前是暫存狀態
                        關閉視訊B
                        切換回視訊A

                if 當前是視訊A && 當前是暫存狀態
                        關閉視訊A
                        關閉輸出流

大致就是這個處理流程, 完整程式碼可以參考iconcat裡面的程式碼