本次嘗試在視頻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的幀。
從上面兩個圖來看,問題好像不是很難解決。 只要達到截斷
的條件,就去處理另外一個視頻,等待視頻處理完畢之后。再返回來處理被截斷
的視頻。
但在實現的道路上有如下三個問題需要解決:
- 如何判斷到達插入時間點
- 如何判斷視頻處理完畢
- 如何從斷點處重新讀取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
里面的代碼。