JitterEstimator: add field trial for not estimating noise for congested frames
The reason for rejecting the congested frames in the first place, is
that they do not obey a Gaussian distribution around the line from the
Kalman filter. It therefore also does not make sense to include them
in the noise (*) estimation.
(*) noise = variation around the line from the Kalman filter.
Bug: webrtc:14151
Change-Id: Id8a44ba5f13bf9787ab54848109430ef7657f67a
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/275762
Commit-Queue: Rasmus Brandt <brandtr@webrtc.org>
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38100}
diff --git a/modules/video_coding/timing/jitter_estimator.cc b/modules/video_coding/timing/jitter_estimator.cc
index 22fdcac..2416ed0 100644
--- a/modules/video_coding/timing/jitter_estimator.cc
+++ b/modules/video_coding/timing/jitter_estimator.cc
@@ -263,9 +263,6 @@
// the frame size also is large the deviation is probably due to an incorrect
// line slope.
if (abs_delay_is_not_outlier || size_is_positive_outlier) {
- // Update the variance of the deviation from the line given by the Kalman
- // filter.
- EstimateRandomJitter(delay_deviation_ms);
// Prevent updating with frames which have been congested by a large frame,
// and therefore arrives almost at the same time as that frame.
// This can occur when we receive a large frame (key frame) which has been
@@ -276,14 +273,25 @@
config_.MaxFrameSizePercentileEnabled()
? max_frame_size_bytes_percentile_.GetFilteredValue()
: max_frame_size_bytes_;
- if (delta_frame_bytes >
- GetCongestionRejectionFactor() * filtered_max_frame_size_bytes) {
- // Update the Kalman filter with the new data
+ bool is_not_congested =
+ delta_frame_bytes >
+ GetCongestionRejectionFactor() * filtered_max_frame_size_bytes;
+
+ if (is_not_congested || config_.estimate_noise_when_congested) {
+ // Update the variance of the deviation from the line given by the Kalman
+ // filter.
+ EstimateRandomJitter(delay_deviation_ms);
+ }
+ if (is_not_congested) {
+ // Neither a delay outlier nor a congested frame, so we can safely update
+ // the Kalman filter with the sample.
kalman_filter_.PredictAndUpdate(frame_delay.ms(), delta_frame_bytes,
filtered_max_frame_size_bytes,
var_noise_ms2_);
}
} else {
+ // Delay outliers affect the noise estimate through a value equal to the
+ // outlier rejection threshold.
double num_stddev = (delay_deviation_ms >= 0) ? num_stddev_delay_outlier
: -num_stddev_delay_outlier;
EstimateRandomJitter(num_stddev * sqrt(var_noise_ms2_));
diff --git a/modules/video_coding/timing/jitter_estimator.h b/modules/video_coding/timing/jitter_estimator.h
index c615be1..769bb12 100644
--- a/modules/video_coding/timing/jitter_estimator.h
+++ b/modules/video_coding/timing/jitter_estimator.h
@@ -51,7 +51,8 @@
"num_stddev_delay_clamp", &num_stddev_delay_clamp,
"num_stddev_delay_outlier", &num_stddev_delay_outlier,
"num_stddev_size_outlier", &num_stddev_size_outlier,
- "congestion_rejection_factor", &congestion_rejection_factor);
+ "congestion_rejection_factor", &congestion_rejection_factor,
+ "estimate_noise_when_congested", &estimate_noise_when_congested);
// clang-format on
}
@@ -59,7 +60,7 @@
return max_frame_size_percentile.has_value();
}
- // If set, the "avg" frame size is calculated as the median over a window
+ // If true, the "avg" frame size is calculated as the median over a window
// of recent frame sizes.
bool avg_frame_size_median = false;
@@ -96,6 +97,12 @@
//
// Decreasing this value rejects fewer samples.
absl::optional<double> congestion_rejection_factor = absl::nullopt;
+
+ // If true, the noise estimate will be updated for congestion rejected
+ // frames. This is currently enabled by default, but that may not be optimal
+ // since congested frames typically are not spread around the line with
+ // Gaussian noise. (This is the whole reason for the congestion rejection!)
+ bool estimate_noise_when_congested = true;
};
JitterEstimator(Clock* clock, const FieldTrialsView& field_trials);
diff --git a/modules/video_coding/timing/jitter_estimator_unittest.cc b/modules/video_coding/timing/jitter_estimator_unittest.cc
index 2602b0d..8e0c015 100644
--- a/modules/video_coding/timing/jitter_estimator_unittest.cc
+++ b/modules/video_coding/timing/jitter_estimator_unittest.cc
@@ -161,6 +161,24 @@
EXPECT_GT(outlier_jitter.ms(), steady_state_jitter.ms());
}
+// Under the default config, congested frames are used when calculating the
+// noise variance, meaning that they will impact the final jitter estimate.
+TEST_F(JitterEstimatorTest, CongestedFrameImpactsJitterEstimate) {
+ ValueGenerator gen(10);
+
+ // Steady state.
+ Run(/*duration_s=*/10, /*framerate_fps=*/30, gen);
+ TimeDelta steady_state_jitter =
+ estimator_.GetJitterEstimate(0, absl::nullopt);
+
+ // Congested frame...
+ estimator_.UpdateEstimate(-10 * gen.Delay(), 0.1 * gen.FrameSize());
+ TimeDelta outlier_jitter = estimator_.GetJitterEstimate(0, absl::nullopt);
+
+ // ...impacts the estimate.
+ EXPECT_GT(outlier_jitter.ms(), steady_state_jitter.ms());
+}
+
TEST_F(JitterEstimatorTest, EmptyFieldTrialsParsesToUnsetConfig) {
JitterEstimator::Config config = estimator_.GetConfigForTest();
EXPECT_FALSE(config.avg_frame_size_median);
@@ -170,6 +188,7 @@
EXPECT_FALSE(config.num_stddev_delay_outlier.has_value());
EXPECT_FALSE(config.num_stddev_size_outlier.has_value());
EXPECT_FALSE(config.congestion_rejection_factor.has_value());
+ EXPECT_TRUE(config.estimate_noise_when_congested);
}
class FieldTrialsOverriddenJitterEstimatorTest : public JitterEstimatorTest {
@@ -183,7 +202,8 @@
"num_stddev_delay_clamp:1.1,"
"num_stddev_delay_outlier:2,"
"num_stddev_size_outlier:3.1,"
- "congestion_rejection_factor:-1.55/") {}
+ "congestion_rejection_factor:-1.55,"
+ "estimate_noise_when_congested:false/") {}
~FieldTrialsOverriddenJitterEstimatorTest() {}
};
@@ -196,6 +216,7 @@
EXPECT_EQ(*config.num_stddev_delay_outlier, 2.0);
EXPECT_EQ(*config.num_stddev_size_outlier, 3.1);
EXPECT_EQ(*config.congestion_rejection_factor, -1.55);
+ EXPECT_FALSE(config.estimate_noise_when_congested);
}
TEST_F(FieldTrialsOverriddenJitterEstimatorTest,
@@ -239,6 +260,25 @@
EXPECT_GT(outlier_jitter_4x.ms(), outlier_jitter_3x.ms());
}
+// When so configured, congested frames are NOT used when calculating the
+// noise variance, meaning that they will NOT impact the final jitter estimate.
+TEST_F(FieldTrialsOverriddenJitterEstimatorTest,
+ CongestedFrameDoesNotImpactJitterEstimate) {
+ ValueGenerator gen(10);
+
+ // Steady state.
+ Run(/*duration_s=*/10, /*framerate_fps=*/30, gen);
+ TimeDelta steady_state_jitter =
+ estimator_.GetJitterEstimate(0, absl::nullopt);
+
+ // Congested frame...
+ estimator_.UpdateEstimate(-10 * gen.Delay(), 0.1 * gen.FrameSize());
+ TimeDelta outlier_jitter = estimator_.GetJitterEstimate(0, absl::nullopt);
+
+ // ...does not impact the estimate.
+ EXPECT_EQ(outlier_jitter.ms(), steady_state_jitter.ms());
+}
+
class MisconfiguredFieldTrialsJitterEstimatorTest : public JitterEstimatorTest {
protected:
MisconfiguredFieldTrialsJitterEstimatorTest()