作者 | 王偉、劉一卓
導讀
網絡直播功能作為一項網際網路基本能力已經越來越重要,手機中的直播功能也越來越完善,電商直播、新聞直播、娛樂直播等多種直播類型為使用者提供了豐富的直播内容。
随着直播的普及,為使用者提供極速、流暢的直播觀看體驗我們有一個平台來周期性的對線上的直播流資料進行某些檢測,例如黑/白屏檢測、靜态畫面檢測……
在檢測中,我們會根據提取到的直播流的幀率來預估要計算的幀數量,例如如果要檢測 5s 的直播流,而該直播流的幀率為 20 fps,需要計算的幀數量則為 100。忽然有一天,我們發現,平台開始大面積的逾時,之前隻需要 2s 就能完成的計算,現在卻需要 30+ 分鐘。
查了之後,我們發現,之是以計算逾時是因為 OpenCV 計算的幀率為 2000,進而導緻需要計算的幀數量從之前的 100 變為了 10000,進而引起了計算逾時。
全文9288字,預計閱讀時間24分鐘。
01 OpenCV 如何計算幀率
這個問題的具體描述可以參見 OpenCV Issues 21006。該問題的模拟直播流片段 test.ts 可以點選連結(https://pan.baidu.com/share/init?surl=RY0Zk5C_DOEwTXYe2SLFEg)下載下傳,下載下傳提取碼為 x87m。
如果用如下的代碼擷取 test.ts 的 fps,
const double FPS = cap.get(cv::CAP_PROP_FPS);
std::cout << "fps: " << FPS << std::endl;
可以得到:
$ fps: 2000
用 ffprobe 對視訊進行分析,
$ ffprobe -select_streams v -show_streams test.ts
可以得到:
codec_name=h264
r_frame_rate=30/1
avg_frame_rate=0/0
……
從 opencv/modules/videoio/src/cap_ffmpeg_impl.hpp 中,我們發現 fps 由 CvCapture_FFMPEG::get_fps() 計算而來,其計算邏輯如下:
double fps = r2d(ic->streams[video_stream]->avg_frame_rate);
if (fps < eps_zero) {
fps = 1.0 / r2d(ic->streams[video_stream]->codec->time_base);
}
02 為什麼 OpenCV 得到的幀率是錯的
利用 test_time_base.cpp,我們可以得到:
time_base: 1/2000
framerate: 0/0
avg_framerate: 0/0
r2d(ic->streams[video_stream]->avg_frame_rate) = 0
是以 OpenCV 采用了:
1.0 / r2d(ic->streams[video_stream]->codec->time_base)
來計算該視訊的 fps。而此處的 time_base = 1/2000,是以,最終得到的 fps 是 2000。
也就是說,AVStream->codec->time_base 的值導緻了 OpenCV 得到一個看起來是錯誤的 fps。那麼,AVStream->codec->time_base 為什麼是這個數呢?FFMpeg 是怎麼計算這個字段的呢?
03 FFMpeg 如何計算 AVCodecContext.time_base
AVStream->codec->time_base 是 AVCodecContext 中定義的 time_base 字段,根據 libavcodec/avcodec.h中的定義,該字段的解釋如下:
/**
* This is the fundamental unit of time (in seconds) in terms
* of which frame timestamps are represented. For fixed-fps content,
* timebase should be 1/framerate and timestamp increments should be
* identically 1.
* This often, but not always is the inverse of the frame rate or field rate
* for video. 1/time_base is not the average frame rate if the frame rate is not
* constant.
*
* Like containers, elementary streams also can store timestamps, 1/time_base
* is the unit in which these timestamps are specified.
* As example of such codec time base see ISO/IEC 14496-2:2001(E)
* vop_time_increment_resolution and fixed_vop_rate
* (fixed_vop_rate == 0 implies that it is different from the framerate)
*
* - encoding: MUST be set by user.
* - decoding: the use of this field for decoding is deprecated.
* Use framerate instead.
*/
AVRational time_base;
從中可以看出,對于解碼而言,time_base 已經被廢棄,需要使用 framerate 來替換 time_base。并且,對于固定幀率而言,time_base = 1/framerate,但是,并非總是如此。
利用 H264Naked 對 test.ts 對應的 H264 碼流進行分析,我們得到 SPS.Vui 資訊:
timing_info_present_flag :1
num_units_in_tick :1
time_scale :2000
fixed_frame_rate_flag :0
從中可以看到,test.ts 是非固定幀率視訊。從 test_time_base.cpp 的結果看,test.ts 視訊中,framerate = 0/0,而 time_base = 1/2000。
難道,對于非固定幀率視訊而言,time_base 和 framerate 之間沒有關聯?如果存在關聯,那又是怎樣的運算才能産生這種結果?這個 time_base 究竟是怎麼計算的呢?究竟和 framerate 有沒有關系呢?一連串的問題随之而來……
源碼面前,了無秘密。接下來,帶着這個問題,我們來一起分析一下 FFMpeg 究竟是如何處理 time_base 的。
04 avformat_find_stream_info
在 FFMpeg 中,avformat_find_stream_info() 會對 ic->streams[video_stream]->codec 進行初始化,是以,我們可以從 avformat_find_stream_info() 開始分析。
從 libavformat/avformat.h 中,可以得知avformat_open_input()會打開視訊流,從中讀取相關的資訊,然後存儲在AVFormatContext中,但是有時候,此處擷取的資訊并不完整,是以需要調用**avformat_find_stream_info()**來擷取更多的資訊。
* @section lavf_decoding_open Opening a media file
* The minimum information required to open a file is its URL, which
* is passed to avformat_open_input(), as in the following code:
* @code
* const char *url = "file:in.mp3";
* AVFormatContext *s = NULL;
* int ret = avformat_open_input(&s, url, NULL, NULL);
* if (ret < 0)
* abort();
* @endcode
* The above code attempts to allocate an AVFormatContext, open the
* specified file (autodetecting the format) and read the header, exporting the
* information stored there into s. Some formats do not have a header or do not
* store enough information there, so it is recommended that you call the
* avformat_find_stream_info() function which tries to read and decode a few
* frames to find missing information.
需要注意的是:avformat_find_stream_info() 會嘗試通過解碼部分視訊幀來擷取需要的資訊。
/**
* Read packets of a media file to get stream information. This
* is useful for file formats with no headers such as MPEG. This
* function also computes the real framerate in case of MPEG-2 repeat
* frame mode.
* The logical file position is not changed by this function;
* examined packets may be buffered for later processing.
*
* @param ic media file handle
* @param options If non-NULL, an ic.nb_streams long array of pointers to
* dictionaries, where i-th member contains options for
* codec corresponding to i-th stream.
* On return each dictionary will be filled with options that were not found.
* @return >=0 if OK, AVERROR_xxx on error
*
* @note this function isn't guaranteed to open all the codecs, so
* options being non-empty at return is a perfectly normal behavior.
*
* @todo Let the user decide somehow what information is needed so that
* we do not waste time getting stuff the user does not need.
*/
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
avformat_find_stream_info() 的整體邏輯大緻如下圖所示,其中特别需要關注圖中所示的 7 個步驟:
avformat_find_stream_info() 的重要步驟說明
- STEP 1. 設定線程數,避免 H264 多線程解碼時沒有把 SPS/PPS 資訊提取到 extradata。
- STEP 2. 設定 *AVStream st,st 會在後續的函數調用中一直透到 try_decode_frame()。
- STEP 4. 設定 *AVCodecContext avctx 為透傳的 st->internal->avctx,在後續的解碼函數調用中,一直透傳的就是這個 avctx,是以,從這裡開始的執行流程,FFMpeg 使用的全部都是 st->internal->avctx,而不是 st->codec,這裡要特别的注意。此處同時會設定解碼的線程數,其目的和 STEP 1是一緻的。
- STEP 5. 因為之前設定了解碼線程數為 1,是以此處會調用
ret = avctx->codec->decode(avctx, frame, &got_frame, pkt)
來解碼并計算 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_fram****e = 2。是以,此處 avctx->time_base = {1, 2000}:
avctx->time_base = av_inv_q(av_mul_q({1000, 1}, {2, 1})) = {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 = {1, 2000}
st->codec->framerate = {0, 0}
st->codec->time_base 的計算和 st->codec->framerate 之間沒有任何關系,而是和 st->internal->avctx->framerate 有關。本質而言,和 sps.time_scale,sps.num_units_in_tick 有關。
st->internal->avctx->time_base.num = sps->num_units_in_tick *
st->internal->avctx->ticks_per_frame
st->internal->avctx->time_base.den = sps->time_scale *
st->internal->avctx->ticks_per_frame;
st->internal->avctx->time_base = {sps->num_units_in_tick, sps->time_scale}
internal->avctx->time_base & internal->framerate
- 是以實際上,internal->avctx->time_base 為:
avctx->time_base = sps->num_units_in_tick / sps->time_scale
- 而,internal->avctx->framerate 則是:
avctx->time_base = sps->num_units_in_tick / sps->time_scale
是以,對于 H264 碼流而言,time_base = 1 / (2 * framerate),而不是 1 / framerate。
這也就是為什麼 libavcodec/avcodec.h 中說:
從如上的分析可以知道:* This often, but not always is the inverse of the frame rate or field rate * for video.
avctx->framerate = 1 / (avctx->time_base * avctx->ticks_per_frame)
是以,當 st->avg_frame_rate = 0 時,OpenCV 計算 fps 的邏輯 是錯誤的。
在 H265 中,ticks_per_frame = 1,是以對于 H265 的編碼,OpenCV 是沒有這個問題的。可以使用 Zond 265工具來分析一個 H265 的視訊碼流,然後對照 OpenCV 以及 FFMpeg 的結果來驗證。
同時,正是如上所示的 STEP 7 中的移花接木導緻了 test_time_base.cpp 的結果:
st->codec->framerate: 0/0
st->codec->time_base: 1/2000
05 ff_h264_decoder
libavcodec/decode.c 中的 decode_simple_internal() 中會調用對應的解碼器來進行解碼(STPE 5)。而正如前所示,test.ts 為 H264 編碼的視訊流,是以,此處會調用 H264 解碼器來進行解碼。在 FFMpeg 中,H264 解碼器位于 libavcodec/h264dec.c 中定義的 const AVCodec ff_h264_decoder。
const AVCodec ff_h264_decoder = {
.name = "h264",
.type = AVMEDIA_TYPE_VIDEO,
.id = AV_CODEC_ID_H264,
.priv_data_size = sizeof(H264Context),
.init = h264_decode_init,
.close = h264_decode_end,
.decode = h264_decode_frame,
......
};
在上文圖中的 STPE 5 中,
ret = avctx->codec->decode(avctx, frame, &got_frame, pkt);
實際調用的就是
ff_h264_decoder->h264_decode_frame(avctx, frame, &got_frame, pkt);
而此處的 avctx 也就是 try_decode_frame() 中的透傳下來的 st->internal->avctx,即上文圖中的 STEP 4。
06 h264_decode_frame
h264_decode\frame() 的整體邏輯如下圖所示:
AVCodecContext.ticks_per_frame
後面會用到 ticks_per_frame 來計算 framerate。在 STEP 6 中計算 time_base 的時候也用到了該值。是以,有必要做一下特殊說明。在 H264 解碼器中,ticks_per_frame=2,其具體的取值可以從如下幾處得知:
libavcodec/avcodec.h 中的字段說明:
/**
* For some codecs, the time base is closer to the field rate than the frame rate.
* Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
* if no telecine is used ...
*
* Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
*/
int ticks_per_frame;
libavcodec/h264dec.c 中的 h264_decode_init():
avctx->ticks_per_frame = 2;
07 如何計算 framerate
如何計算 st->internal->avctx->framerate
- STEP 1. 根據整體的計算流程可知,此處的 h 實際上就是 avformat_find_stream_info() 中的 st->internal->avctx->priv_data。h 會一直透傳到之後的所有流程,這個務必要注意。
- STEP 2. 此處會首先擷取到 sps 的相關資訊,以備後續的計算使用,我們可以再次看一下 test.ts sps 的相關資訊。
timing_info_present_flag :1
num_units_in_tick :1
time_scale :2000
fixed_frame_rate_flag :0
- STEP 3. 根據 sps 的相關資訊計算 framerate,在上文的 STEP 6 中計算 time_base 用到的 framerate 就是在此處計算的。因為 timing_info_present_flag = 1,是以會執行計算 framerate 的邏輯:
avctx->framerate.den = sps->num_units_in_tick * h->avctx->ticks_per_frame = 1 * 2 = 2
avctx->framerate.num = sps->time_scale = 2000
avctx->framerate = (AVRational){1000, 1}
是以,
st->internal->avctx->framerate = {1000, 1}
08 結論
通過如上的分析我們可以知道:
- FFMpeg 在計算 AVCodecContex 中的 framerate 和 time_base 的時候,會用到:
- sps.time_scale
- sps.num_units_in_tick
- AVCodecContex.ticks_per_frame
- 在 FFMpeg 中,framerate 和 time_base 的關系為:
- framerate = 1 / (time_base * ticks_per_frame)
- 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。
————END————
推薦閱讀:
百度 Android 直播秒開體驗優化
iOS SIGKILL 信号量崩潰抓取以及優化實踐
如何在幾百萬qps的網關服務中實作靈活排程政策
深入淺出DDD程式設計
百度APP iOS端記憶體優化實踐-記憶體管控方案
Ernie-SimCSE對比學習在内容反作弊上應用