/* * 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 #include #include #include #include #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(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((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 at " << unwrapped_ts90khz << ". 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(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 TimestampExtrapolator::ExtrapolateLocalTime( uint32_t timestamp90khz) const { int64_t unwrapped_ts90khz = unwrapper_.PeekUnwrap(timestamp90khz); RTC_DCHECK_GE(unwrapped_ts90khz, 0); 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(unwrapped_ts90khz - *first_unwrapped_timestamp_); TimeDelta diff = TimeDelta::Millis( static_cast((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(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