Generalize MovingMedianFilter to MovingPercentileFilter

* Make `percentile` configurable and rename class.
* Introduce convenience type `MovingMedianFilter` that
  maintains the behaviour of the old class with that name.
* Move home grown moving 95th percentile filter in
  `JitterEstimator` to this new utility class.

Bug: webrtc:14151
Change-Id: I17d525b6e0bc98c28568c7dfe94b72eeab4a1ca2
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/275311
Commit-Queue: Rasmus Brandt <brandtr@webrtc.org>
Reviewed-by: Åsa Persson <asapersson@webrtc.org>
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38082}
diff --git a/modules/rtp_rtcp/include/remote_ntp_time_estimator.h b/modules/rtp_rtcp/include/remote_ntp_time_estimator.h
index e45ee39..01d0c85 100644
--- a/modules/rtp_rtcp/include/remote_ntp_time_estimator.h
+++ b/modules/rtp_rtcp/include/remote_ntp_time_estimator.h
@@ -16,7 +16,7 @@
 #include "absl/types/optional.h"
 #include "api/units/time_delta.h"
 #include "api/units/timestamp.h"
-#include "rtc_base/numerics/moving_median_filter.h"
+#include "rtc_base/numerics/moving_percentile_filter.h"
 #include "system_wrappers/include/rtp_to_ntp_estimator.h"
 
 namespace webrtc {
diff --git a/modules/video_coding/timing/jitter_estimator.cc b/modules/video_coding/timing/jitter_estimator.cc
index f5d6fdf..cc8680c 100644
--- a/modules/video_coding/timing/jitter_estimator.cc
+++ b/modules/video_coding/timing/jitter_estimator.cc
@@ -92,7 +92,9 @@
           config_.frame_size_window.value_or(kDefaultFrameSizeWindow))),
       max_frame_size_bytes_percentile_(
           config_.max_frame_size_percentile.value_or(
-              kDefaultMaxFrameSizePercentile)),
+              kDefaultMaxFrameSizePercentile),
+          static_cast<size_t>(
+              config_.frame_size_window.value_or(kDefaultFrameSizeWindow))),
       fps_counter_(30),  // TODO(sprang): Use an estimator with limit based
                          // on time, rather than number of samples.
       clock_(clock) {
@@ -108,7 +110,6 @@
   var_frame_size_bytes2_ = 100;
   avg_frame_size_median_bytes_.Reset();
   max_frame_size_bytes_percentile_.Reset();
-  frame_sizes_in_percentile_filter_ = std::queue<int64_t>();
   last_update_time_ = absl::nullopt;
   prev_estimate_ = absl::nullopt;
   prev_frame_size_ = absl::nullopt;
@@ -168,14 +169,6 @@
     avg_frame_size_median_bytes_.Insert(frame_size.bytes());
   }
   if (config_.MaxFrameSizePercentileEnabled()) {
-    frame_sizes_in_percentile_filter_.push(frame_size.bytes());
-    if (frame_sizes_in_percentile_filter_.size() >
-        static_cast<size_t>(
-            config_.frame_size_window.value_or(kDefaultFrameSizeWindow))) {
-      max_frame_size_bytes_percentile_.Erase(
-          frame_sizes_in_percentile_filter_.front());
-      frame_sizes_in_percentile_filter_.pop();
-    }
     max_frame_size_bytes_percentile_.Insert(frame_size.bytes());
   }
 
@@ -232,12 +225,16 @@
     // delayed. The next frame is of normal size (delta frame), and thus deltaFS
     // will be << 0. This removes all frame samples which arrives after a key
     // frame.
-    double max_frame_size_bytes = GetMaxFrameSizeEstimateBytes();
+    double filtered_max_frame_size_bytes =
+        config_.MaxFrameSizePercentileEnabled()
+            ? max_frame_size_bytes_percentile_.GetFilteredValue()
+            : max_frame_size_bytes_;
     if (delta_frame_bytes >
-        GetCongestionRejectionFactor() * max_frame_size_bytes) {
+        GetCongestionRejectionFactor() * filtered_max_frame_size_bytes) {
       // Update the Kalman filter with the new data
       kalman_filter_.PredictAndUpdate(frame_delay.ms(), delta_frame_bytes,
-                                      max_frame_size_bytes, var_noise_ms2_);
+                                      filtered_max_frame_size_bytes,
+                                      var_noise_ms2_);
     }
   } else {
     double num_stddev = (delay_deviation_ms >= 0) ? num_stddev_delay_outlier
@@ -268,16 +265,6 @@
   return config_;
 }
 
