引言
我們有一個平臺來周期性地對線上的直播流數據進行某些檢測,例如黑/白屏檢測、靜態畫面檢測……在檢測中,我們會根據提取到的直播流的幀率來預估要計算的幀數量,例如,如果要檢測5s的直播流,而該直播流的幀率為20fps,需要計算的幀數量則為100。忽然有一天,我們發現,平臺開始大面積的超時,之前只需要2s就能完成的計算,現在卻需要30+分鐘。查了之后,我們發現,之所以計算超時是因為OpenCV計算的幀率為2000,從而導致需要計算的幀數量從之前的100變為了10000,進而引起了計算超時。
1、OpenCV 如何計算幀率
這個問題的具體描述可以參見 OpenCVIssues 21006。該問題的模擬直播流片段test.ts可以下載。
如果用如下的代碼獲取test.ts的fps,
可以得到:
用ffprobe對視頻進行分析,可以得到:
從 opencv/modules/videoio/src/cap_ffmpeg_impl.hpp[2]中,我們發現fps由CvCapture_FFMPEG::get計算而來,其計算邏輯如下:
2、為什么OpenCV得到的幀率是錯的
利用test_time_base.cpp,我們可以得到:
所以OpenCV采用了:
來計算該視頻的fps。而此處的time_base = 1/2000,因此,最終得到的fps是2000。
也就是說,AVStream->codec->time_base的值導致了OpenCV得到一個看起來是錯誤的fps。那么,AVStream->codec->time_base為什么是這個值呢?FFmpeg是怎么計算這個字段的呢?
3、FFmpeg 如何計算
AVCodecContext.time_base
AVStream->codec->time_base是AVCodecContext中定義的 time_base字段,根據libavcodec/avcodec.h[4]中的定義可知,對于解碼而言,time_base已經被廢棄,需要使用framerate來替換 time_base。并且,對于固定幀率而言,time_base = 1/framerate,但并非總是如此。
利用H264Naked對test.ts對應的H.264碼流進行分析,我們得到SPS.Vui信息:
從中可以看到,test.ts是非固定幀率視頻。從test_time_base.cpp的結果看,test.ts視頻中,framerate = 0/0,而time_base = 1/2000。
難道,對于非固定幀率視頻而言,time_base和framerate之間沒有關聯?如果存在關聯,那又是怎樣的運算才能產生這種結果?這個 time_base究竟是怎么計算的呢?究竟和framerate有沒有關系呢?一連串的問題隨之而來……
源碼面前,了無秘密。接下來,帶著這個問題,我們來一起分析一下FFmpeg究竟是如何處理time_base的。
3.1 avformat_find_stream_info
在 FFmpeg中,avformat_find_stream_info()對ic->streams[video_stream]->codec進行初始化,因此我們可以從avformat_find_stream_info()開始分析。
從libavformat/avformat.h[6]中,可以得知avformat_open_input()會打開視頻流,從中讀取相關的信息,然后存儲在AVFormatContext中,但是有時候,此處獲取的信息并不完整,因此需要調用avformat_find_stream_info()來獲取更多的信息。
需要注意的是:
avformat_find_stream_info()會嘗試通過解碼部分視頻幀來獲取需要的信息。
avformat_find_stream_info()的整體邏輯大致如下圖所示,其中特別需要關注圖中所示的 7 個步驟:
3.2 avformat_find_stream_info()的重要步驟說明
STEP 1 設置線程數,避免H.264多線程解碼時沒有把SPS/PPS信息提取到extradata。
STEP 2 設置AVStream *st,st會在后續的函數調用中一直透傳到try_decode_frame()。
STEP 3比較簡單,這里不再贅述。
STEP 4 設置AVCodecContext *avctx為透傳的st->internal->avctx,在后續的解碼函數調用中,一直透傳的就是這個avctx,因此,從這里開始的執行流程,FFmpeg使用的全部都是st->internal->avctx,而不是st->codec,這里要特別的注意。此處同時會設置解碼的線程數,其目的和STEP 1是一致的。
STEP 5 因為之前設置了解碼線程數為1,所以此處會調用
來解碼并計算avctx->framerate。注意,此處的avctx實際上是透傳而來的st->internal->avctx。計算 framerate的邏輯會在如何計算framerate部分介紹。
STEP 6 根據解碼器得到的framerate信息來計算 avctx->time_base,注意此處實際上是st->internal->avctx->time_base。根據如何計算framerate可知,此處framerate ={1000, 1}。根據 AVCodecContext.ticks_per_frame的介紹可知,ticks_per_frame = 2。因此,此處avctx->time_base ={1, 2000}:
STEP 7 這一步可謂是“瞞天過海,明修棧道暗度陳倉”。這一步為了解決API的前向兼容,做了一個替換,把st->internal->avctx->time_base 賦值給了st->codec->time_base,而把st->avg_frame_rate 賦值給了 st->codec->framerate。因此:
st->codec->time_base 的計算和 st->codec->framerate 之間沒有任何關系,而是和 st->internal->avctx->framerate 有關。究其本質,是和sps.time_scale,sps.num_units_in_tick有關。
3.3 internal->avctx->time_base &internal->framerate
所以實際上,internal->avctx->time_base為:
而internal->avctx->framerate則是:
因此,對于 H.264 碼流而言,time_base = 1 / (2 * framerate),而不是1 /framerate。
這也就是為什么
libavcodec/avcodec.h中說:
從如上的分析可以知道:
因此,當st->avg_frame_rate = 0時,OpenCV計算fps的邏輯是錯誤的。
在H.265中,ticks_per_frame = 1,因此對于H.265的編碼,OpenCV是沒有這個問題的。可以使用Zond 265 工具來分析一個 H.265的視頻碼流,然后對照OpenCV以及FFmpeg的結果來驗證。
同時,正是如上所示的STEP 7中的移花接木導致了 test_time_base.cpp[3]的結果:
3.4 ff_h264_decoder
libavcodec/decode.c[8]中的
decode_simple_internal()會調用對應的解碼器來進行解碼(STEP5)。而正如前所示,test.ts為H.264 編碼的視頻流,因此此處會調用 H.264 解碼器來進行解碼。在FFmpeg中,H.264解碼器位于 libavcodec/h264dec.c[9]中定義的
const AVCodec ff_h264_decoder。
在上文圖中的STEP 5中,
實際調用的就是:
而此處的avctx也就是
try_decode_frame()中透傳下來的st->internal->avctx,即上文圖中的STEP4。
3.5 h264_decode_frame
h264_decode_frame()的整體邏輯如下圖所示:
3.6 AVCodecContext.ticks_per_frame
后面會用到ticks_per_frame來計算framerate。在STEP6中計算 time_base的時候也用到了該值。因此,有必要做一下特殊說明。在H.264解碼器中,ticks_per_frame=2,其具體的取值可以從如下幾處得知:
libavcodec/avcodec.h 中的字段說明:
libavcodec/h264dec.c 中的h264_decode_init():
4、如何計算framerate
STEP 1 根據整體的計算流程可知,此處的h實際上就是
avformat_find_stream_info()中的
st->internal->avctx->priv_data。h會一直透傳到之后的所有流程,這個務必要注意。
STEP 2此處會首先獲取到sps的相關信息,以備后續的計算使用,我們可以再次看一下test.ts sps的相關信息。
STEP 3根據sps的相關信息計算framerate,在上文的STEP 6中計算 time_base用到的framerate就是在此處計算的。因為 timing_info_present_flag = 1,因此會執行計算framerate的邏輯:
因此,
但是,因為avctx->time_base={1,2000},所以OpenCV計算出來的幀率結果為2000。導致這種不一致的原因在于,OpenCV在使用codec->time_base計算幀率的時候沒有考慮ticks_per_frame。因此,對于OpenCV而言,正確的計算幀率的方式應該為:
結論
通過上面的分析我們可以知道:
FFmpeg在計算 AVCodecContex 中的framerate和time_base的時候,會用到:
o sps.time_scale
o sps.num_units_in_tick
o AVCodecContex.ticks_per_frame
在 FFmpeg 中,framerate和time_base的關系為:
o framerate = 1 / (time_base *ticks_per_frame)
o time_base = 1 / (framerate *ticks_per_frame)
對于非 H.264/MPEG-2,
ticks_per_frame=1,因此framerate和time_base是互為倒數的關系。而對于H.264/MPEG-2 而言,ticks_per_frame=2,因此,此時二者并非是互為倒數的關系。因而,FFmpeg 中才說,framerate和time_base通常是互為倒數的關系,但并非總是如此。
在OpenCV中,對于H.264/MPEG-2視頻而言,當
AVStream.avg_frame_rate=0時,其計算fps的邏輯存在BUG。
因為在解碼時,
AVCodecContex.time_base已經廢棄,同時 AVStream.avctx也已經廢棄,而
avformat_find_stream_info()中為了兼容老的API,因此會利用 AVStream.internal.avctx和其他的信息來設置AVStream.avctx。而AVStream.avctx.time_base取自AVStream.internal.avctx,AVStream.avctx.framerate 則取自 AVStream.framerate。
審核編輯:劉清
-
H.264解碼器
+關注
關注
0文章
2瀏覽量
6319 -
OpenCV
+關注
關注
31文章
635瀏覽量
41343 -
ffmpeg
+關注
關注
0文章
46瀏覽量
7398
原文標題:為什么OpenCV計算的幀率是錯誤的?
文章出處:【微信號:livevideostack,微信公眾號:LiveVideoStack】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論