播放器在渲染視頻時就是根據PTS來確定渲染和展示時間點的。 根據這個原理,我們就可以通過調整幀的PTS時間來實現視頻加速/降速播放。
加速/降速的原理
我們都知道,當幀速率(frame rate)大於24時,也就是1秒播放24幀時,我們的視覺就會看到流程的視頻。 在幀總量不變的情況下,如果將1/24變為1/48,那么在相同時間內多播放了一倍的幀,對於我們的視覺來說,就感覺播放速度加快了(因為本該20秒才能播放完的幀,在10秒內就播放完了,就相當加速了一倍)。同理,如果將1/24調整為1/12,就會看到慢動作。
FFmpeg提供了setpts濾鏡可以實現調整pts的效果。 典型的用法如下:
ffmpeg -i ~/tmp/trailer.mp4 -filter:v "setpts=0.5*PTS" output.mp4
0.5*PTS表示將幀的PTS值乘以0.5后作為新的PTS值。 比如說: 幀A當前的PTS是4000(根據以前的知識,根據PTS和Time_base可以計算出渲染的時間點)。 假設對應的時間點是: 00:00:05, 現在將PTS調整為0.5*PTS就變成了2000,那么對應的渲染時間點就變成了: 00:00:02.5。這樣就實現了加速播放。
同理,如果是2*PTS就是降速播放。
局部調整
setpts只能實現全部加速或者全部減速。 因為在其內部實現中,對每個幀都應用相同的計算規則,所以要么都調整要么都不調整。如果要實現局部調整,按照通用的解決方案,只能先切割視頻,然后對單獨視頻進行加速/降速處理,然后再將視頻連接起來。
但如果我們適當調整PTS值,也可以實現部分調整的效果。
問題分析
假設存在一段30s的視頻,幀分布如下:
+------------------------------------------------------------------+
| F1 F2 F3 F4 F5 F6 F7 |
| |--------------|--------------|--------------|---> |
|Time 0 10 20 30 |
|PTS 0 100 200 250 300 350 400 |
+------------------------------------------------------------------+
F1 - F7表示7個I幀(30秒包含的幀比這個多多了,這里是為了方便描述問題)。 假設我們需要加速前15秒(后15秒播放速率不變)的視頻,那么需要調整F1到F4(F4是第15秒時渲染的幀)如下:
+------------------------------------------------------------------+
| F1 F2 F3 F4 F5 F6 F7 |
| |--------------|--------------|--------------|---> |
|Time 0 10 20 30 |
|PTS 0 100 200 250 300 350 400 |
+------------------------------------------------------------------+
這樣調整看似沒問題,但仔細分析會發現在10s-20s之間會出現天窗,這是因為這段時間內的PTS沒有任何幀需要渲染,直到第20秒的時候,才會開始繼續渲染F5幀。顯然這樣不滿足實際應用需求。
而發生問題的關鍵在於將F2-F4調整PTS之后,也需要實時調整F5-F7的PTS。 也就是正確的幀分布應該是下面的樣子:
+------------------------------------------------------------------+
| F1 F2 F3 F4 F5 F6 F7 |
| |--------------|--------------|--------------|---> |
|Time 0 10 20 30 |
|PTS 0 100 200 250 300 350 400 |
+------------------------------------------------------------------+
F1-F4以一個速率播放,而F5-F7以另外一個速率播放。這樣就實現了部分加速的效果。
代碼實現
為了簡化編碼難度,我們以setpts的代碼為基礎進行修改。 在setpts代碼中修改pts的代碼是下面部分:
d=av_expr_eval(setpts->expr, setpts->var_values, NULL);
frame->pts=D2TS(d);
d是根據規則(0.5*PTS)計算出來的pts值. 然后將新的PTS賦值給當前幀,而后繼續后面的編碼處理。
所以在這里,我們做一些判斷,為了簡化其它無關步驟,先假設只修改前5秒的視頻,所以需要先判斷當前幀是否需要修改:
(frame->pts * av_q2d(inlink->time_base)) < 5.0
通過pts*time_base可以計算出當前時間點,通過這個判斷,可以得出是否需要修改此幀的PTS值。 如果需要修改,那么仍然通過frame->pts=D2TS(d)來調整。 而處理不需要修改的幀才是重點,
按照上圖所示意的PTS,F5應該繼承F4調整前的PTS值,所以需要在調整F4之前需要保存舊的PTS。所以是下面的偽代碼:
保存Old PTS
if (當前時間 < 0.5) {
計算新的PTS並賦值給當前幀
}else{
當前幀使用上個幀的PTS
}
將偽代碼實現后如下:
oldPts=frame->pts;
if ((frame->pts * av_q2d(inlink->time_base)) < 5.0) {
frame->pts=D2TS(d);
setpts->_pts=frame->pts;
} else {
frame->pts=setpts->_pts;
}
setpts->_pts++;