-double JitterEstimator::GetMaxFrameSizeEstimateBytes() const {
-  if (config_.MaxFrameSizePercentileEnabled()) {
-    RTC_DCHECK_GT(frame_sizes_in_percentile_filter_.size(), 1u);
-    RTC_DCHECK_LE(frame_sizes_in_percentile_filter_.size(),
-                  config_.frame_size_window.value_or(kDefaultFrameSizeWindow));
-    return max_frame_size_bytes_percentile_.GetPercentileValue();
-  }
-  return max_frame_size_bytes_;
-}
-
 double JitterEstimator::GetNumStddevDelayOutlier() const {
   return config_.num_stddev_delay_outlier.value_or(kNumStdDevDelayOutlier);
 }
@@ -357,8 +344,12 @@
       config_.avg_frame_size_median
           ? avg_frame_size_median_bytes_.GetFilteredValue()
           : avg_frame_size_bytes_;
+  double filtered_max_frame_size_bytes =
+      config_.MaxFrameSizePercentileEnabled()
+          ? max_frame_size_bytes_percentile_.GetFilteredValue()
+          : max_frame_size_bytes_;
   double worst_case_frame_size_deviation_bytes =
-      GetMaxFrameSizeEstimateBytes() - filtered_avg_frame_size_bytes;
+      filtered_max_frame_size_bytes - filtered_avg_frame_size_bytes;
   double ret_ms = kalman_filter_.GetFrameDelayVariationEstimateSizeBased(
                       worst_case_frame_size_deviation_bytes) +
                   NoiseThreshold();
diff --git a/modules/video_coding/timing/jitter_estimator.h b/modules/video_coding/timing/jitter_estimator.h
index bbb148d..d7561c2 100644
--- a/modules/video_coding/timing/jitter_estimator.h
+++ b/modules/video_coding/timing/jitter_estimator.h
@@ -24,8 +24,7 @@
 #include "modules/video_coding/timing/frame_delay_variation_kalman_filter.h"
 #include "modules/video_coding/timing/rtt_filter.h"
 #include "rtc_base/experiments/struct_parameters_parser.h"
