blob: 776d984dc288e589a82e0f32043582ad8b02d401 [file] [log] [blame] [edit]
/*
* Copyright (c) 2011 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 "modules/video_coding/timing/timestamp_extrapolator.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <optional>
#include "api/field_trials_view.h"
#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "rtc_base/logging.h"
#include "rtc_base/numerics/sequence_number_unwrapper.h"
#include "system_wrappers/include/metrics.h"
namespace webrtc {
namespace {
constexpr int kMinimumSamplesToLogEstimatedClockDrift =
3000; // 100 seconds at 30 fps.
constexpr double kLambda = 1;
constexpr int kStartUpFilterDelayInPackets = 2;
constexpr double kP00 = 1.0;
constexpr double kP11 = 1e10;
constexpr double kStartResidualVariance = 3000 * 3000;
} // namespace
TimestampExtrapolator::Config TimestampExtrapolator::Config::ParseAndValidate(
const FieldTrialsView& field_trials) {
// Parse.
Config config;
config.Parser()->Parse(field_trials.Lookup(kFieldTrialsKey));
// Validate.
Config defaults;
if (config.hard_reset_timeout <= TimeDelta::Zero()) {
RTC_LOG(LS_WARNING) << "Skipping invalid hard_reset_timeout="
<< 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;
config.alarm_threshold = defaults.alarm_threshold;
}
if (config.acc_drift < 0) {
RTC_LOG(LS_WARNING) << "Skipping invalid acc_drift=" << config.acc_drift;
config.acc_drift = defaults.acc_drift;
}
if (config.acc_max_error <= 0) {
RTC_LOG(LS_WARNING) << "Skipping invalid acc_max_error="
<< config.acc_max_error;
config.acc_max_error = defaults.acc_max_error;
}
return config;
}
TimestampExtrapolator::TimestampExtrapolator(
Timestamp start,
const FieldTrialsView& field_trials)
: config_(Config::ParseAndValidate(field_trials)),
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);
}
TimestampExtrapolator::~TimestampExtrapolator() {
if (packet_count_ >= kMinimumSamplesToLogEstimatedClockDrift) {
// Relative clock drift per million (ppm).
double clock_drift_ppm = 1e6 * (w_[0] - 90.0) / 90.0;
RTC_HISTOGRAM_COUNTS_100000("WebRTC.Video.EstimatedClockDrift_ppm",
static_cast<int>(std::abs(clock_drift_ppm)));
}
}
void TimestampExtrapolator::Reset(Timestamp start) {
start_ = start;
prev_ = start_;
first_unwrapped_timestamp_ = std::nullopt;
prev_unwrapped_timestamp_ = std::nullopt;
w_[0] = 90.0;
w_[1] = 0;
p_[0][0] = kP00;
p_[1][1] = kP11;
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) {
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();
if (!first_unwrapped_timestamp_) {
// Make an initial guess of the offset,
// should be almost correct since t_ms - start
// should about zero at this time.
w_[1] = -w_[0] * t_ms;
first_unwrapped_timestamp_ = unwrapped_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.
SoftReset();
}
// 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);
double K[2];
K[0] = p_[0][0] * t_ms + p_[0][1];
K[1] = p_[1][0] * t_ms + p_[1][1];
double TPT = kLambda + t_ms * K[0] + K[1];
K[0] /= TPT;
K[1] /= TPT;
// w = w + K*(ts(k) - that);
w_[0] = w_[0] + K[0] * residual;
w_[1] = w_[1] + K[1] * residual;
// P = 1/lambda*(P - K*T'*P);
double p00 =
1 / kLambda * (p_[0][0] - (K[0] * t_ms * p_[0][0] + K[0] * p_[1][0]));
double p01 =
1 / kLambda * (p_[0][1] - (K[0] * t_ms * p_[0][1] + K[0] * p_[1][1]));
p_[1][0] =
1 / kLambda * (p_[1][0] - (K[1] * t_ms * p_[0][0] + K[1] * p_[1][0]));
p_[1][1] =
1 / kLambda * (p_[1][1] - (K[1] * t_ms * p_[0][1] + K[1] * p_[1][1]));
p_[0][0] = p00;
p_[0][1] = p01;
prev_unwrapped_timestamp_ = unwrapped_ts90khz;
if (packet_count_ < kStartUpFilterDelayInPackets ||
packet_count_ < kMinimumSamplesToLogEstimatedClockDrift) {
packet_count_++;
}
}
std::optional<Timestamp> TimestampExtrapolator::ExtrapolateLocalTime(
uint32_t timestamp90khz) const {
int64_t unwrapped_ts90khz = unwrapper_.PeekUnwrap(timestamp90khz);
if (!first_unwrapped_timestamp_) {
return std::nullopt;
}
if (packet_count_ < kStartUpFilterDelayInPackets) {
constexpr double kRtpTicksPerMs = 90;
TimeDelta diff = TimeDelta::Millis(
(unwrapped_ts90khz - *prev_unwrapped_timestamp_) / kRtpTicksPerMs);
if (prev_.us() + diff.us() < 0) {
// Prevent the construction of a negative Timestamp.
// This scenario can occur when the RTP timestamp wraps around.
return std::nullopt;
}
return prev_ + diff;
}
if (w_[0] < 1e-3) {
return start_;
}
double timestamp_diff =
static_cast<double>(unwrapped_ts90khz - *first_unwrapped_timestamp_);
TimeDelta diff = TimeDelta::Millis(
static_cast<int64_t>((timestamp_diff - w_[1]) / w_[0] + 0.5));
if (start_.us() + diff.us() < 0) {
// Prevent the construction of a negative Timestamp.
// This scenario can occur when the RTP timestamp wraps around.
return std::nullopt;
}
return start_ + diff;
}
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);
residual = (residual > 0) ? std::min(residual, acc_max_error)
: std::max(residual, -acc_max_error);
detector_accumulator_pos_ = std::max(
detector_accumulator_pos_ + residual - config_.acc_drift, double{0});
detector_accumulator_neg_ = std::min(
detector_accumulator_neg_ + residual + config_.acc_drift, double{0});
if (detector_accumulator_pos_ > config_.alarm_threshold ||
detector_accumulator_neg_ < -config_.alarm_threshold) {
// Alarm
detector_accumulator_pos_ = detector_accumulator_neg_ = 0;
return true;
}
return false;
}
} // namespace webrtc