commit e1a8fd0ff8ed27f57d1b280dba50cd0479a0bc33
parent cce0baff53452f64206424f9533fff2acc03fa24
Author: Michael Froman <mfroman@mozilla.com>
Date: Wed, 8 Oct 2025 18:44:16 -0500
Bug 1993083 - Vendor libwebrtc from 6eae1ac476
Upstream commit: https://webrtc.googlesource.com/src/+/6eae1ac476b62f1f83def66dbb5cab1bbb568aa2
Add hard outlier rejection to `TimestampExtrapolator`
This change adds field trials for a hard outlier rejection mode. It also
adds a hard reset on RTP timestamp jumps, which might be required in the
case of remote clock source replacements (which otherwise would be
detected as outliers only).
Bug: b/422493053, b/428657776
Change-Id: I219daf7cc2550d710a2bee453f6467700ff0e340
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/398740
Reviewed-by: Åsa Persson <asapersson@webrtc.org>
Commit-Queue: Rasmus Brandt <brandtr@google.com>
Cr-Commit-Position: refs/heads/main@{#45076}
Diffstat:
5 files changed, 393 insertions(+), 21 deletions(-)
diff --git a/third_party/libwebrtc/README.mozilla.last-vendor b/third_party/libwebrtc/README.mozilla.last-vendor
@@ -1,4 +1,4 @@
# ./mach python dom/media/webrtc/third_party_build/vendor-libwebrtc.py --from-local /home/mfroman/mozilla/elm/.moz-fast-forward/moz-libwebrtc --commit mozpatches libwebrtc
-libwebrtc updated from /home/mfroman/mozilla/elm/.moz-fast-forward/moz-libwebrtc commit mozpatches on 2025-10-08T23:43:01.892200+00:00.
+libwebrtc updated from /home/mfroman/mozilla/elm/.moz-fast-forward/moz-libwebrtc commit mozpatches on 2025-10-08T23:44:06.475027+00:00.
# base of lastest vendoring
-8fc86b7e46
+6eae1ac476
diff --git a/third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator.cc b/third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator.cc
@@ -11,6 +11,7 @@
#include "modules/video_coding/timing/timestamp_extrapolator.h"
#include <algorithm>
+#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <optional>
@@ -32,6 +33,7 @@ constexpr double kLambda = 1;
constexpr int kStartUpFilterDelayInPackets = 2;
constexpr double kP00 = 1.0;
constexpr double kP11 = 1e10;
+constexpr double kStartResidualVariance = 3000 * 3000;
} // namespace
@@ -48,6 +50,39 @@ TimestampExtrapolator::Config TimestampExtrapolator::Config::ParseAndValidate(
<< config.hard_reset_timeout;
config.hard_reset_timeout = defaults.hard_reset_timeout;
}
+ if (config.hard_reset_rtp_timestamp_jump_threshold <= 0) {
+ RTC_LOG(LS_WARNING)
+ << "Skipping invalid hard_reset_rtp_timestamp_jump_threshold="
+ << config.hard_reset_rtp_timestamp_jump_threshold;
+ config.hard_reset_rtp_timestamp_jump_threshold =
+ defaults.hard_reset_rtp_timestamp_jump_threshold;
+ }
+ if (config.outlier_rejection_startup_delay < 0) {
+ RTC_LOG(LS_WARNING) << "Skipping invalid outlier_rejection_startup_delay="
+ << config.outlier_rejection_startup_delay;
+ config.outlier_rejection_startup_delay =
+ defaults.outlier_rejection_startup_delay;
+ }
+ if (config.outlier_rejection_max_consecutive <= 0) {
+ RTC_LOG(LS_WARNING) << "Skipping invalid outlier_rejection_max_consecutive="
+ << config.outlier_rejection_max_consecutive;
+ config.outlier_rejection_max_consecutive =
+ defaults.outlier_rejection_max_consecutive;
+ }
+ if (config.outlier_rejection_forgetting_factor < 0 ||
+ config.outlier_rejection_forgetting_factor >= 1) {
+ RTC_LOG(LS_WARNING)
+ << "Skipping invalid outlier_rejection_forgetting_factor="
+ << config.outlier_rejection_forgetting_factor;
+ config.outlier_rejection_forgetting_factor =
+ defaults.outlier_rejection_forgetting_factor;
+ }
+ if (config.outlier_rejection_stddev.has_value() &&
+ *config.outlier_rejection_stddev <= 0) {
+ RTC_LOG(LS_WARNING) << "Skipping invalid outlier_rejection_stddev="
+ << *config.outlier_rejection_stddev;
+ config.outlier_rejection_stddev = defaults.outlier_rejection_stddev;
+ }
if (config.alarm_threshold <= 0) {
RTC_LOG(LS_WARNING) << "Skipping invalid alarm_threshold="
<< config.alarm_threshold;
@@ -73,6 +108,9 @@ TimestampExtrapolator::TimestampExtrapolator(
start_(Timestamp::Zero()),
prev_(Timestamp::Zero()),
packet_count_(0),
+ residual_mean_(0),
+ residual_variance_(kStartResidualVariance),
+ outliers_consecutive_count_(0),
detector_accumulator_pos_(0),
detector_accumulator_neg_(0) {
Reset(start);
@@ -99,24 +137,48 @@ void TimestampExtrapolator::Reset(Timestamp start) {
p_[0][1] = p_[1][0] = 0;
unwrapper_ = RtpTimestampUnwrapper();
packet_count_ = 0;
+ // Hard outlier rejection.
+ residual_mean_ = 0.0;
+ residual_variance_ = kStartResidualVariance;
+ outliers_consecutive_count_ = 0;
+ // Soft outlier attenuation.
detector_accumulator_pos_ = 0;
detector_accumulator_neg_ = 0;
}
void TimestampExtrapolator::Update(Timestamp now, uint32_t ts90khz) {
+ // Hard reset based local clock timeouts.
if (now - prev_ > config_.hard_reset_timeout) {
- // No complete frame within the timeout.
Reset(now);
} else {
prev_ = now;
}
+ const int64_t unwrapped_ts90khz = unwrapper_.Unwrap(ts90khz);
+
+ // Hard reset based on large RTP timestamp jumps. This is only enabled if
+ // outlier rejection is enabled, since that feature would by itself
+ // consistently block any long-term static offset changes due to, e.g.,
+ // remote clock source replacements.
+ if (config_.OutlierRejectionEnabled() && prev_unwrapped_timestamp_) {
+ // Predict the expected RTP timestamp change based on elapsed wall clock
+ // time.
+ int64_t expected_rtp_diff =
+ static_cast<int64_t>((now - prev_).ms() * w_[0]);
+ int64_t actual_rtp_diff = unwrapped_ts90khz - *prev_unwrapped_timestamp_;
+ int64_t rtp_jump = actual_rtp_diff - expected_rtp_diff;
+ if (std::abs(rtp_jump) > config_.hard_reset_rtp_timestamp_jump_threshold) {
+ RTC_LOG(LS_WARNING) << "Large jump in RTP timestamp detected. "
+ "Difference between actual and expected change: "
+ << rtp_jump << " ticks. Resetting filter.";
+ Reset(now);
+ }
+ }
+
// Remove offset to prevent badly scaled matrices
const TimeDelta offset = now - start_;
double t_ms = offset.ms();
- int64_t unwrapped_ts90khz = unwrapper_.Unwrap(ts90khz);
-
if (!first_unwrapped_timestamp_) {
// Make an initial guess of the offset,
// should be almost correct since t_ms - start
@@ -128,23 +190,46 @@ void TimestampExtrapolator::Update(Timestamp now, uint32_t ts90khz) {
double residual =
(static_cast<double>(unwrapped_ts90khz) - *first_unwrapped_timestamp_) -
t_ms * w_[0] - w_[1];
+
+ // Hard outlier rejection: reject outliers and avoid updating the filter state
+ // for frames whose residuals are too large.
+ if (config_.OutlierRejectionEnabled() && OutlierDetection(residual)) {
+ ++outliers_consecutive_count_;
+ if (outliers_consecutive_count_ <=
+ config_.outlier_rejection_max_consecutive) {
+ // This appears to be a transient spike. Reject it.
+ return;
+ } else {
+ // This appears to be a persistent delay change. Force the filter to
+ // adapt.
+ SoftReset();
+ }
+ }
+ // Frame is an inlier, or we have reached `outlier_rejection_max_consecutive`.
+ outliers_consecutive_count_ = 0;
+
+ // Soft outlier attenuation: boost the filter's uncertainty if the integrated
+ // delay has changed too much.
+ // TODO(brandtr): Move the packet count check into DelayChangeDetection.
if (DelayChangeDetection(residual) &&
packet_count_ >= kStartUpFilterDelayInPackets) {
// Force the filter to adjust its offset parameter by changing
// the uncertainties. Don't do this during startup.
- if (config_.reset_full_cov_on_alarm) {
- p_[0][0] = kP00;
- p_[0][1] = p_[1][0] = 0;
- }
- p_[1][1] = kP11;
+ SoftReset();
}
- if (prev_unwrapped_timestamp_ &&
- unwrapped_ts90khz < prev_unwrapped_timestamp_) {
+ // If hard outlier rejection is enabled, we handle large RTP timestamp jumps
+ // above.
+ if (!config_.OutlierRejectionEnabled() &&
+ (prev_unwrapped_timestamp_ &&
+ unwrapped_ts90khz < prev_unwrapped_timestamp_)) {
// Drop reordered frames.
return;
}
+ // Update recursive least squares filter.
+ // TODO(b/428657776): Document better.
+
// T = [t(k) 1]';
// that = T'*w;
// K = P*T/(lambda + T'*P*T);
@@ -210,15 +295,55 @@ std::optional<Timestamp> TimestampExtrapolator::ExtrapolateLocalTime(
return start_ + diff;
}
-bool TimestampExtrapolator::DelayChangeDetection(double error) {
+void TimestampExtrapolator::SoftReset() {
+ if (config_.reset_full_cov_on_alarm) {
+ p_[0][0] = kP00;
+ p_[0][1] = p_[1][0] = 0;
+ }
+ p_[1][1] = kP11;
+}
+
+bool TimestampExtrapolator::OutlierDetection(double residual) {
+ if (!config_.outlier_rejection_stddev.has_value()) {
+ return false;
+ }
+
+ if (packet_count_ >= config_.outlier_rejection_startup_delay) {
+ double threshold =
+ *config_.outlier_rejection_stddev * std::sqrt(residual_variance_);
+ // Outlier frames trigger the alarm.
+ // We intentionally use a symmetric detection here, meaning that
+ // significantly early frames are also alarmed on. The main reason is to
+ // ensure a symmetric update to the running statistics below.
+ if (std::abs(residual - residual_mean_) > threshold) {
+ // Alarm.
+ return true;
+ }
+ }
+
+ // Update residual statistics only with inliers.
+ double forgetting_factor = config_.outlier_rejection_forgetting_factor;
+ residual_mean_ =
+ forgetting_factor * residual_mean_ + (1.0 - forgetting_factor) * residual;
+ double residual_deviation = residual - residual_mean_;
+ double squared_residual_deviation = residual_deviation * residual_deviation;
+ residual_variance_ =
+ std::max(forgetting_factor * residual_variance_ +
+ (1.0 - forgetting_factor) * squared_residual_deviation,
+ 1.0);
+
+ return false;
+}
+
+bool TimestampExtrapolator::DelayChangeDetection(double residual) {
// CUSUM detection of sudden delay changes
double acc_max_error = static_cast<double>(config_.acc_max_error);
- error = (error > 0) ? std::min(error, acc_max_error)
- : std::max(error, -acc_max_error);
+ residual = (residual > 0) ? std::min(residual, acc_max_error)
+ : std::max(residual, -acc_max_error);
detector_accumulator_pos_ = std::max(
- detector_accumulator_pos_ + error - config_.acc_drift, double{0});
+ detector_accumulator_pos_ + residual - config_.acc_drift, double{0});
detector_accumulator_neg_ = std::min(
- detector_accumulator_neg_ + error + config_.acc_drift, double{0});
+ detector_accumulator_neg_ + residual + config_.acc_drift, double{0});
if (detector_accumulator_pos_ > config_.alarm_threshold ||
detector_accumulator_neg_ < -config_.alarm_threshold) {
// Alarm
diff --git a/third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator.h b/third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator.h
@@ -25,6 +25,22 @@
namespace webrtc {
+// The `TimestampExtrapolator` is an adaptive filter that estimates the
+// local clock time of incoming RTP timestamps. It's main purpose is to handle
+// clock drift and clock offset, not to model network behaviour.
+//
+// The mechanisms applied for this are:
+// * Recursive least-squares filter for estimating clock skew and clock offset.
+// * Hard reset on wall clock timeout.
+// * Hard reset on large incoming RTP timestamp jumps.
+// * Hard outlier rejection based on the difference between the predicted and
+// the actual wall clock time for an RTP timestamp.
+// * Soft outlier attenuation based on the integrated (CUSUM style) difference
+// between predicted and actual wall clock time for an RTP timestamp.
+//
+// Not all of the mechanisms are enabled by default. Use the field trial string
+// to experiment with different settings.
+//
// Not thread safe.
class TimestampExtrapolator {
public:
@@ -42,6 +58,13 @@ class TimestampExtrapolator {
// clang-format off
return StructParametersParser::Create(
"hard_reset_timeout", &hard_reset_timeout,
+ "hard_reset_rtp_timestamp_jump_threshold",
+ &hard_reset_rtp_timestamp_jump_threshold,
+ "outlier_rejection_startup_delay", &outlier_rejection_startup_delay,
+ "outlier_rejection_max_consecutive", &outlier_rejection_max_consecutive,
+ "outlier_rejection_forgetting_factor",
+ &outlier_rejection_forgetting_factor,
+ "outlier_rejection_stddev", &outlier_rejection_stddev,
"alarm_threshold", &alarm_threshold,
"acc_drift", &acc_drift,
"acc_max_error", &acc_max_error,
@@ -49,9 +72,44 @@ class TimestampExtrapolator {
// clang-format on
}
+ bool OutlierRejectionEnabled() const {
+ return outlier_rejection_stddev.has_value();
+ }
+
+ // -- Hard reset behaviour --
+
// If a frame has not been received within this timeout, do a full reset.
TimeDelta hard_reset_timeout = TimeDelta::Seconds(10);
+ // A jump in the RTP timestamp of this magnitude, not accounted for by the
+ // passage of time, is considered a source clock replacement and will
+ // trigger a filter reset. 900000 ticks = 10 seconds.
+ // (Only enabled if hard outlier rejection is enabled.)
+ int hard_reset_rtp_timestamp_jump_threshold = 900'000;
+
+ // -- Hard outlier rejection --
+
+ // Number of frames to wait before starting to update the residual
+ // statistics. 300 frames = 10 seconds@30fps.
+ int outlier_rejection_startup_delay = 300;
+
+ // Number of consecutive frames that are allowed to be treated as outliers.
+ // If more frames than these are outliers, hard outlier rejection stops
+ // and soft outlier attentuation starts.
+ // 150 frames = 5 seconds@30fps.
+ int outlier_rejection_max_consecutive = 150;
+
+ // Smoothing factor for the residual statistics.
+ // Half-life is log(0.5)/log(0.999) ~= 693 frames ~= 23 seconds@30fps.
+ double outlier_rejection_forgetting_factor = 0.999;
+
+ // If set, will reject outliers based on this number of standard deviations
+ // of the filtered residuals.
+ // Setting this field to non-nullopt enables hard outlier rejection.
+ std::optional<double> outlier_rejection_stddev = std::nullopt;
+
+ // -- Soft outlier attenuation --
+
// Alarm on sudden delay change if the (filtered; see below) accumulated
// residuals are larger than this number of RTP ticks. After the
// startup period, an alarm will result in a full or partial reset of the
@@ -74,15 +132,21 @@ class TimestampExtrapolator {
TimestampExtrapolator(Timestamp start, const FieldTrialsView& field_trials);
~TimestampExtrapolator();
+
+ // Update the filter with a new incoming local timestamp/RTP timestamp pair.
void Update(Timestamp now, uint32_t ts90khz);
+
+ // Return the expected local timestamp for an RTP timestamp.
std::optional<Timestamp> ExtrapolateLocalTime(uint32_t timestamp90khz) const;
+
void Reset(Timestamp start);
Config GetConfigForTest() const { return config_; }
private:
- void CheckForWrapArounds(uint32_t ts90khz);
- bool DelayChangeDetection(double error);
+ void SoftReset();
+ bool OutlierDetection(double residual);
+ bool DelayChangeDetection(double residual);
const Config config_;
@@ -94,6 +158,13 @@ class TimestampExtrapolator {
RtpTimestampUnwrapper unwrapper_;
std::optional<int64_t> prev_unwrapped_timestamp_;
int packet_count_;
+
+ // Running residual statistics for the hard outlier rejection.
+ double residual_mean_;
+ double residual_variance_;
+ int outliers_consecutive_count_;
+
+ // Integrated residual statistics for the soft outlier attenuation.
double detector_accumulator_pos_;
double detector_accumulator_neg_;
};
diff --git a/third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator_unittest.cc b/third_party/libwebrtc/modules/video_coding/timing/timestamp_extrapolator_unittest.cc
@@ -340,6 +340,11 @@ TEST(TimestampExtrapolatorTest, SetsValidConfig) {
clock.CurrentTime(), CreateTestFieldTrials(
"WebRTC-TimestampExtrapolatorConfig/"
"hard_reset_timeout:1s,"
+ "hard_reset_rtp_timestamp_jump_threshold:45000,"
+ "outlier_rejection_startup_delay:123,"
+ "outlier_rejection_max_consecutive:456,"
+ "outlier_rejection_forgetting_factor:0.987,"
+ "outlier_rejection_stddev:3.5,"
"alarm_threshold:123,"
"acc_drift:456,"
"acc_max_error:789,"
@@ -347,7 +352,13 @@ TEST(TimestampExtrapolatorTest, SetsValidConfig) {
// clang-format on
TimestampExtrapolator::Config config = ts_extrapolator.GetConfigForTest();
+ EXPECT_TRUE(config.OutlierRejectionEnabled());
EXPECT_EQ(config.hard_reset_timeout, TimeDelta::Seconds(1));
+ EXPECT_EQ(config.hard_reset_rtp_timestamp_jump_threshold, 45000);
+ EXPECT_EQ(config.outlier_rejection_startup_delay, 123);
+ EXPECT_EQ(config.outlier_rejection_max_consecutive, 456);
+ EXPECT_EQ(config.outlier_rejection_forgetting_factor, 0.987);
+ EXPECT_EQ(config.outlier_rejection_stddev, 3.5);
EXPECT_EQ(config.alarm_threshold, 123);
EXPECT_EQ(config.acc_drift, 456);
EXPECT_EQ(config.acc_max_error, 789);
@@ -361,16 +372,181 @@ TEST(TimestampExtrapolatorTest, DoesNotSetInvalidConfig) {
clock.CurrentTime(), CreateTestFieldTrials(
"WebRTC-TimestampExtrapolatorConfig/"
"hard_reset_timeout:-1s,"
+ "hard_reset_rtp_timestamp_jump_threshold:-1,"
+ "outlier_rejection_startup_delay:-1,"
+ "outlier_rejection_max_consecutive:0,"
+ "outlier_rejection_forgetting_factor:1.1,"
+ "outlier_rejection_stddev:-1,"
"alarm_threshold:-123,"
"acc_drift:-456,"
"acc_max_error:-789/"));
// clang-format on
TimestampExtrapolator::Config config = ts_extrapolator.GetConfigForTest();
+ EXPECT_FALSE(config.OutlierRejectionEnabled());
EXPECT_EQ(config.hard_reset_timeout, TimeDelta::Seconds(10));
+ EXPECT_EQ(config.hard_reset_rtp_timestamp_jump_threshold, 900000);
+ EXPECT_EQ(config.outlier_rejection_startup_delay, 300);
+ EXPECT_EQ(config.outlier_rejection_max_consecutive, 150);
+ EXPECT_EQ(config.outlier_rejection_forgetting_factor, 0.999);
+ EXPECT_FALSE(config.outlier_rejection_stddev.has_value());
EXPECT_EQ(config.alarm_threshold, 60000);
EXPECT_EQ(config.acc_drift, 6600);
EXPECT_EQ(config.acc_max_error, 7000);
}
+TEST(TimestampExtrapolatorTest, ExtrapolationNotAffectedByRtpTimestampJump) {
+ SimulatedClock clock(Timestamp::Millis(1337));
+ TimestampExtrapolator extrapolator(
+ clock.CurrentTime(),
+ CreateTestFieldTrials("WebRTC-TimestampExtrapolatorConfig/"
+ "outlier_rejection_stddev:3,hard_reset_rtp_"
+ "timestamp_jump_threshold:900000/"));
+
+ // Stabilize filter.
+ uint32_t rtp = 0;
+ for (int i = 0; i < 2000; ++i) {
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ }
+
+ // Last frame before jump is expected on time.
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+
+ // Next frame arrives on time, but with a 20 second RTP timestamp jump.
+ rtp += 2 * 900000; // 20 seconds.
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+
+ // First frame after jump is expected on time.
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+}
+
+TEST(TimestampExtrapolatorTest, ExtrapolationNotAffectedByFrameOutliers) {
+ SimulatedClock clock(Timestamp::Millis(1337));
+ TimestampExtrapolator extrapolator(
+ clock.CurrentTime(),
+ CreateTestFieldTrials(
+ "WebRTC-TimestampExtrapolatorConfig/outlier_rejection_stddev:3/"));
+
+ // Stabilize filter.
+ uint32_t rtp = 0;
+ for (int i = 0; i < 2000; ++i) {
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ }
+
+ // Last frame before outlier arrives on time.
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+
+ // Outlier frame arrives 1000ms late, but is expected on time.
+ rtp += kRtpHz / k25Fps;
+ Timestamp expected = clock.CurrentTime() + k25FpsDelay;
+ clock.AdvanceTime(TimeDelta::Millis(1000));
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), expected);
+
+ // Congested frames arrive back-to-back, but are expected on time.
+ for (int i = 0; i < 24; ++i) {
+ rtp += kRtpHz / k25Fps;
+ expected += k25FpsDelay;
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), expected);
+ }
+
+ // Regular frame after outliers arrives on time.
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+}
+
+TEST(TimestampExtrapolatorTest,
+ ExtrapolationAffectedByFrameOutliersAfterRejectionPeriod) {
+ SimulatedClock clock(Timestamp::Millis(1337));
+ TimestampExtrapolator extrapolator(
+ clock.CurrentTime(),
+ CreateTestFieldTrials(
+ "WebRTC-TimestampExtrapolatorConfig/"
+ "outlier_rejection_stddev:3,outlier_rejection_max_consecutive:20/"));
+
+ // Stabilize filter.
+ uint32_t rtp = 0;
+ for (int i = 0; i < 2000; ++i) {
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ }
+
+ // Last frame before outlier arrives on time.
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+
+ // Outlier frame arrives 1000ms late, but is expected on time.
+ rtp += kRtpHz / k25Fps;
+ Timestamp expected = clock.CurrentTime() + k25FpsDelay;
+ clock.AdvanceTime(TimeDelta::Millis(1000));
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), expected);
+
+ // Congested frames arrive back-to-back. The first 19 are expected on time.
+ for (int i = 0; i < 19; ++i) {
+ rtp += kRtpHz / k25Fps;
+ expected += k25FpsDelay;
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), expected);
+ }
+
+ // After the 20 consecutive outlier frames, the filter soft resets and starts
+ // expecting frames on the new baseline, which is partially congested.
+ rtp += kRtpHz / k25Fps;
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+ for (int i = 0; i < 4; ++i) {
+ rtp += kRtpHz / k25Fps;
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp),
+ clock.CurrentTime() + (i + 1) * k25FpsDelay);
+ }
+
+ // Now we have caught up with realtime, but since the soft reset happened
+ // 4 frames too early, the new baseline is 4 * 1000/25 = 160ms off.
+ for (int i = 0; i < 10; ++i) {
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp),
+ clock.CurrentTime() + 4 * k25FpsDelay);
+ }
+
+ // Let the filter stabilize at a realtime rate again.
+ for (int i = 0; i < 2000; ++i) {
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ }
+
+ // After the stabilization, the 160ms congestion offset has been canceled.
+ for (int i = 0; i < 10; ++i) {
+ rtp += kRtpHz / k25Fps;
+ clock.AdvanceTime(k25FpsDelay);
+ extrapolator.Update(clock.CurrentTime(), rtp);
+ EXPECT_EQ(extrapolator.ExtrapolateLocalTime(rtp), clock.CurrentTime());
+ }
+}
+
} // namespace webrtc
diff --git a/third_party/libwebrtc/moz-patch-stack/s0061.patch b/third_party/libwebrtc/moz-patch-stack/s0061.patch
@@ -10,10 +10,10 @@ Mercurial Revision: https://hg.mozilla.org/mozilla-central/rev/c5df7f40392464ffc
1 file changed, 1 insertion(+)
diff --git a/modules/video_coding/timing/timestamp_extrapolator.cc b/modules/video_coding/timing/timestamp_extrapolator.cc
-index 6f1be747eb..d46e961670 100644
+index 776d984dc2..177a382d6b 100644
--- a/modules/video_coding/timing/timestamp_extrapolator.cc
+++ b/modules/video_coding/timing/timestamp_extrapolator.cc
-@@ -179,6 +179,7 @@ void TimestampExtrapolator::Update(Timestamp now, uint32_t ts90khz) {
+@@ -264,6 +264,7 @@ void TimestampExtrapolator::Update(Timestamp now, uint32_t ts90khz) {
std::optional<Timestamp> TimestampExtrapolator::ExtrapolateLocalTime(
uint32_t timestamp90khz) const {
int64_t unwrapped_ts90khz = unwrapper_.PeekUnwrap(timestamp90khz);