-#include "rtc_base/numerics/moving_median_filter.h"
-#include "rtc_base/numerics/percentile_filter.h"
+#include "rtc_base/numerics/moving_percentile_filter.h"
 #include "rtc_base/rolling_accumulator.h"
 
 namespace webrtc {
@@ -133,7 +132,6 @@
 
  private:
   // These functions return values that could be overriden through the config.
-  double GetMaxFrameSizeEstimateBytes() const;
   double GetNumStddevDelayOutlier() const;
   double GetNumStddevSizeOutlier() const;
   double GetCongestionRejectionFactor() const;
@@ -176,10 +174,7 @@
   double max_frame_size_bytes_;
   // Percentile frame sized received (over a window). Only used if configured.
   MovingMedianFilter<int64_t> avg_frame_size_median_bytes_;
-  // TODO(webrtc:14151): Make `MovingMedianFilter` take a percentile value and
-  // switch `max_frame_size_bytes_percentile_` over to that class.
-  PercentileFilter<int64_t> max_frame_size_bytes_percentile_;
-  std::queue<int64_t> frame_sizes_in_percentile_filter_;
+  MovingPercentileFilter<int64_t> max_frame_size_bytes_percentile_;
   // TODO(bugs.webrtc.org/14381): Update `startup_frame_size_sum_bytes_` to
   // DataSize when api/units have sufficient precision.
   double startup_frame_size_sum_bytes_;
diff --git a/rtc_base/BUILD.gn b/rtc_base/BUILD.gn
index 12676a0..efd6388 100644
--- a/rtc_base/BUILD.gn
+++ b/rtc_base/BUILD.gn
@@ -763,7 +763,7 @@
     "numerics/math_utils.h",
     "numerics/moving_average.cc",
     "numerics/moving_average.h",
-    "numerics/moving_median_filter.h",
+    "numerics/moving_percentile_filter.h",
     "numerics/percentile_filter.h",
     "numerics/running_statistics.h",
     "numerics/sequence_number_util.h",
@@ -1682,7 +1682,7 @@
         "numerics/event_based_exponential_moving_average_unittest.cc",
         "numerics/exp_filter_unittest.cc",
         "numerics/moving_average_unittest.cc",
-        "numerics/moving_median_filter_unittest.cc",
+        "numerics/moving_percentile_filter_unittest.cc",
         "numerics/percentile_filter_unittest.cc",
         "numerics/running_statistics_unittest.cc",
         "numerics/sequence_number_util_unittest.cc",
diff --git a/rtc_base/numerics/moving_median_filter.h b/rtc_base/numerics/moving_median_filter.h
deleted file mode 100644
index 2a8ea7d..0000000
--- a/rtc_base/numerics/moving_median_filter.h
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- *  Copyright (c) 2017 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 RTC_BASE_NUMERICS_MOVING_MEDIAN_FILTER_H_
-#define RTC_BASE_NUMERICS_MOVING_MEDIAN_FILTER_H_
-
-#include <stddef.h>
-
-#include <cstddef>
-#include <list>
-
-#include "rtc_base/checks.h"
-#include "rtc_base/numerics/percentile_filter.h"
-
-namespace webrtc {
-
-// Class to efficiently get moving median filter from a stream of samples.
-template <typename T>
-class MovingMedianFilter {
- public:
-  // Construct filter. `window_size` is how many latest samples are stored and
-  // used to take median. `window_size` must be positive.
-  explicit MovingMedianFilter(size_t window_size);
-
-  MovingMedianFilter(const MovingMedianFilter&) = delete;
-  MovingMedianFilter& operator=(const MovingMedianFilter&) = delete;
-
-  // Insert a new sample.
-  void Insert(const T& value);
-
-  // Removes all samples;
-  void Reset();
-
-  // Get median over the latest window.
-  T GetFilteredValue() const;
-
-  // The number of samples that are currently stored.
-  size_t GetNumberOfSamplesStored() const;
-
- private:
-  PercentileFilter<T> percentile_filter_;
-  std::list<T> samples_;
-  size_t samples_stored_;
-  const size_t window_size_;
-};
-
-template <typename T>
-MovingMedianFilter<T>::MovingMedianFilter(size_t window_size)
-    : percentile_filter_(0.5f), samples_stored_(0), window_size_(window_size) {
-  RTC_CHECK_GT(window_size, 0);
-}
-
-template <typename T>
-void MovingMedianFilter<T>::Insert(const T& value) {
-  percentile_filter_.Insert(value);
-  samples_.emplace_back(value);
-  ++samples_stored_;
-  if (samples_stored_ > window_size_) {
-    percentile_filter_.Erase(samples_.front());
-    samples_.pop_front();
-    --samples_stored_;
-  }
-}
-
-template <typename T>
-T MovingMedianFilter<T>::GetFilteredValue() const {
-  return percentile_filter_.GetPercentileValue();
-}
-
-template <typename T>
-void MovingMedianFilter<T>::Reset() {
-  percentile_filter_.Reset();
-  samples_.clear();
-  samples_stored_ = 0;
-}
-
-template <typename T>
-size_t MovingMedianFilter<T>::GetNumberOfSamplesStored() const {
-  return samples_stored_;
-}
-
-}  // namespace webrtc
-#endif  // RTC_BASE_NUMERICS_MOVING_MEDIAN_FILTER_H_
diff --git a/rtc_base/numerics/moving_percentile_filter.h b/rtc_base/numerics/moving_percentile_filter.h
new file mode 100644
index 0000000..d68814a
--- /dev/null
+++ b/rtc_base/numerics/moving_percentile_filter.h
@@ -0,0 +1,103 @@
+/*
+ *  Copyright (c) 2017 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 RTC_BASE_NUMERICS_MOVING_PERCENTILE_FILTER_H_
+#define RTC_BASE_NUMERICS_MOVING_PERCENTILE_FILTER_H_
+
+#include <stddef.h>
+
+#include <cstddef>
+#include <list>
+
+#include "rtc_base/checks.h"
+#include "rtc_base/numerics/percentile_filter.h"
+
+namespace webrtc {
+
+// Class to efficiently get moving percentile filter from a stream of samples.
+template <typename T>
+class MovingPercentileFilter {
+ public:
+  // Construct filter. `percentile` defines what percentile to track and
+  // `window_size` is how many latest samples are stored for finding the
+  // percentile. `percentile` must be between 0.0 and 1.0 (inclusive) and
+  // `window_size` must be greater than 0.
+  MovingPercentileFilter(float percentile, size_t window_size);
+
+  MovingPercentileFilter(const MovingPercentileFilter&) = delete;
+  MovingPercentileFilter& operator=(const MovingPercentileFilter&) = delete;
+
+  // Insert a new sample.
+  void Insert(const T& value);
+
+  // Removes all samples;
+  void Reset();
+
+  // Get percentile over the latest window.
+  T GetFilteredValue() const;
+
+  // The number of samples that are currently stored.
+  size_t GetNumberOfSamplesStored() const;
+
+ private:
+  PercentileFilter<T> percentile_filter_;
+  std::list<T> samples_;
+  size_t samples_stored_;
+  const size_t window_size_;
+};
+
+// Convenience type for the common median case.
+template <typename T>
+class MovingMedianFilter : public MovingPercentileFilter<T> {
+ public:
+  explicit MovingMedianFilter(size_t window_size)
+      : MovingPercentileFilter<T>(0.5f, window_size) {}
+};
+
+template <typename T>
+MovingPercentileFilter<T>::MovingPercentileFilter(float percentile,
+                                                  size_t window_size)
+    : percentile_filter_(percentile),
+      samples_stored_(0),
+      window_size_(window_size) {
+  RTC_CHECK_GT(window_size, 0);
+}
+
+template <typename T>
+void MovingPercentileFilter<T>::Insert(const T& value) {
+  percentile_filter_.Insert(value);
+  samples_.emplace_back(value);
+  ++samples_stored_;
+  if (samples_stored_ > window_size_) {
+    percentile_filter_.Erase(samples_.front());
+    samples_.pop_front();
+    --samples_stored_;
+  }
+}
+
+template <typename T>
+T MovingPercentileFilter<T>::GetFilteredValue() const {
+  return percentile_filter_.GetPercentileValue();
+}
+
+template <typename T>
+void MovingPercentileFilter<T>::Reset() {
+  percentile_filter_.Reset();
+  samples_.clear();
+  samples_stored_ = 0;
+}
+
+template <typename T>
+size_t MovingPercentileFilter<T>::GetNumberOfSamplesStored() const {
+  return samples_stored_;
+}
+
+}  // namespace webrtc
+#endif  // RTC_BASE_NUMERICS_MOVING_PERCENTILE_FILTER_H_
diff --git a/rtc_base/numerics/moving_median_filter_unittest.cc b/rtc_base/numerics/moving_percentile_filter_unittest.cc
similarity index 61%
rename from rtc_base/numerics/moving_median_filter_unittest.cc
rename to rtc_base/numerics/moving_percentile_filter_unittest.cc
index 12c1114..30c0ebb 100644
--- a/rtc_base/numerics/moving_median_filter_unittest.cc
+++ b/rtc_base/numerics/moving_percentile_filter_unittest.cc
@@ -8,15 +8,42 @@
  *  be found in the AUTHORS file in the root of the source tree.
  */
 
-#include "rtc_base/numerics/moving_median_filter.h"
+#include "rtc_base/numerics/moving_percentile_filter.h"
 
 #include <stdint.h>
+
 #include <algorithm>
 
 #include "test/gtest.h"
 
 namespace webrtc {
 
+// 25th percentile can be exactly found with a window of length 4.
+TEST(MovingPercentileFilter, Percentile25ReturnsMovingPercentile25WithWindow4) {
+  MovingPercentileFilter<int> perc25(0.25f, 4);
+  const int64_t kSamples[10] = {1, 2, 3, 4, 4, 4, 5, 6, 7, 8};
+  const int64_t kExpectedFilteredValues[10] = {1, 1, 1, 1, 2, 3, 4, 4, 4, 5};
+  for (size_t i = 0; i < 10; ++i) {
+    perc25.Insert(kSamples[i]);
+    EXPECT_EQ(kExpectedFilteredValues[i], perc25.GetFilteredValue());
+    EXPECT_EQ(std::min<size_t>(i + 1, 4), perc25.GetNumberOfSamplesStored());
+  }
+}
+
+// 90th percentile becomes the 67th percentile with a window of length 4.
+TEST(MovingPercentileFilter, Percentile90ReturnsMovingPercentile67WithWindow4) {
+  MovingPercentileFilter<int> perc67(0.67f, 4);
+  MovingPercentileFilter<int> perc90(0.9f, 4);
+  const int64_t kSamples[8] = {1, 10, 1, 9, 1, 10, 1, 8};
+  const int64_t kExpectedFilteredValues[9] = {1, 1, 1, 9, 9, 9, 9, 8};
+  for (size_t i = 0; i < 8; ++i) {
+    perc67.Insert(kSamples[i]);
+    perc90.Insert(kSamples[i]);
+    EXPECT_EQ(kExpectedFilteredValues[i], perc67.GetFilteredValue());
+    EXPECT_EQ(kExpectedFilteredValues[i], perc90.GetFilteredValue());
+  }
+}
+
 TEST(MovingMedianFilterTest, ProcessesNoSamples) {
   MovingMedianFilter<int> filter(2);
   EXPECT_EQ(0, filter.GetFilteredValue());