diff --git a/common_video/BUILD.gn b/common_video/BUILD.gn index ccb93ee627..c0cd959948 100644 --- a/common_video/BUILD.gn +++ b/common_video/BUILD.gn @@ -13,6 +13,8 @@ rtc_static_library("common_video") { sources = [ "bitrate_adjuster.cc", + "frame_rate_estimator.cc", + "frame_rate_estimator.h", "h264/h264_bitstream_parser.cc", "h264/h264_bitstream_parser.h", "h264/h264_common.cc", @@ -42,6 +44,8 @@ rtc_static_library("common_video") { deps = [ "../api:scoped_refptr", "../api/task_queue", + "../api/units:time_delta", + "../api/units:timestamp", "../api/video:encoded_image", "../api/video:video_bitrate_allocation", "../api/video:video_bitrate_allocator", @@ -78,6 +82,7 @@ if (rtc_include_tests) { sources = [ "bitrate_adjuster_unittest.cc", + "frame_rate_estimator_unittest.cc", "h264/h264_bitstream_parser_unittest.cc", "h264/pps_parser_unittest.cc", "h264/profile_level_id_unittest.cc", @@ -102,6 +107,7 @@ if (rtc_include_tests) { "../rtc_base:checks", "../rtc_base:rtc_base_approved", "../rtc_base:rtc_base_tests_utils", + "../system_wrappers:system_wrappers", "../test:fileutils", "../test:test_main", "../test:test_support", diff --git a/common_video/frame_rate_estimator.cc b/common_video/frame_rate_estimator.cc new file mode 100644 index 0000000000..86f07228e7 --- /dev/null +++ b/common_video/frame_rate_estimator.cc @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "common_video/frame_rate_estimator.h" + +#include "rtc_base/time_utils.h" + +namespace webrtc { + +FrameRateEstimator::FrameRateEstimator(TimeDelta averaging_window) + : averaging_window_(averaging_window) {} + +void FrameRateEstimator::OnFrame(Timestamp time) { + CullOld(time); + frame_times_.push_back(time); +} + +absl::optional FrameRateEstimator::GetAverageFps() const { + if (frame_times_.size() < 2) { + return absl::nullopt; + } + TimeDelta time_span = frame_times_.back() - frame_times_.front(); + if (time_span < TimeDelta::us(1)) { + return absl::nullopt; + } + TimeDelta avg_frame_interval = time_span / (frame_times_.size() - 1); + + return static_cast(rtc::kNumMicrosecsPerSec) / + avg_frame_interval.us(); +} + +absl::optional FrameRateEstimator::GetAverageFps(Timestamp now) { + CullOld(now); + return GetAverageFps(); +} + +void FrameRateEstimator::Reset() { + frame_times_.clear(); +} + +void FrameRateEstimator::CullOld(Timestamp now) { + while (!frame_times_.empty() && + frame_times_.front() + averaging_window_ < now) { + frame_times_.pop_front(); + } +} + +} // namespace webrtc diff --git a/common_video/frame_rate_estimator.h b/common_video/frame_rate_estimator.h new file mode 100644 index 0000000000..4cdd284c34 --- /dev/null +++ b/common_video/frame_rate_estimator.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef COMMON_VIDEO_FRAME_RATE_ESTIMATOR_H_ +#define COMMON_VIDEO_FRAME_RATE_ESTIMATOR_H_ + +#include + +#include "absl/types/optional.h" +#include "api/units/time_delta.h" +#include "api/units/timestamp.h" + +namespace webrtc { + +// Class used to estimate a frame-rate using inter-frame intervals. +// Some notes on usage: +// This class is intended to accurately estimate the frame rate during a +// continuous stream. Unlike a traditional rate estimator that looks at number +// of data points within a time window, if the input stops this implementation +// will not smoothly fall down towards 0. This is done so that the estimated +// fps is not affected by edge conditions like if we sample just before or just +// after the next frame. +// To avoid problems if a stream is stopped and restarted (where estimated fps +// could look too low), users of this class should explicitly call Reset() on +// restart. +// Also note that this class is not thread safe, it's up to the user to guard +// against concurrent access. +class FrameRateEstimator { + public: + explicit FrameRateEstimator(TimeDelta averaging_window); + + // Insert a frame, potentially culling old frames that falls outside the + // averaging window. + void OnFrame(Timestamp time); + + // Get the current average FPS, based on the frames currently in the window. + absl::optional GetAverageFps() const; + + // Move the window so it ends at |now|, and return the new fps estimate. + absl::optional GetAverageFps(Timestamp now); + + // Completely clear the averaging window. + void Reset(); + + private: + void CullOld(Timestamp now); + const TimeDelta averaging_window_; + std::deque frame_times_; +}; + +} // namespace webrtc + +#endif // COMMON_VIDEO_FRAME_RATE_ESTIMATOR_H_ diff --git a/common_video/frame_rate_estimator_unittest.cc b/common_video/frame_rate_estimator_unittest.cc new file mode 100644 index 0000000000..9058bac414 --- /dev/null +++ b/common_video/frame_rate_estimator_unittest.cc @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2019 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "common_video/frame_rate_estimator.h" + +#include "system_wrappers/include/clock.h" +#include "test/gmock.h" +#include "test/gtest.h" + +namespace webrtc { +namespace { +constexpr TimeDelta kDefaultWindow = TimeDelta::Millis<1000>(); +} + +class FrameRateEstimatorTest : public ::testing::Test { + public: + FrameRateEstimatorTest() : clock_(123), estimator_(kDefaultWindow) {} + + protected: + SimulatedClock clock_; + FrameRateEstimator estimator_; +}; + +TEST_F(FrameRateEstimatorTest, NoEstimateWithLessThanTwoFrames) { + EXPECT_FALSE(estimator_.GetAverageFps()); + estimator_.OnFrame(clock_.CurrentTime()); + EXPECT_FALSE(estimator_.GetAverageFps()); + clock_.AdvanceTime(TimeDelta::ms(33)); + EXPECT_FALSE(estimator_.GetAverageFps()); +} + +TEST_F(FrameRateEstimatorTest, NoEstimateWithZeroSpan) { + // Two frames, but they are spanning 0ms so can't estimate frame rate. + estimator_.OnFrame(clock_.CurrentTime()); + estimator_.OnFrame(clock_.CurrentTime()); + EXPECT_FALSE(estimator_.GetAverageFps()); +} + +TEST_F(FrameRateEstimatorTest, SingleSpanFps) { + const double kExpectedFps = 30.0; + estimator_.OnFrame(clock_.CurrentTime()); + clock_.AdvanceTime(TimeDelta::seconds(1) / kExpectedFps); + estimator_.OnFrame(clock_.CurrentTime()); + EXPECT_NEAR(*estimator_.GetAverageFps(), kExpectedFps, 0.001); +} + +TEST_F(FrameRateEstimatorTest, AverageFps) { + // Insert frames a intervals corresponding to 10fps for half the window, then + // 40fps half the window. The average should be 20fps. + const double kLowFps = 10.0; + const double kHighFps = 30.0; + const double kExpectedFps = 20.0; + + const Timestamp start_time = clock_.CurrentTime(); + while (clock_.CurrentTime() - start_time < kDefaultWindow / 2) { + estimator_.OnFrame(clock_.CurrentTime()); + clock_.AdvanceTime(TimeDelta::seconds(1) / kLowFps); + } + while (clock_.CurrentTime() - start_time < kDefaultWindow) { + estimator_.OnFrame(clock_.CurrentTime()); + clock_.AdvanceTime(TimeDelta::seconds(1) / kHighFps); + } + + EXPECT_NEAR(*estimator_.GetAverageFps(), kExpectedFps, 0.001); +} + +TEST_F(FrameRateEstimatorTest, CullsOldFramesFromAveragingWindow) { + // Two frames, just on the border of the 1s window => 1 fps. + estimator_.OnFrame(clock_.CurrentTime()); + clock_.AdvanceTime(kDefaultWindow); + estimator_.OnFrame(clock_.CurrentTime()); + EXPECT_TRUE(estimator_.GetAverageFps()); + EXPECT_NEAR(*estimator_.GetAverageFps(), 1.0, 0.001); + + // Oldest frame should just be pushed out the window, leaving a single frame + // => no estimate possible. + clock_.AdvanceTime(TimeDelta::us(1)); + EXPECT_FALSE(estimator_.GetAverageFps(clock_.CurrentTime())); +} + +TEST_F(FrameRateEstimatorTest, Reset) { + estimator_.OnFrame(clock_.CurrentTime()); + clock_.AdvanceTime(TimeDelta::seconds(1) / 30); + estimator_.OnFrame(clock_.CurrentTime()); + EXPECT_TRUE(estimator_.GetAverageFps()); + + // Clear estimator, no estimate should be possible even after inserting one + // new frame. + estimator_.Reset(); + EXPECT_FALSE(estimator_.GetAverageFps()); + clock_.AdvanceTime(TimeDelta::seconds(1) / 30); + estimator_.OnFrame(clock_.CurrentTime()); + EXPECT_FALSE(estimator_.GetAverageFps()); +} + +} // namespace webrtc