| /* |
| * 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 |