Skip to content

Commit

Permalink
Encoded Frame Size Estimator component
Browse files Browse the repository at this point in the history
The encoded frame size estimate is based on QP values of current and
previous frames and stats of previously encoded frame data. The frame
size estimator uses a weighted moving average filter to keep the
statistics.

Bug: 1234020
Change-Id: I2bffeb4e48060d69b4a147aa706c8077c3cb184a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4915782
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Mosa Morosev <mosamorosev@microsoft.com>
Cr-Commit-Position: refs/heads/main@{#1211993}
  • Loading branch information
mosamorosev authored and Chromium LUCI CQ committed Oct 19, 2023
1 parent 277a5bb commit c7eed3f
Show file tree
Hide file tree
Showing 7 changed files with 471 additions and 0 deletions.
6 changes: 6 additions & 0 deletions media/gpu/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,10 @@ source_set("common") {
"accelerated_video_decoder.h",
"codec_picture.cc",
"codec_picture.h",
"exponential_moving_average.cc",
"exponential_moving_average.h",
"frame_size_estimator.cc",
"frame_size_estimator.h",
"gpu_video_decode_accelerator_helpers.cc",
"gpu_video_decode_accelerator_helpers.h",
"gpu_video_encode_accelerator_helpers.cc",
Expand Down Expand Up @@ -586,6 +590,8 @@ source_set("unit_tests") {
"//ui/gl:test_support",
]
sources = [
"exponential_moving_average_unittest.cc",
"frame_size_estimator_unittest.cc",
"h264_decoder_unittest.cc",
"hrd_buffer_unittest.cc",
]
Expand Down
23 changes: 23 additions & 0 deletions media/gpu/exponential_moving_average.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/gpu/exponential_moving_average.h"

#include "base/check.h"
#include "base/check_op.h"
#include "base/logging.h"

namespace media {

ExponentialMovingAverage::ExponentialMovingAverage(
base::TimeDelta max_window_size)
: max_window_size_(max_window_size) {}

ExponentialMovingAverage::~ExponentialMovingAverage() = default;

float ExponentialMovingAverage::GetStdDeviation() const {
return std::sqrtf(std::max(mean_square_ - std::pow(mean_, 2), 0.0));
}

} // namespace media
77 changes: 77 additions & 0 deletions media/gpu/exponential_moving_average.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef MEDIA_GPU_EXPONENTIAL_MOVING_AVERAGE_H_
#define MEDIA_GPU_EXPONENTIAL_MOVING_AVERAGE_H_

#include <algorithm>
#include <memory>

#include "base/time/time.h"
#include "media/gpu/media_gpu_export.h"

namespace media {

// An Exponential Moving Average filter.
// It is an implementation of exponential moving average filter with a time
// constant. The effective window size equals to the time elapsed since the
// first sample was added, until the maximum window size is reached. This makes
// a difference to the standard exponential moving average filter
// implementation. Alpha, the time constant, is calculated as a ratio between
// the time period passed from the last added sample and the effective window
// size.
// mean += alpha * (value - mean)
// mean_square += alpha * (value^2 - mean_square)
// std_dev = sqrt(mean_square - mean^2)
// alpha = elapsed_time / curr_window_size
class MEDIA_GPU_EXPORT ExponentialMovingAverage {
public:
explicit ExponentialMovingAverage(base::TimeDelta max_window_size);
~ExponentialMovingAverage();

ExponentialMovingAverage(const ExponentialMovingAverage& other) = delete;
ExponentialMovingAverage& operator=(const ExponentialMovingAverage& other) =
delete;

base::TimeDelta curr_window_size() const { return curr_window_size_; }
base::TimeDelta max_window_size() const { return max_window_size_; }

float mean() const { return mean_; }

void update_max_window_size(base::TimeDelta max_window_size) {
max_window_size_ = max_window_size;
}

// Adds a new value to the filter. The T type is casted to the float value.
// Elapsed time is the period between the current and previous sample.
template <typename T>
void AddValue(T value, base::TimeDelta elapsed_time) {
float float_value = static_cast<float>(value);
// The minimum window size is 1ms. This is to avoid division by zero.
curr_window_size_ = std::clamp(curr_window_size_ + elapsed_time,
base::Milliseconds(1), max_window_size_);
float alpha = static_cast<float>(std::min(
elapsed_time.InMillisecondsF() / curr_window_size_.InMillisecondsF(),
1.0));
mean_ += alpha * (float_value - mean_);
mean_square_ += alpha * (float_value * float_value - mean_square_);
}

float GetStdDeviation() const;

private:
// Mean of values.
float mean_ = 0.0f;
// Mean of squared values.
float mean_square_ = 0.0f;

// Effective window size.
base::TimeDelta curr_window_size_;
// Max window size.
base::TimeDelta max_window_size_;
};

} // namespace media

#endif // MEDIA_GPU_EXPONENTIAL_MOVING_AVERAGE_H_
93 changes: 93 additions & 0 deletions media/gpu/exponential_moving_average_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/gpu/exponential_moving_average.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace media {
namespace {

// Test ExponentialMovingAverageTest adds the predefined values and checks
// whether the correct mean and standard deviation values are produced.
class ExponentialMovingAverageTest : public testing::Test {
public:
ExponentialMovingAverageTest() = default;

void SetUp() override {
moving_average_ =
std::make_unique<ExponentialMovingAverage>(base::Milliseconds(100));
EXPECT_EQ(base::TimeDelta(), moving_average_->curr_window_size());
EXPECT_EQ(base::Milliseconds(100), moving_average_->max_window_size());
}

protected:
std::unique_ptr<ExponentialMovingAverage> moving_average_;
};

// Test Cases

// Adding predefined sequence to the moving average filter and checking
// whether the stats are inside expected ranges. The filter is checked
// with two sequences using different window sizes.
TEST_F(ExponentialMovingAverageTest, RunBasicMovingAverageTest) {
constexpr float kExpectedMeanMin1 = 94.73f;
constexpr float kExpectedMeanMax1 = 94.74f;
constexpr float kExpectedStdDevMin1 = 1.72f;
constexpr float kExpectedStdDevMax1 = 1.73f;
constexpr float kExpectedMeanMin2 = 103.16f;
constexpr float kExpectedMeanMax2 = 103.17f;
constexpr float kExpectedStdDevMin2 = 3.19f;
constexpr float kExpectedStdDevMax2 = 3.20f;

base::TimeDelta timestamp = base::Microseconds(0);
moving_average_->AddValue(100, timestamp);
timestamp += base::Milliseconds(10);
moving_average_->AddValue(120, timestamp);
timestamp += base::Milliseconds(8);
moving_average_->AddValue(90, timestamp);
timestamp += base::Milliseconds(12);
moving_average_->AddValue(115, timestamp);
timestamp += base::Milliseconds(11);
moving_average_->AddValue(95, timestamp);
timestamp += base::Milliseconds(9);
moving_average_->AddValue(100, timestamp);
timestamp += base::Milliseconds(10);
moving_average_->AddValue(120, timestamp);
timestamp += base::Milliseconds(11);
moving_average_->AddValue(115, timestamp);
timestamp += base::Milliseconds(7);
moving_average_->AddValue(90, timestamp);
timestamp += base::Milliseconds(11);
moving_average_->AddValue(85, timestamp);
timestamp += base::Milliseconds(8);
moving_average_->AddValue(95, timestamp);

EXPECT_LT(kExpectedMeanMin1, moving_average_->mean());
EXPECT_GT(kExpectedMeanMax1, moving_average_->mean());
EXPECT_LT(kExpectedStdDevMin1, moving_average_->GetStdDeviation());
EXPECT_GT(kExpectedStdDevMax1, moving_average_->GetStdDeviation());

moving_average_->update_max_window_size(base::Milliseconds(200));
EXPECT_EQ(base::Milliseconds(100), moving_average_->curr_window_size());
EXPECT_EQ(base::Milliseconds(200), moving_average_->max_window_size());

moving_average_->AddValue(105, timestamp);
timestamp += base::Milliseconds(11);
moving_average_->AddValue(90, timestamp);
timestamp += base::Milliseconds(11);
moving_average_->AddValue(100, timestamp);
timestamp += base::Milliseconds(8);
moving_average_->AddValue(100, timestamp);
timestamp += base::Milliseconds(10);
moving_average_->AddValue(105, timestamp);

EXPECT_LT(kExpectedMeanMin2, moving_average_->mean());
EXPECT_GT(kExpectedMeanMax2, moving_average_->mean());
EXPECT_LT(kExpectedStdDevMin2, moving_average_->GetStdDeviation());
EXPECT_GT(kExpectedStdDevMax2, moving_average_->GetStdDeviation());
}

} // namespace

} // namespace media
73 changes: 73 additions & 0 deletions media/gpu/frame_size_estimator.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/gpu/frame_size_estimator.h"

#include "base/check.h"
#include "base/check_op.h"
#include "base/logging.h"

namespace media {
namespace {

// Maps QP to quantizer step size. 0.625 is Q-step value for QP=0 for H.26x
// codecs.
float Qp2QStepSize(uint32_t qp) {
return 0.625f * std::powf(2, qp / 6.0f);
}

void CalculateQSteps(uint32_t qp,
uint32_t qp_prev,
float& q_step,
float& q_step_prev,
float& delta_q_step_factor) {
q_step = Qp2QStepSize(qp);
q_step_prev = Qp2QStepSize(qp_prev);
delta_q_step_factor = q_step_prev / q_step;
}

} // namespace

FrameSizeEstimator::FrameSizeEstimator(base::TimeDelta max_window_size,
float initial_qp_size,
float initial_size_correction)
: qp_size_stats_(max_window_size), size_correction_stats_(max_window_size) {
// The elapsed time is initially set to 1 millisecond to match the minimum
// window size.
qp_size_stats_.AddValue(initial_qp_size, base::Milliseconds(1));
size_correction_stats_.AddValue(initial_size_correction,
base::Milliseconds(1));
}

FrameSizeEstimator::~FrameSizeEstimator() = default;

size_t FrameSizeEstimator::Estimate(uint32_t qp, uint32_t qp_prev) const {
float q_step, q_step_prev, delta_q_step_factor;
CalculateQSteps(qp, qp_prev, q_step, q_step_prev, delta_q_step_factor);
float pred_frame_byte = qp_size_stats_.mean() * delta_q_step_factor / q_step +
size_correction_stats_.mean();
return static_cast<size_t>(std::max(pred_frame_byte, 0.0f));
}

void FrameSizeEstimator::Update(size_t frame_bytes,
uint32_t qp,
uint32_t qp_prev,
base::TimeDelta elapsed_time) {
float q_step, q_step_prev, delta_q_step_factor;
CalculateQSteps(qp, qp_prev, q_step, q_step_prev, delta_q_step_factor);

float qp_size = q_step * frame_bytes / delta_q_step_factor;
qp_size_stats_.AddValue(qp_size, elapsed_time);

float corr =
frame_bytes - qp_size_stats_.mean() * delta_q_step_factor / q_step;
size_correction_stats_.AddValue(corr, elapsed_time);
}

void FrameSizeEstimator::UpdateMaxWindowSize(base::TimeDelta max_window_size) {
qp_size_stats_.update_max_window_size(max_window_size);
size_correction_stats_.update_max_window_size(max_window_size);
}

} // namespace media
70 changes: 70 additions & 0 deletions media/gpu/frame_size_estimator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef MEDIA_GPU_FRAME_SIZE_ESTIMATOR_H_
#define MEDIA_GPU_FRAME_SIZE_ESTIMATOR_H_

#include "base/time/time.h"
#include "media/gpu/exponential_moving_average.h"
#include "media/gpu/media_gpu_export.h"

namespace media {

// An encoded frame size estimator.
// The estimator maintains the history of intermediate values (qp_size_value)
// that are proportional to encoded frame size and QP, and inversely
// proportional to the QP ratio of the previous and the current frame
// (delta_q_step_factor). The QP is converted to Q step value that has linear
// dependency to the encoded frame size.
//
// q_step = 5 / 8 * 2^(qp / 6)
//
// delta_q_step_factor = q_step_prev / q_step
//
// qp_size_value = q_step * frame_bytes / delta_q_step_factor
//
// The prediction of the encoded frame size is based on average values of
// qp_size_value and qp_size_correction. The qp_size_correction is the
// difference between actual encoded bytes and the predicted value.
//
// qp_size_correction = frame_bytes -
// qp_size_value * delta_q_step_factor / q_step
//
// pred_frame_bytes =
// qp_size_value * delta_q_step_factor / q_step + qp_size_correction
class MEDIA_GPU_EXPORT FrameSizeEstimator {
public:
FrameSizeEstimator(base::TimeDelta max_window_size,
float initial_qp_size,
float initial_size_correction);
~FrameSizeEstimator();

FrameSizeEstimator(const FrameSizeEstimator& other) = delete;
FrameSizeEstimator& operator=(const FrameSizeEstimator& other) = delete;

// Estimates encoded frame size for the given qp and qp_prev, based on the
// stats of the previous frames. In usual encoding scenario, the current
// QP is unknown at this point, but the estimate of the QP parameter is used
// instead.
size_t Estimate(uint32_t qp, uint32_t qp_prev) const;

// Updates the frame size estimator state with the real encoded frame size and
// with the parameters used for video frame encoding.
void Update(size_t frame_bytes,
uint32_t qp,
uint32_t qp_prev,
base::TimeDelta elapsed_time);

float qp_size_mean() const { return qp_size_stats_.mean(); }
float size_correction_mean() const { return size_correction_stats_.mean(); }
void UpdateMaxWindowSize(base::TimeDelta max_window_size);

private:
ExponentialMovingAverage qp_size_stats_;
ExponentialMovingAverage size_correction_stats_;
};

} // namespace media

#endif // MEDIA_GPU_FRAME_SIZE_ESTIMATOR_H_

0 comments on commit c7eed3f

Please sign in to comment.