blob: 49a7002bcb6386338e55218ceeaf4266a31039e4 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cast/streaming/impl/clock_offset_estimator_impl.h"
#include <algorithm>
#include <chrono>
#include <memory>
#include <utility>
#include "platform/base/trivial_clock_traits.h"
#include "util/chrono_helpers.h"
namespace openscreen::cast {
namespace {
// This should be large enough so that we can collect all 3 events before
// the entry gets removed from the map.
constexpr size_t kMaxEventTimesMapSize = 500;
// Bitwise merging of values to produce an ordered key for entries in the
// BoundCalculator::events_ map. Since std::map is sorted by key value, we
// ensure that the Packet ID is first (since the RTP timestamp may roll over
// eventually).
//
// 0 1 2 3 4 5 6
// 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Packet ID | RTP Timestamp |*| (is_audio)
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
uint64_t MakeEventKey(RtpTimeTicks rtp, uint16_t packet_id, bool audio) {
return (static_cast<uint64_t>(packet_id) << 48) |
(static_cast<uint64_t>(rtp.lower_32_bits()) << 1) |
static_cast<uint64_t>(audio ? 1 : 0);
}
} // namespace
std::unique_ptr<ClockOffsetEstimator> ClockOffsetEstimator::Create() {
return std::make_unique<ClockOffsetEstimatorImpl>();
}
ClockOffsetEstimatorImpl::ClockOffsetEstimatorImpl() = default;
ClockOffsetEstimatorImpl::ClockOffsetEstimatorImpl(
ClockOffsetEstimatorImpl&&) noexcept = default;
ClockOffsetEstimatorImpl& ClockOffsetEstimatorImpl::operator=(
ClockOffsetEstimatorImpl&&) = default;
ClockOffsetEstimatorImpl::~ClockOffsetEstimatorImpl() = default;
void ClockOffsetEstimatorImpl::OnFrameEvent(const FrameEvent& frame_event) {
switch (frame_event.type) {
case StatisticsEvent::Type::kFrameAckSent:
frame_bound_.SetSent(
frame_event.rtp_timestamp, 0,
frame_event.media_type == StatisticsEvent::MediaType::kAudio,
frame_event.timestamp);
break;
case StatisticsEvent::Type::kFrameAckReceived:
frame_bound_.SetReceived(
frame_event.rtp_timestamp, 0,
frame_event.media_type == StatisticsEvent::MediaType::kAudio,
frame_event.timestamp);
break;
default:
// Ignored
break;
}
}
void ClockOffsetEstimatorImpl::OnPacketEvent(const PacketEvent& packet_event) {
switch (packet_event.type) {
case StatisticsEvent::Type::kPacketSentToNetwork:
packet_bound_.SetSent(
packet_event.rtp_timestamp, packet_event.packet_id,
packet_event.media_type == StatisticsEvent::MediaType::kAudio,
packet_event.timestamp);
break;
case StatisticsEvent::Type::kPacketReceived:
packet_bound_.SetReceived(
packet_event.rtp_timestamp, packet_event.packet_id,
packet_event.media_type == StatisticsEvent::MediaType::kAudio,
packet_event.timestamp);
break;
default:
// Ignored
break;
}
}
bool ClockOffsetEstimatorImpl::GetReceiverOffsetBounds(
Clock::duration& frame_bound,
Clock::duration& packet_bound) const {
if (!frame_bound_.has_bound() || !packet_bound_.has_bound()) {
return false;
}
frame_bound = -frame_bound_.bound();
packet_bound = packet_bound_.bound();
return true;
}
std::optional<Clock::duration> ClockOffsetEstimatorImpl::GetEstimatedOffset()
const {
Clock::duration frame_bound;
Clock::duration packet_bound;
if (!GetReceiverOffsetBounds(frame_bound, packet_bound)) {
return {};
}
return (packet_bound + frame_bound) / 2;
}
std::optional<Clock::duration> ClockOffsetEstimatorImpl::GetEstimatedLatency()
const {
Clock::duration frame_bound;
Clock::duration packet_bound;
if (!GetReceiverOffsetBounds(frame_bound, packet_bound)) {
return {};
}
return (packet_bound - frame_bound) / 2;
}
ClockOffsetEstimatorImpl::KalmanFilter::KalmanFilter(
Clock::duration process_noise,
Clock::duration measurement_noise)
: q_nanos_squared_(
static_cast<double>(std::chrono::nanoseconds(process_noise).count()) *
std::chrono::nanoseconds(process_noise).count()),
r_nanos_squared_(
static_cast<double>(
std::chrono::nanoseconds(measurement_noise).count()) *
std::chrono::nanoseconds(measurement_noise).count()) {}
void ClockOffsetEstimatorImpl::KalmanFilter::Update(
Clock::duration measurement) {
if (!has_estimate_) {
// First measurement, initialize the state.
estimated_latency_ = measurement;
error_covariance_nanos_squared_ = r_nanos_squared_;
has_estimate_ = true;
return;
}
// --- 1. PREDICT ---
// The predicted state is the same as the previous state.
// The uncertainty (covariance) increases by the process noise.
const double predicted_error_covariance =
error_covariance_nanos_squared_ + q_nanos_squared_;
// --- 2. UPDATE ---
// Calculate Kalman Gain.
const double kalman_gain = predicted_error_covariance /
(predicted_error_covariance + r_nanos_squared_);
// Update the estimate with the new measurement.
const double measurement_nanos =
static_cast<double>(std::chrono::nanoseconds(measurement).count());
const double estimate_nanos =
static_cast<double>(std::chrono::nanoseconds(estimated_latency_).count());
const double new_estimate_nanos =
estimate_nanos + kalman_gain * (measurement_nanos - estimate_nanos);
estimated_latency_ =
std::chrono::duration_cast<Clock::duration>(std::chrono::nanoseconds(
static_cast<Clock::duration::rep>(new_estimate_nanos)));
// Update the error covariance.
error_covariance_nanos_squared_ =
(1.0 - kalman_gain) * predicted_error_covariance;
}
ClockOffsetEstimatorImpl::BoundCalculator::BoundCalculator()
: filter_(kProcessNoise, kMeasurementNoise) {}
ClockOffsetEstimatorImpl::BoundCalculator::BoundCalculator(
BoundCalculator&&) noexcept = default;
ClockOffsetEstimatorImpl::BoundCalculator&
ClockOffsetEstimatorImpl::BoundCalculator::operator=(BoundCalculator&&) =
default;
ClockOffsetEstimatorImpl::BoundCalculator::~BoundCalculator() = default;
void ClockOffsetEstimatorImpl::BoundCalculator::SetSent(RtpTimeTicks rtp,
uint16_t packet_id,
bool audio,
Clock::time_point t) {
const uint64_t key = MakeEventKey(rtp, packet_id, audio);
events_[key].first = t;
CheckUpdate(key);
}
void ClockOffsetEstimatorImpl::BoundCalculator::SetReceived(
RtpTimeTicks rtp,
uint16_t packet_id,
bool audio,
Clock::time_point t) {
const uint64_t key = MakeEventKey(rtp, packet_id, audio);
events_[key].second = t;
CheckUpdate(key);
}
void ClockOffsetEstimatorImpl::BoundCalculator::UpdateBound(
Clock::time_point sent,
Clock::time_point received) {
filter_.Update(received - sent);
}
void ClockOffsetEstimatorImpl::BoundCalculator::CheckUpdate(uint64_t key) {
const TimeTickPair& ticks = events_[key];
if (ticks.first && ticks.second) {
UpdateBound(ticks.first.value(), ticks.second.value());
events_.erase(key);
return;
}
if (events_.size() > kMaxEventTimesMapSize) {
// We can make use of the fact that std::map sorts by key and just erase
// the first entry.
events_.erase(events_.begin());
}
}
} // namespace openscreen::cast