Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s

Builds out the full v1 scope: a vendored+patched openscreen subset for
low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the
existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's
Idle/Casting state machine with a private IPC socket, the breadcast GTK4
popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd
integration (device discovery, start/stop, mirroring lifecycle events).
Also adds bakery/systemd/Forgejo CI packaging.

Validated end-to-end against a real Chromecast/Google TV: negotiated
Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/
stop through the actual popup.
This commit is contained in:
Breadway 2026-08-03 09:07:21 +08:00
parent 887c29002f
commit 8c745d18e0
283 changed files with 36788 additions and 0 deletions

View file

@ -0,0 +1,155 @@
// Copyright 2020 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/bandwidth_estimator.h"
#include <algorithm>
#include "util/osp_logging.h"
#include "util/saturate_cast.h"
namespace openscreen::cast {
using clock_operators::operator<<;
namespace {
// Converts units from `bytes` per `time_window` number of Clock ticks into
// bits-per-second.
int ToClampedBitsPerSecond(int32_t bytes, Clock::duration time_window) {
OSP_CHECK_GT(time_window, Clock::duration::zero());
// Divide `bytes` by `time_window` and scale the units to bits per second.
constexpr int64_t kBitsPerByte = 8;
constexpr int64_t kClockTicksPerSecond =
Clock::to_duration(std::chrono::seconds(1)).count();
const int64_t bits = bytes * kBitsPerByte;
const int64_t bits_per_second =
(bits * kClockTicksPerSecond) / time_window.count();
return saturate_cast<int>(bits_per_second);
}
} // namespace
BandwidthEstimator::BandwidthEstimator(int max_packets_per_timeslice,
Clock::duration timeslice_duration,
Clock::time_point start_time)
: max_packets_per_history_window_(max_packets_per_timeslice *
kNumTimeslices),
history_window_(timeslice_duration * kNumTimeslices),
burst_history_(timeslice_duration, start_time),
feedback_history_(timeslice_duration, start_time) {
OSP_CHECK_GT(max_packets_per_timeslice, 0);
OSP_CHECK_GT(timeslice_duration, Clock::duration::zero());
}
BandwidthEstimator::~BandwidthEstimator() = default;
void BandwidthEstimator::OnBurstComplete(int num_packets_sent,
Clock::time_point when) {
OSP_CHECK_GE(num_packets_sent, 0);
burst_history_.Accumulate(num_packets_sent, when);
}
void BandwidthEstimator::OnRtcpReceived(
Clock::time_point arrival_time,
Clock::duration estimated_round_trip_time) {
OSP_CHECK_GE(estimated_round_trip_time, Clock::duration::zero());
// Move forward the feedback history tracking timeline to include the latest
// moment a packet could have left the Sender.
feedback_history_.AdvanceToIncludeTime(arrival_time -
estimated_round_trip_time);
}
void BandwidthEstimator::OnPayloadReceived(
int payload_bytes_acknowledged,
Clock::time_point ack_arrival_time,
Clock::duration estimated_round_trip_time) {
OSP_CHECK_GE(payload_bytes_acknowledged, 0);
OSP_CHECK_LT(ack_arrival_time, Clock::time_point::max());
OSP_CHECK_GE(estimated_round_trip_time, Clock::duration::zero());
// Track the bytes in terms of when the last packet was sent.
feedback_history_.Accumulate(payload_bytes_acknowledged,
ack_arrival_time - estimated_round_trip_time);
}
int BandwidthEstimator::ComputeNetworkBandwidth() const {
// Determine whether the `burst_history_` time window overlaps with the
// `feedback_history_` time window by at least half. The time windows don't
// have to overlap entirely because the calculations are averaging all the
// measurements (i.e., recent typical behavior). Though, they should overlap
// by "enough" so that the measurements correlate "enough."
const Clock::time_point overlap_begin =
std::max(burst_history_.begin_time(), feedback_history_.begin_time());
const Clock::time_point overlap_end =
std::min(burst_history_.end_time(), feedback_history_.end_time());
if ((overlap_end - overlap_begin) < (history_window_ / 2)) {
return 0;
}
const int32_t num_packets_transmitted = burst_history_.Sum();
if (num_packets_transmitted <= 0) {
// Cannot estimate because there have been no transmissions recently.
return 0;
}
const Clock::duration transmit_duration = history_window_ *
num_packets_transmitted /
max_packets_per_history_window_;
const int32_t num_bytes_received = feedback_history_.Sum();
return ToClampedBitsPerSecond(num_bytes_received, transmit_duration);
}
// static
constexpr int BandwidthEstimator::kNumTimeslices;
BandwidthEstimator::FlowTracker::FlowTracker(Clock::duration timeslice_duration,
Clock::time_point begin_time)
: timeslice_duration_(timeslice_duration), begin_time_(begin_time) {}
BandwidthEstimator::FlowTracker::~FlowTracker() = default;
void BandwidthEstimator::FlowTracker::AdvanceToIncludeTime(
Clock::time_point until) {
if (until < end_time()) {
return; // Not advancing.
}
// Step forward in time, at timeslice granularity.
const int64_t num_periods = 1 + (until - end_time()) / timeslice_duration_;
begin_time_ += num_periods * timeslice_duration_;
// Shift the ring elements, discarding N oldest timeslices, and creating N new
// ones initialized to zero.
const int shift_count = std::min<int64_t>(num_periods, kNumTimeslices);
for (int i = 0; i < shift_count; ++i) {
history_ring_[tail_++] = 0;
}
}
void BandwidthEstimator::FlowTracker::Accumulate(int32_t amount,
Clock::time_point when) {
if (when < begin_time_) {
return; // Ignore a data point that is already too old.
}
AdvanceToIncludeTime(when);
// Because of the AdvanceToIncludeTime() call just made, the offset/index
// calculations here are guaranteed to point to a valid element in the
// `history_ring_`.
const int64_t offset_from_first = (when - begin_time_) / timeslice_duration_;
const index_mod_256_t ring_index = tail_ + offset_from_first;
int32_t& timeslice = history_ring_[ring_index];
timeslice = saturate_cast<int32_t>(int64_t{timeslice} + amount);
}
int32_t BandwidthEstimator::FlowTracker::Sum() const {
int64_t result = 0;
for (int32_t amount : history_ring_) {
result += amount;
}
return saturate_cast<int32_t>(result);
}
} // namespace openscreen::cast

View file

@ -0,0 +1,168 @@
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_BANDWIDTH_ESTIMATOR_H_
#define CAST_STREAMING_IMPL_BANDWIDTH_ESTIMATOR_H_
#include <stdint.h>
#include <limits>
#include "platform/api/time.h"
namespace openscreen::cast {
// Tracks send attempts and successful receives, and then computes a total
// network bandwith estimate.
//
// Two metrics are tracked by the BandwidthEstimator, over a "recent history"
// time window:
//
// 1. The number of packets sent during bursts (see SenderPacketRouter for
// explanation of what a "burst" is). These track when the network was
// actually in-use for transmission and the magnitude of each burst. When
// computing bandwidth, the estimator assumes the timeslices where the
// network was not in-use could have been used to send even more bytes at
// the same rate.
//
// 2. Successful receipt of payload bytes over time, or a lack thereof.
// Packets that include acknowledgements from the Receivers are providing
// proof of the successful receipt of payload bytes. All other packets
// provide proof of network connectivity over time, and are used to
// identify periods of time where nothing was received.
//
// The BandwidthEstimator assumes a simplified model for streaming over the
// network. The model does not include any detailed knowledge about things like
// protocol overhead, packet re-transmits, parasitic bufferring, network
// reliability, etc. Instead, it automatically accounts for all such things by
// looking at what's actually leaving the Senders and what's actually making it
// to the Receivers.
//
// This simplified model does produce some known inaccuracies in the resulting
// estimations. If no data has recently been transmitted (or been received),
// estimations cannot be provided. If the transmission rate is near (or
// exceeding) the network's capacity, the estimations will be very accurate. In
// between those two extremes, the logic will tend to under-estimate the
// network's capacity. However, those under-estimates will still be far larger
// than the current transmission rate.
//
// Thus, these estimates can be used effectively as a control signal for
// congestion control in upstream code modules. The logic computing the media's
// encoding target bitrate should be adjusted in realtime using a TCP-like
// congestion control algorithm:
//
// 1. When the estimated bitrate is less than the current encoding target
// bitrate, aggressively and immediately decrease the encoding bitrate.
//
// 2. When the estimated bitrate is more than the current encoding target
// bitrate, gradually increase the encoding bitrate (up to the maximum
// that is reasonable for the application).
class BandwidthEstimator {
public:
// `max_packets_per_timeslice` and `timeslice_duration` should match the burst
// configuration in SenderPacketRouter. `start_time` should be a recent
// point-in-time before the first packet is sent.
BandwidthEstimator(int max_packets_per_timeslice,
Clock::duration timeslice_duration,
Clock::time_point start_time);
~BandwidthEstimator();
// Returns the duration of the fixed, recent-history time window over which
// data flows are being tracked.
Clock::duration history_window() const { return history_window_; }
// Records `when` burst-sending was active or inactive. For the active case,
// `num_packets_sent` should include all network packets sent, including
// non-payload packets (since both affect the modeled utilization/capacity).
// For the inactive case, this method should be called with zero for
// `num_packets_sent`.
void OnBurstComplete(int num_packets_sent, Clock::time_point when);
// Records when a RTCP packet was received. It's important for Senders to call
// this any time a packet comes in from the Receivers, even if no payload is
// being acknowledged, since the time windows of "nothing successfully
// received" is also important information to track.
void OnRtcpReceived(Clock::time_point arrival_time,
Clock::duration estimated_round_trip_time);
// Records that some number of payload bytes has been acknowledged (i.e.,
// successfully received).
void OnPayloadReceived(int payload_bytes_acknowledged,
Clock::time_point ack_arrival_time,
Clock::duration estimated_round_trip_time);
// Computes the current network bandwith estimate. Returns 0 if this cannot be
// determined due to a lack of sufficiently-recent data.
int ComputeNetworkBandwidth() const;
private:
// FlowTracker (below) manages a ring buffer of size 256. It simplifies the
// index calculations to use an integer data type where all arithmetic is mod
// 256.
using index_mod_256_t = uint8_t;
static constexpr int kNumTimeslices =
static_cast<int>(std::numeric_limits<index_mod_256_t>::max()) + 1;
// Tracks volume (e.g., the total number of payload bytes) over a fixed
// recent-history time window. The time window is divided up into a number of
// identical timeslices, each of which represents the total number of bytes
// that flowed during a certain period of time. The data is accumulated in
// ring buffer elements so that old data points drop-off as newer ones (that
// move the history window forward) are added.
class FlowTracker {
public:
FlowTracker(Clock::duration timeslice_duration,
Clock::time_point begin_time);
~FlowTracker();
Clock::time_point begin_time() const { return begin_time_; }
Clock::time_point end_time() const {
return begin_time_ + timeslice_duration_ * kNumTimeslices;
}
// Advance the end of the time window being tracked such that the
// most-recent timeslice includes `until`. Too-old timeslices are dropped
// and new ones are initialized to a zero amount.
void AdvanceToIncludeTime(Clock::time_point until);
// Accumulate the given `amount` into the timeslice that includes `when`.
void Accumulate(int32_t amount, Clock::time_point when);
// Return the sum of all the amounts in recent history. This clamps to the
// valid range of int32_t, if necessary.
int32_t Sum() const;
private:
const Clock::duration timeslice_duration_;
// The beginning of the oldest timeslice in the recent-history time window,
// the one pointed to by `tail_`.
Clock::time_point begin_time_;
// A ring buffer tracking the accumulated amount for each timeslice.
int32_t history_ring_[kNumTimeslices]{};
// The index of the oldest timeslice in the `history_ring_`. This can also
// be thought of, equivalently, as the index just after the most-recent
// timeslice.
index_mod_256_t tail_ = 0;
};
// The maximum number of packet sends that could possibly be attempted during
// the recent-history time window.
const int max_packets_per_history_window_;
// The range of time being tracked.
const Clock::duration history_window_;
// History tracking for send attempts, and success feeback. These timeseries
// are in terms of when packets have left the Senders.
FlowTracker burst_history_;
FlowTracker feedback_history_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_BANDWIDTH_ESTIMATOR_H_

View file

@ -0,0 +1,79 @@
// Copyright 2014 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_drift_smoother.h"
#include <cmath>
#include "util/chrono_helpers.h"
#include "util/osp_logging.h"
#include "util/saturate_cast.h"
namespace openscreen::cast {
namespace {
constexpr Clock::time_point kNullTime = Clock::time_point::min();
}
using clock_operators::operator<<;
ClockDriftSmoother::ClockDriftSmoother(Clock::duration time_constant)
: time_constant_(time_constant),
last_update_time_(kNullTime),
estimated_tick_offset_(0.0) {
OSP_CHECK(time_constant_ > decltype(time_constant_)::zero());
}
ClockDriftSmoother::~ClockDriftSmoother() = default;
std::optional<Clock::duration> ClockDriftSmoother::Current() const {
if (last_update_time_ == kNullTime) {
return std::nullopt;
}
return Clock::duration(
rounded_saturate_cast<Clock::duration::rep>(estimated_tick_offset_));
}
void ClockDriftSmoother::Reset(Clock::time_point now,
Clock::duration measured_offset) {
OSP_CHECK_NE(now, kNullTime);
last_update_time_ = now;
estimated_tick_offset_ = static_cast<double>(measured_offset.count());
}
void ClockDriftSmoother::Update(Clock::time_point now,
Clock::duration measured_offset) {
OSP_CHECK_NE(now, kNullTime);
if (last_update_time_ == kNullTime) {
Reset(now, measured_offset);
return;
}
if (now < last_update_time_) {
// `now` is not monotonically non-decreasing.
OSP_NOTREACHED();
}
const double elapsed_ticks =
static_cast<double>((now - last_update_time_).count());
last_update_time_ = now;
// This is a standard exponential moving average (EMA) filter.
// The alpha value is calculated such that the filter has the desired time
// constant.
const double alpha = 1.0 - std::exp(-elapsed_ticks / time_constant_.count());
estimated_tick_offset_ =
alpha * static_cast<double>(measured_offset.count()) +
(1.0 - alpha) * estimated_tick_offset_;
const auto current = Current();
OSP_VLOG << "Local clock is ahead of the remote clock by: measured = "
<< measured_offset << ", "
<< "filtered = " << (current ? ToString(*current) : "null") << ".";
}
// static
constexpr std::chrono::seconds ClockDriftSmoother::kDefaultTimeConstant;
} // namespace openscreen::cast

View file

@ -0,0 +1,58 @@
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_CLOCK_DRIFT_SMOOTHER_H_
#define CAST_STREAMING_IMPL_CLOCK_DRIFT_SMOOTHER_H_
#include <chrono>
#include <optional>
#include "platform/api/time.h"
namespace openscreen::cast {
// Tracks the jitter and drift between clocks, providing a smoothed offset.
// Internally, a Simple IIR filter is used to maintain a running average that
// moves at a rate based on the passage of time.
class ClockDriftSmoother {
public:
// `time_constant` is the amount of time an impulse signal takes to decay by
// ~62.6%. Interpretation: If the value passed to several Update() calls is
// held constant for T seconds, then the running average will have moved
// towards the value by ~62.6% from where it started.
explicit ClockDriftSmoother(Clock::duration time_constant);
~ClockDriftSmoother();
// Returns the current offset. Will be std::nullopt if no values have been
// set yet (via Reset() or Update()).
std::optional<Clock::duration> Current() const;
// Discard all history and reset to exactly `offset`, measured `now`.
void Reset(Clock::time_point now, Clock::duration offset);
// Update the current offset, which was measured `now`. The weighting that
// `measured_offset` will have on the running average is influenced by how
// much time has passed since the last call to this method (or Reset()).
// `now` should be monotonically non-decreasing over successive calls of this
// method.
void Update(Clock::time_point now, Clock::duration measured_offset);
// A time constant suitable for most use cases, where the clocks are expected
// to drift very little with respect to each other, and the jitter caused by
// clock imprecision is effectively canceled out.
static constexpr std::chrono::seconds kDefaultTimeConstant{30};
private:
const std::chrono::duration<double, Clock::duration::period> time_constant_;
// The time at which `estimated_tick_offset_` was last updated.
Clock::time_point last_update_time_;
// The current estimated offset, as number of Clock::duration ticks.
double estimated_tick_offset_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_CLOCK_DRIFT_SMOOTHER_H_

View file

@ -0,0 +1,71 @@
// 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.
#ifndef CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_
#define CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_
#include <memory>
#include <optional>
#include "cast/streaming/impl/statistics_common.h"
#include "cast/streaming/public/statistics.h"
#include "platform/base/trivial_clock_traits.h"
namespace openscreen::cast {
// Used to estimate the offset between the Sender and Receiver clocks.
class ClockOffsetEstimator {
public:
static std::unique_ptr<ClockOffsetEstimator> Create();
virtual ~ClockOffsetEstimator() {}
// TODO(issuetracker.google.com/298085631): these should be in a separate
// header, like Chrome's raw event subscriber pattern.
// See: //media/cast/logging/raw_event_subscriber.h
virtual void OnFrameEvent(const FrameEvent& frame_event) = 0;
virtual void OnPacketEvent(const PacketEvent& packet_event) = 0;
// Estimates the clock offset between the sender and the receiver.
//
// This is calculated by solving a system of two linear equations with two
// unknowns: the clock offset and the network latency. The two equations are
// derived from two round-trip time measurements.
//
// Let's define:
// - latency: the one-way network latency.
// - offset: the clock offset, where Clock_Receiver(t) = Clock_Sender(t) +
// offset.
//
// The estimator measures two bounds:
//
// 1. packet_bound (sender -> receiver):
// delta = TS_receiver - TS_sender
// = (TS_sender + latency + offset) - TS_sender
// = latency + offset
//
// 2. frame_bound (receiver -> sender):
// delta = TS_sender - TS_receiver
// = (TS_receiver + latency - offset) - TS_receiver
// = latency - offset
//
// The offset is then isolated by the formula:
// (packet_bound - frame_bound) / 2 =
// ( (latency + offset) - (latency - offset) ) / 2 =
// (2 * offset) / 2 = offset
virtual std::optional<Clock::duration> GetEstimatedOffset() const = 0;
// Estimates the one-way network latency.
// This uses the same bounds as GetEstimatedOffset().
//
// The latency is isolated by the formula:
// (packet_bound + frame_bound) / 2 =
// ( (latency + offset) + (latency - offset) ) / 2 = (2 * latency) / 2 =
// latency
virtual std::optional<Clock::duration> GetEstimatedLatency() const = 0;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_

View file

@ -0,0 +1,222 @@
// 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

View file

@ -0,0 +1,136 @@
// 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.
#ifndef CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_IMPL_H_
#define CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_IMPL_H_
#include <stddef.h>
#include <stdint.h>
#include <map>
#include <optional>
#include <utility>
#include "cast/streaming/impl/clock_offset_estimator.h"
#include "cast/streaming/impl/statistics_common.h"
#include "cast/streaming/rtp_time.h"
#include "platform/base/trivial_clock_traits.h"
#include "util/chrono_helpers.h"
namespace openscreen::cast {
// This implementation listens to two pairs of events:
// 1. FrameAckSent / FrameAckReceived (receiver->sender)
// 2. PacketSentToNetwork / PacketReceived (sender->receiver)
//
// There is a causal relationship between these events in that these events
// must happen in order. This class obtains the lower and upper bounds for
// the offset by taking the difference of timestamps.
class ClockOffsetEstimatorImpl final : public ClockOffsetEstimator {
public:
ClockOffsetEstimatorImpl();
ClockOffsetEstimatorImpl(ClockOffsetEstimatorImpl&&) noexcept;
ClockOffsetEstimatorImpl(const ClockOffsetEstimatorImpl&) = delete;
ClockOffsetEstimatorImpl& operator=(ClockOffsetEstimatorImpl&&);
ClockOffsetEstimatorImpl& operator=(const ClockOffsetEstimatorImpl&) = delete;
~ClockOffsetEstimatorImpl() final;
void OnFrameEvent(const FrameEvent& frame_event) final;
void OnPacketEvent(const PacketEvent& packet_event) final;
bool GetReceiverOffsetBounds(Clock::duration& frame_bound,
Clock::duration& packet_bound) const;
// ClockOffsetEstimator overrides.
std::optional<Clock::duration> GetEstimatedOffset() const final;
std::optional<Clock::duration> GetEstimatedLatency() const final;
private:
// These values are chosen based on common network conditions.
//
// Q (process_noise): We expect latency to drift by up to 5ms between
// measurements.
static constexpr Clock::duration kProcessNoise = milliseconds(5);
//
// R (measurement_noise): We expect jitter of up to 30ms.
static constexpr Clock::duration kMeasurementNoise = milliseconds(30);
// Simplified 1D Kalman Filter for latency estimation.
class KalmanFilter {
public:
// Q: process_noise - Represents the expected variance of the latency
// itself between time steps. A higher value makes the filter adapt
// more quickly to real changes in latency.
// R: measurement_noise - Represents the variance of the measurement
// noise (jitter). A higher value makes the filter trust its own
// prediction more and smooth out noisy measurements.
KalmanFilter(Clock::duration process_noise,
Clock::duration measurement_noise);
KalmanFilter(KalmanFilter&&) noexcept = default;
KalmanFilter& operator=(KalmanFilter&&) = default;
Clock::duration GetEstimate() const { return estimated_latency_; }
bool HasEstimate() const { return has_estimate_; }
void Update(Clock::duration measurement);
private:
double q_nanos_squared_;
double r_nanos_squared_;
bool has_estimate_ = false;
Clock::duration estimated_latency_{};
double error_covariance_nanos_squared_ = 0.0;
};
// This helper uses the difference between sent and received event
// to calculate an upper bound on the difference between the clocks
// on the sender and receiver. Note that this difference can take
// very large positive or negative values, but the smaller value is
// always the better estimate, since a receive event cannot possibly
// happen before a send event. Note that we use this to calculate
// both upper and lower bounds by reversing the sender/receiver
// relationship.
class BoundCalculator {
public:
typedef std::pair<std::optional<Clock::time_point>,
std::optional<Clock::time_point>>
TimeTickPair;
typedef std::map<uint64_t, TimeTickPair> EventMap;
BoundCalculator();
BoundCalculator(BoundCalculator&&) noexcept;
BoundCalculator(const BoundCalculator&) = delete;
BoundCalculator& operator=(BoundCalculator&&);
BoundCalculator& operator=(const BoundCalculator&) = delete;
~BoundCalculator();
bool has_bound() const { return filter_.HasEstimate(); }
Clock::duration bound() const { return filter_.GetEstimate(); }
void SetSent(RtpTimeTicks rtp,
uint16_t packet_id,
bool audio,
Clock::time_point t);
void SetReceived(RtpTimeTicks rtp,
uint16_t packet_id,
bool audio,
Clock::time_point t);
private:
void UpdateBound(Clock::time_point a, Clock::time_point b);
void CheckUpdate(uint64_t key);
private:
EventMap events_;
KalmanFilter filter_;
};
// Fixed size storage to store event times for recent frames and packets.
BoundCalculator packet_bound_;
BoundCalculator frame_bound_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_IMPL_H_

View file

@ -0,0 +1,479 @@
// Copyright 2019 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/compound_rtcp_parser.h"
#include <algorithm>
#include <utility>
#include "cast/streaming/impl/packet_util.h"
#include "cast/streaming/impl/rtcp_session.h"
#include "cast/streaming/impl/statistics_common.h"
#include "util/chrono_helpers.h"
#include "util/osp_logging.h"
#include "util/std_util.h"
namespace openscreen::cast {
namespace {
// Use the Clock's minimum time value (an impossible value, waaaaay before epoch
// time) to represent unset time_point values.
constexpr auto kNullTimePoint = Clock::time_point::min();
// Some receivers send time sync requests (that we ignore).
constexpr uint32_t kTimeSyncRequestName =
('T' << 24) + ('I' << 16) + ('M' << 8) + 'E';
// Canonicalizes the just-parsed list of packet-specific NACKs so that the
// CompoundRtcpParser::Client can make several simplifying assumptions when
// processing the results.
void CanonicalizePacketNackVector(std::vector<PacketNack>* packets) {
// First, sort all elements. The sort order is the normal lexicographical
// ordering, with one exception: The special kAllPacketsLost packet_id value
// should be treated as coming before all others. This special sort order
// allows the filtering algorithm below to be simpler, and only require one
// pass; and the final result will be the normal lexicographically-sorted
// output the CompoundRtcpParser::Client expects.
std::sort(packets->begin(), packets->end(),
[](const PacketNack& a, const PacketNack& b) {
// Since the comparator is a hot code path, use a simple modular
// arithmetic trick in lieu of extra branching: When comparing the
// tuples, map all packet_id values to packet_id + 1, mod 0x10000.
// This results in the desired sorting behavior since
// kAllPacketsLost (0xffff) wraps-around to 0x0000, and all other
// values become N + 1.
static_assert(static_cast<FramePacketId>(kAllPacketsLost + 1) <
FramePacketId{0x0000 + 1},
"comparison requires integer wrap-around");
return PacketNack{a.frame_id,
static_cast<FramePacketId>(a.packet_id + 1)} <
PacketNack{b.frame_id,
static_cast<FramePacketId>(b.packet_id + 1)};
});
// De-duplicate elements. Two possible cases:
//
// 1. Identical elements (same FrameId+FramePacketId).
// 2. If there are any elements with kAllPacketsLost as the packet ID,
// prune-out all other elements having the same frame ID, as they are
// redundant.
//
// This is done by walking forwards over the sorted vector and deciding which
// elements to keep. Those that are kept are stacked-up at the front of the
// vector. After the "to-keep" pass, the vector is truncated to remove the
// left-over garbage at the end.
auto have_it = packets->begin();
if (have_it != packets->end()) {
auto kept_it = have_it; // Always keep the first element.
for (++have_it; have_it != packets->end(); ++have_it) {
if (have_it->frame_id != kept_it->frame_id ||
(kept_it->packet_id != kAllPacketsLost &&
have_it->packet_id != kept_it->packet_id)) { // Keep it.
++kept_it;
*kept_it = *have_it;
}
}
packets->erase(++kept_it, packets->end());
}
}
} // namespace
CompoundRtcpParser::CompoundRtcpParser(RtcpSession& session,
CompoundRtcpParser::Client& client)
: session_(session),
client_(client),
latest_receiver_timestamp_(kNullTimePoint) {}
CompoundRtcpParser::~CompoundRtcpParser() = default;
bool CompoundRtcpParser::Parse(ByteView buffer, FrameId max_feedback_frame_id) {
// These will contain the results from the various ParseXYZ() methods. None of
// the results will be dispatched to the Client until the entire parse
// succeeds.
Clock::time_point receiver_reference_time = kNullTimePoint;
std::optional<RtcpReportBlock> receiver_report;
std::vector<RtcpReceiverFrameLogMessage> log_messages;
FrameId checkpoint_frame_id;
milliseconds target_playout_delay{};
std::vector<FrameId> received_frames;
std::vector<PacketNack> packet_nacks;
bool picture_loss_indicator = false;
// The data contained in `buffer` can be a "compound packet," which means that
// it can be the concatenation of multiple RTCP packets. The loop here
// processes each one-by-one.
while (!buffer.empty()) {
const auto header = RtcpCommonHeader::Parse(buffer);
if (!header) {
return false;
}
buffer = buffer.subspan(kRtcpCommonHeaderSize);
if (static_cast<int>(buffer.size()) < header->payload_size) {
return false;
}
ByteView payload = buffer.subspan(0, header->payload_size);
buffer = buffer.subspan(header->payload_size);
switch (header->packet_type) {
case RtcpPacketType::kReceiverReport:
if (!ParseReceiverReport(payload, header->with.report_count,
receiver_report)) {
return false;
}
break;
case RtcpPacketType::kApplicationDefined:
if (!ParseApplicationDefined(header->with.subtype, payload,
log_messages)) {
return false;
}
break;
case RtcpPacketType::kPayloadSpecific:
switch (header->with.subtype) {
case RtcpSubtype::kPictureLossIndicator:
if (!ParsePictureLossIndicator(payload, picture_loss_indicator)) {
return false;
}
break;
case RtcpSubtype::kFeedback:
if (!ParseFeedback(payload, max_feedback_frame_id,
&checkpoint_frame_id, &target_playout_delay,
&received_frames, packet_nacks)) {
return false;
}
break;
default:
// Ignore: Unimplemented or not part of the Cast Streaming spec.
break;
}
break;
case RtcpPacketType::kExtendedReports:
if (!ParseExtendedReports(payload, receiver_reference_time)) {
return false;
}
break;
default:
// Ignored, unimplemented or not part of the Cast Streaming spec.
break;
}
}
// A well-behaved Cast Streaming Receiver will always include a reference time
// report. This essentially "timestamps" the RTCP packets just parsed.
// However, the spec does not explicitly require this be included. When it is
// present, improve the stability of the system by ignoring stale/out-of-order
// RTCP packets.
if (receiver_reference_time != kNullTimePoint) {
// If the packet is out-of-order (e.g., it got delayed/shuffled when going
// through the network), just ignore it. Since RTCP packets always include
// all the necessary current state from the peer, dropping them does not
// mean important signals will be lost. In fact, it can actually be harmful
// to process compound RTCP packets out-of-order.
if (latest_receiver_timestamp_ != kNullTimePoint &&
receiver_reference_time < latest_receiver_timestamp_) {
return true;
}
latest_receiver_timestamp_ = receiver_reference_time;
client_->OnReceiverReferenceTimeAdvanced(latest_receiver_timestamp_);
}
// At this point, the packet is known to be well-formed. Dispatch events of
// interest to the Client.
if (receiver_report) {
client_->OnReceiverReport(*receiver_report);
}
if (!log_messages.empty()) {
client_->OnCastReceiverFrameLogMessages(std::move(log_messages));
}
if (!checkpoint_frame_id.is_null()) {
client_->OnReceiverCheckpoint(checkpoint_frame_id, target_playout_delay);
}
if (!received_frames.empty()) {
OSP_DCHECK(AreElementsSortedAndUnique(received_frames));
client_->OnReceiverHasFrames(std::move(received_frames));
}
CanonicalizePacketNackVector(&packet_nacks);
if (!packet_nacks.empty()) {
client_->OnReceiverIsMissingPackets(std::move(packet_nacks));
}
if (picture_loss_indicator) {
client_->OnReceiverIndicatesPictureLoss();
}
return true;
}
bool CompoundRtcpParser::ParseReceiverReport(
ByteView in,
int num_report_blocks,
std::optional<RtcpReportBlock>& receiver_report) {
if (in.size() < kRtcpReceiverReportSize) {
return false;
}
if (ConsumeField<uint32_t>(in) == session_->receiver_ssrc()) {
receiver_report = RtcpReportBlock::ParseOne(in, num_report_blocks,
session_->sender_ssrc());
}
return true;
}
bool CompoundRtcpParser::ParseApplicationDefined(
RtcpSubtype subtype,
ByteView in,
std::vector<RtcpReceiverFrameLogMessage>& messages) {
if (in.size() < 2 * sizeof(uint32_t)) {
return false;
}
const uint32_t sender_ssrc = ConsumeField<uint32_t>(in);
const uint32_t name = ConsumeField<uint32_t>(in);
// Just ignore events that aren't intended for us.
if (sender_ssrc != session_->receiver_ssrc()) {
return true;
}
if (name != kCastName) {
// We ignore time sync requests but don't throw an error for them.
return name == kTimeSyncRequestName;
}
if (subtype == RtcpSubtype::kReceiverLog) {
return ParseFrameLogMessages(in, messages);
}
return true;
}
bool CompoundRtcpParser::ParseFrameLogMessages(
ByteView in,
std::vector<RtcpReceiverFrameLogMessage>& messages) {
while (!in.empty()) {
if (in.size() < kRtcpReceiverFrameLogMessageHeaderSize) {
messages.clear();
return false;
}
const uint32_t truncated_rtp_timestamp = ConsumeField<uint32_t>(in);
const uint32_t data = ConsumeField<uint32_t>(in);
// The 24 least significant bits contain the event timestamp, which is
// offset from when the first packet was sent.
const uint32_t raw_timestamp = data & 0xFFFFFF;
const Clock::time_point event_timestamp_base =
session_->start_time() + milliseconds(raw_timestamp);
// The 8 most significant bits contain the number of events.
// NOTE: at least one event is required, so a value of "0" over the wire
// actually means there is one event.
const size_t num_events = 1u + static_cast<uint8_t>(data >> 24);
const RtpTimeTicks frame_log_rtp_timestamp =
latest_frame_log_rtp_timestamp_.Expand(truncated_rtp_timestamp);
RtcpReceiverFrameLogMessage frame_log_message{.rtp_timestamp =
frame_log_rtp_timestamp};
for (size_t event = 0; event < num_events; ++event) {
if (in.size() < kRtcpReceiverFrameLogMessageBlockSize) {
messages.clear();
return false;
}
const uint16_t delay_delta_or_packet_id = ConsumeField<uint16_t>(in);
const uint16_t event_type_and_timestamp_delta =
ConsumeField<uint16_t>(in);
// Skip unknown event types, they are not useful.
const auto event_type =
StatisticsEvent::FromWireType(static_cast<StatisticsEvent::WireType>(
event_type_and_timestamp_delta >> 12));
if (event_type == StatisticsEvent::Type::kUnknown) {
continue;
}
RtcpReceiverEventLogMessage event_log{
.type = event_type,
.timestamp = event_timestamp_base +
milliseconds(event_type_and_timestamp_delta & 0xFFF)};
if (event_type == StatisticsEvent::Type::kPacketReceived) {
event_log.packet_id = delay_delta_or_packet_id;
} else {
event_log.delay =
milliseconds(static_cast<int16_t>(delay_delta_or_packet_id));
}
frame_log_message.messages.emplace_back(std::move(event_log));
}
latest_frame_log_rtp_timestamp_ = frame_log_rtp_timestamp;
messages.emplace_back(std::move(frame_log_message));
}
return true;
}
bool CompoundRtcpParser::ParseFeedback(ByteView in,
FrameId max_feedback_frame_id,
FrameId* checkpoint_frame_id,
milliseconds* target_playout_delay,
std::vector<FrameId>* received_frames,
std::vector<PacketNack>& packet_nacks) {
OSP_CHECK(!max_feedback_frame_id.is_null());
if (static_cast<int>(in.size()) < kRtcpFeedbackHeaderSize) {
return false;
}
if (ConsumeField<uint32_t>(in) != session_->receiver_ssrc() ||
ConsumeField<uint32_t>(in) != session_->sender_ssrc()) {
return true; // Ignore report from mismatched SSRC(s).
}
if (ConsumeField<uint32_t>(in) != kRtcpCastIdentifierWord) {
return false;
}
const FrameId feedback_frame_id =
max_feedback_frame_id.ExpandLessThanOrEqual(ConsumeField<uint8_t>(in));
const int loss_field_count = ConsumeField<uint8_t>(in);
const auto playout_delay = milliseconds(ConsumeField<uint16_t>(in));
// Don't process feedback that would move the checkpoint backwards. The Client
// makes assumptions about what frame data and other tracking state can be
// discarded based on a monotonically non-decreasing checkpoint FrameId.
if (!checkpoint_frame_id->is_null() &&
*checkpoint_frame_id > feedback_frame_id) {
return true;
}
*checkpoint_frame_id = feedback_frame_id;
*target_playout_delay = playout_delay;
received_frames->clear();
packet_nacks.clear();
if (static_cast<int>(in.size()) <
(kRtcpFeedbackLossFieldSize * loss_field_count)) {
return false;
}
// Parse the NACKs.
for (int i = 0; i < loss_field_count; ++i) {
const FrameId frame_id =
feedback_frame_id.ExpandGreaterThan(ConsumeField<uint8_t>(in));
FramePacketId packet_id = ConsumeField<uint16_t>(in);
uint8_t bits = ConsumeField<uint8_t>(in);
packet_nacks.push_back(PacketNack{frame_id, packet_id});
if (packet_id != kAllPacketsLost) {
// Translate each set bit in the bit vector into another missing
// FramePacketId.
while (bits) {
++packet_id;
if (bits & 1) {
packet_nacks.push_back(PacketNack{frame_id, packet_id});
}
bits >>= 1;
}
}
}
// Parse the optional CST2 feedback (frame-level ACKs).
if (static_cast<int>(in.size()) < kRtcpFeedbackAckHeaderSize ||
ConsumeField<uint32_t>(in) != kRtcpCst2IdentifierWord) {
// Optional CST2 extended feedback is not present. For backwards-
// compatibility reasons, do not consider any extra "garbage" in the packet
// that doesn't match 'CST2' as corrupted input.
return true;
}
// Skip over the "Feedback Count" field. It's currently unused, though it
// might be useful for event tracing later...
in = in.subspan(sizeof(uint8_t));
const int ack_bitvector_octet_count = ConsumeField<uint8_t>(in);
if (static_cast<int>(in.size()) < ack_bitvector_octet_count) {
return false;
}
// Translate each set bit in the bit vector into a FrameId. See the
// explanation of this wire format in rtp_defines.h for where the "plus two"
// comes from.
FrameId starting_frame_id = feedback_frame_id + 2;
for (int i = 0; i < ack_bitvector_octet_count; ++i) {
uint8_t bits = ConsumeField<uint8_t>(in);
FrameId frame_id = starting_frame_id;
while (bits) {
if (bits & 1) {
received_frames->push_back(frame_id);
}
++frame_id;
bits >>= 1;
}
constexpr int kBitsPerOctet = 8;
starting_frame_id += kBitsPerOctet;
}
return true;
}
bool CompoundRtcpParser::ParseExtendedReports(
ByteView in,
Clock::time_point& receiver_reference_time) {
if (static_cast<int>(in.size()) < kRtcpExtendedReportHeaderSize) {
return false;
}
if (ConsumeField<uint32_t>(in) != session_->receiver_ssrc()) {
return true; // Ignore report from unknown receiver.
}
while (!in.empty()) {
// All extended report types have the same 4-byte subheader.
if (static_cast<int>(in.size()) < kRtcpExtendedReportBlockHeaderSize) {
return false;
}
const uint8_t block_type = ConsumeField<uint8_t>(in);
in = in.subspan(sizeof(uint8_t)); // Skip the "reserved" byte.
const int block_data_size =
static_cast<int>(ConsumeField<uint16_t>(in)) * 4;
if (static_cast<int>(in.size()) < block_data_size) {
return false;
}
if (block_type == kRtcpReceiverReferenceTimeReportBlockType) {
if (block_data_size != sizeof(uint64_t)) {
return false; // Length field must always be 2 words.
}
receiver_reference_time = session_->ntp_converter().ToLocalTime(
ReadBigEndian<uint64_t>(in.data()));
} else {
// Ignore any other type of extended report.
}
in = in.subspan(block_data_size);
}
return true;
}
bool CompoundRtcpParser::ParsePictureLossIndicator(
ByteView in,
bool& picture_loss_indicator) {
if (static_cast<int>(in.size()) < kRtcpPictureLossIndicatorHeaderSize) {
return false;
}
// Only set the flag if the PLI is from the Receiver and to this Sender.
if (ConsumeField<uint32_t>(in) == session_->receiver_ssrc() &&
ConsumeField<uint32_t>(in) == session_->sender_ssrc()) {
picture_loss_indicator = true;
}
return true;
}
CompoundRtcpParser::Client::Client() = default;
CompoundRtcpParser::Client::~Client() = default;
void CompoundRtcpParser::Client::OnReceiverReferenceTimeAdvanced(
Clock::time_point reference_time) {}
void CompoundRtcpParser::Client::OnReceiverReport(
const RtcpReportBlock& receiver_report) {}
void CompoundRtcpParser::Client::OnCastReceiverFrameLogMessages(
std::vector<RtcpReceiverFrameLogMessage> messages) {}
void CompoundRtcpParser::Client::OnReceiverIndicatesPictureLoss() {}
void CompoundRtcpParser::Client::OnReceiverCheckpoint(
FrameId frame_id,
milliseconds playout_delay) {}
void CompoundRtcpParser::Client::OnReceiverHasFrames(
std::vector<FrameId> acks) {}
void CompoundRtcpParser::Client::OnReceiverIsMissingPackets(
std::vector<PacketNack> nacks) {}
} // namespace openscreen::cast

View file

@ -0,0 +1,135 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_COMPOUND_RTCP_PARSER_H_
#define CAST_STREAMING_IMPL_COMPOUND_RTCP_PARSER_H_
#include <chrono>
#include <optional>
#include <vector>
#include "cast/streaming/impl/rtcp_common.h"
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/public/frame_id.h"
#include "platform/base/span.h"
#include "util/raw_ref.h"
namespace openscreen::cast {
class RtcpSession;
// Parses compound RTCP packets from a Receiver, invoking client callbacks when
// information of interest to a Sender (in the current process) is encountered.
class CompoundRtcpParser {
public:
// Callback interface used while parsing RTCP packets of interest to a Sender.
// The implementation must take into account:
//
// 1. Some/All of the data could be stale, as it only reflects the state of
// the Receiver at the time the packet was generated. A significant
// amount of time may have passed, depending on how long it took the
// packet to reach this local instance over the network.
// 2. The data shouldn't necessarily be trusted blindly: Some may be
// inconsistent (e.g., the same frame being ACKed and NACKed; or a frame
// that has not been sent yet is being NACKed). While that would indicate
// a badly-behaving Receiver, the Sender should be robust to such things.
class Client {
public:
Client();
// Called when a Receiver Reference Time Report has been parsed.
virtual void OnReceiverReferenceTimeAdvanced(
Clock::time_point reference_time);
// Called when a Receiver Report with a Report Block has been parsed.
virtual void OnReceiverReport(const RtcpReportBlock& receiver_report);
// Called when a group of Cast Receiver frame log messages has been parsed.
virtual void OnCastReceiverFrameLogMessages(
std::vector<RtcpReceiverFrameLogMessage> messages);
// Called when the Receiver has encountered an unrecoverable error in
// decoding the data. The Sender should provide a key frame as soon as
// possible.
virtual void OnReceiverIndicatesPictureLoss();
// Called when the Receiver indicates that all of the packets for all frames
// up to and including `frame_id` have been successfully received (or
// otherwise do not need to be re-transmitted). The `playout_delay` is the
// Receiver's current end-to-end target playout delay setting, which should
// reflect any changes the Sender has made by using the "Cast Adaptive
// Latency Extension" in RTP packets.
virtual void OnReceiverCheckpoint(FrameId frame_id,
std::chrono::milliseconds playout_delay);
// Called to indicate the Receiver has successfully received all of the
// packets for each of the given `acks`. The argument's elements are in
// monotonically increasing order.
virtual void OnReceiverHasFrames(std::vector<FrameId> acks);
// Called to indicate the Receiver is missing certain specific packets for
// certain specific frames. Any elements where the packet_id is
// kAllPacketsLost indicates that all the packets are missing for a frame.
// The argument's elements are in monotonically increasing order.
virtual void OnReceiverIsMissingPackets(std::vector<PacketNack> nacks);
protected:
virtual ~Client();
};
// `session` and `client` must be non-null and must outlive the
// CompoundRtcpParser instance.
CompoundRtcpParser(RtcpSession& session, Client& client);
~CompoundRtcpParser();
// Parses the packet, invoking the Client callback methods when appropriate.
// Returns true if the `packet` was well-formed, or false if it was corrupt.
// Note that none of the Client callback methods will be invoked until a
// packet is known to be well-formed.
//
// `max_feedback_frame_id` is the maximum-valued FrameId that could possibly
// be ACKnowledged by the Receiver, if there is Cast Feedback in the `packet`.
// This is needed for expanding truncated frame IDs correctly.
bool Parse(ByteView packet, FrameId max_feedback_frame_id);
private:
// These return true if the input was well-formed, and false if it was
// invalid/corrupt. The true/false value does NOT indicate whether the data
// contained within was ignored. Output arguments are only modified if the
// input contained the relevant field(s).
bool ParseReceiverReport(ByteView in,
int num_report_blocks,
std::optional<RtcpReportBlock>& receiver_report);
bool ParseApplicationDefined(
RtcpSubtype subtype,
ByteView in,
std::vector<RtcpReceiverFrameLogMessage>& messages);
bool ParseFrameLogMessages(
ByteView in,
std::vector<RtcpReceiverFrameLogMessage>& messages);
bool ParseFeedback(ByteView in,
FrameId max_feedback_frame_id,
FrameId* checkpoint_frame_id,
std::chrono::milliseconds* target_playout_delay,
std::vector<FrameId>* received_frames,
std::vector<PacketNack>& packet_nacks);
bool ParseExtendedReports(ByteView in,
Clock::time_point& receiver_reference_time);
bool ParsePictureLossIndicator(ByteView in, bool& picture_loss_indicator);
const raw_ref<RtcpSession> session_;
const raw_ref<Client> client_;
// Tracks the latest timestamp seen from any Receiver Reference Time Report,
// and uses this to ignore stale RTCP packets that arrived out-of-order and/or
// late from the network.
Clock::time_point latest_receiver_timestamp_;
// Tracks the last parsed RTP timestamp seen from any Cast receiver frame log.
RtpTimeTicks latest_frame_log_rtp_timestamp_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_COMPOUND_RTCP_PARSER_H_

View file

@ -0,0 +1,174 @@
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_EXPANDED_VALUE_BASE_H_
#define CAST_STREAMING_IMPL_EXPANDED_VALUE_BASE_H_
#include <stdint.h>
#include <limits>
#include "util/osp_logging.h"
namespace openscreen::cast {
// Abstract base template class for common "sequence value" data types such as
// RtpTimeTicks, FrameId, or PacketId which generally increment/decrement in
// predictable amounts as media is streamed, and which often need to be reliably
// truncated and re-expanded for over-the-wire transmission.
//
// FullWidthInteger should be a signed integer POD type that is of sufficiently
// high width (in bits) such that it is never expected to under/overflow during
// the longest reasonable length of continuous system operation. Subclass is
// the class inheriting the common functionality provided in this template, and
// is used to provide operator overloads. The Subclass must friend this class
// to enable these operator overloads.
//
// Please see RtpTimeTicks and unit test code for examples of how to define
// Subclasses and add features specific to their concrete data type, and how to
// use data types derived from ExpandedValueBase. For example, a RtpTimeTicks
// adds math operators consisting of the meaningful and valid set of operations
// allowed for doing "time math." On the other hand, FrameId only adds math
// operators for incrementing/decrementing since multiplication and division are
// meaningless.
template <typename FullWidthInteger, class Subclass>
class ExpandedValueBase {
static_assert(std::numeric_limits<FullWidthInteger>::is_signed,
"FullWidthInteger must be a signed integer.");
static_assert(std::numeric_limits<FullWidthInteger>::is_integer,
"FullWidthInteger must be a signed integer.");
public:
// Methods that return the lower bits of this value. This should only be used
// for serializing/wire-formatting, and not to subvert the restricted set of
// operators allowed on this data type.
constexpr uint8_t lower_8_bits() const {
return static_cast<uint8_t>(value_);
}
constexpr uint16_t lower_16_bits() const {
return static_cast<uint16_t>(value_);
}
constexpr uint32_t lower_32_bits() const {
return static_cast<uint32_t>(value_);
}
// Compute the greatest value less than or equal to `this` value whose lower
// bits are those of `x`. The purpose of this method is to re-instantiate an
// original value from its truncated form, usually when deserializing
// off-the-wire, when `this` value is known to be the greatest possible valid
// value.
//
// Use case example: Start with an original 32-bit value of 0x000001fe (510
// decimal) and truncate, throwing away its upper 24 bits: 0xfe. Now, send
// this truncated value over-the-wire to a peer who needs to expand it back to
// the original 32-bit value. The peer knows that the greatest possible valid
// value is 0x00000202 (514 decimal). This method will initially attempt to
// just concatenate the upper 24 bits of |this->value_| with |x| (the 8-bit
// value), and get a result of 0x000002fe (766 decimal). However, this is
// greater than |this->value_|, so the upper 24 bits are subtracted by one to
// get 0x000001fe, which is the original value.
template <typename ShortUnsigned>
Subclass ExpandLessThanOrEqual(ShortUnsigned x) const {
static_assert(!std::numeric_limits<ShortUnsigned>::is_signed,
"`x` must be an unsigned integer.");
static_assert(std::numeric_limits<ShortUnsigned>::is_integer,
"`x` must be an unsigned integer.");
static_assert(sizeof(ShortUnsigned) <= sizeof(FullWidthInteger),
"`x` must fit within the FullWidthInteger.");
if (sizeof(ShortUnsigned) < sizeof(FullWidthInteger)) {
// Initially, the `result` is composed of upper bits from `value_` and
// lower bits from `x`.
const FullWidthInteger short_max =
std::numeric_limits<ShortUnsigned>::max();
FullWidthInteger result = (value_ & ~short_max) | x;
// If the `result` is larger than `value_`, decrement the upper bits by
// one. In other words, `x` must always be interpreted as a truncated
// version of a value less than or equal to `value_`.
if (result > value_)
result -= short_max + 1;
return Subclass(result);
} else {
// Debug builds: Ensure the highest bit is not set (which would cause
// overflow when casting to the signed integer).
OSP_CHECK_EQ(
static_cast<ShortUnsigned>(0),
x & (static_cast<ShortUnsigned>(1) << ((sizeof(x) * 8) - 1)));
return Subclass(x);
}
}
// Compute the smallest value greater than `this` value whose lower bits are
// those of `x`.
template <typename ShortUnsigned>
Subclass ExpandGreaterThan(ShortUnsigned x) const {
const Subclass maximum_possible_result(
value_ + std::numeric_limits<ShortUnsigned>::max() + 1);
return maximum_possible_result.ExpandLessThanOrEqual(x);
}
// Compute the value closest to `this` value whose lower bits are those of
// `x`. The result is always within `max_distance_for_expansion()` of `this`
// value. The purpose of this method is to re-instantiate an original value
// from its truncated form, usually when deserializing off-the-wire. See
// comments for ExpandLessThanOrEqual() above for further explanation.
template <typename ShortUnsigned>
Subclass Expand(ShortUnsigned x) const {
const Subclass maximum_possible_result(
value_ + max_distance_for_expansion<ShortUnsigned>());
return maximum_possible_result.ExpandLessThanOrEqual(x);
}
// Comparison operators.
constexpr bool operator==(const ExpandedValueBase& rhs) const {
return value_ == rhs.value_;
}
constexpr bool operator!=(const ExpandedValueBase& rhs) const {
return value_ != rhs.value_;
}
constexpr bool operator<(const ExpandedValueBase& rhs) const {
return value_ < rhs.value_;
}
constexpr bool operator>(const ExpandedValueBase& rhs) const {
return value_ > rhs.value_;
}
constexpr bool operator<=(const ExpandedValueBase& rhs) const {
return value_ <= rhs.value_;
}
constexpr bool operator>=(const ExpandedValueBase& rhs) const {
return value_ >= rhs.value_;
}
// (De)Serialize for transmission over IPC. Do not use these to subvert the
// valid set of operators allowed by this class or its Subclass.
uint64_t SerializeForIPC() const {
static_assert(sizeof(uint64_t) >= sizeof(FullWidthInteger),
"Cannot serialize FullWidthInteger into an uint64_t.");
return static_cast<uint64_t>(value_);
}
static Subclass DeserializeForIPC(uint64_t serialized) {
return Subclass(static_cast<FullWidthInteger>(serialized));
}
// Design limit: Values that are truncated to the ShortUnsigned type must be
// no more than this maximum distance from each other in order to ensure the
// original value can be determined correctly.
template <typename ShortUnsigned>
static constexpr FullWidthInteger max_distance_for_expansion() {
return std::numeric_limits<ShortUnsigned>::max() / 2;
}
protected:
// Only subclasses are permitted to instantiate directly.
constexpr explicit ExpandedValueBase(FullWidthInteger value)
: value_(value) {}
FullWidthInteger value_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_EXPANDED_VALUE_BASE_H_

View file

@ -0,0 +1,106 @@
// Copyright 2019 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/frame_crypto.h"
#include <random>
#include <utility>
#include "openssl/crypto.h"
#include "openssl/err.h"
#include "openssl/rand.h"
#include "platform/base/span.h"
#include "util/big_endian.h"
#include "util/crypto/openssl_util.h"
#include "util/crypto/random_bytes.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
EncryptedFrame::EncryptedFrame() {
data = owned_data_;
}
EncryptedFrame::~EncryptedFrame() = default;
EncryptedFrame::EncryptedFrame(EncryptedFrame&& other) noexcept
: EncodedFrame(static_cast<EncodedFrame&&>(other)),
owned_data_(std::move(other.owned_data_)) {
data = owned_data_;
other.data = ByteView();
}
EncryptedFrame& EncryptedFrame::operator=(EncryptedFrame&& other) {
this->EncodedFrame::operator=(static_cast<EncodedFrame&&>(other));
owned_data_ = std::move(other.owned_data_);
data = owned_data_;
other.data = ByteView();
return *this;
}
FrameCrypto::FrameCrypto(const std::array<uint8_t, 16>& aes_key,
const std::array<uint8_t, 16>& cast_iv_mask)
: aes_key_{}, cast_iv_mask_(cast_iv_mask) {
// Ensure that the library has been initialized. CRYPTO_library_init() may be
// safely called multiple times during the life of a process.
CRYPTO_library_init();
// Initialize the 244-byte AES_KEY struct once, here at construction time. The
// const_cast<> is reasonable as this is a one-time-ctor-initialized value
// that will remain constant from here onward.
const int return_code = AES_set_encrypt_key(
aes_key.data(), aes_key.size() * 8, const_cast<AES_KEY*>(&aes_key_));
if (return_code != 0) {
ClearOpenSSLERRStack(CURRENT_LOCATION);
OSP_LOG_FATAL << "Failure when setting encryption key; unsafe to continue.";
OSP_NOTREACHED();
}
}
FrameCrypto::~FrameCrypto() = default;
EncryptedFrame FrameCrypto::Encrypt(const EncodedFrame& encoded_frame) const {
EncryptedFrame result;
encoded_frame.CopyMetadataTo(&result);
result.owned_data_.resize(encoded_frame.data.size());
result.data = result.owned_data_;
Crypt(encoded_frame.frame_id, {&encoded_frame.data, 1}, result.owned_data_);
return result;
}
void FrameCrypto::Decrypt(FrameId frame_id,
ChunkList chunks,
ByteBuffer out) const {
Crypt(frame_id, chunks, out);
}
void FrameCrypto::Crypt(FrameId frame_id,
ChunkList chunks,
ByteBuffer out) const {
OSP_CHECK(!frame_id.is_null());
// Compute the AES nonce for Cast Streaming payload encryption, which is based
// on the `frame_id`.
std::array<uint8_t, 16> aes_nonce{};
static_assert(AES_BLOCK_SIZE == sizeof(aes_nonce),
"AES_BLOCK_SIZE is not 16 bytes.");
WriteBigEndian<uint32_t>(frame_id.lower_32_bits(), aes_nonce.data() + 8);
for (size_t i = 0; i < aes_nonce.size(); ++i) {
aes_nonce[i] ^= cast_iv_mask_[i];
}
std::array<uint8_t, 16> ecount_buf{};
unsigned int block_offset = 0;
size_t out_offset = 0;
for (ByteView chunk : chunks) {
OSP_CHECK_LE(out_offset + chunk.size(), out.size());
AES_ctr128_encrypt(chunk.data(), out.data() + out_offset, chunk.size(),
&aes_key_, aes_nonce.data(), ecount_buf.data(),
&block_offset);
out_offset += chunk.size();
}
OSP_CHECK_EQ(out_offset, out.size());
}
} // namespace openscreen::cast

View file

@ -0,0 +1,78 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_FRAME_CRYPTO_H_
#define CAST_STREAMING_IMPL_FRAME_CRYPTO_H_
#include <stddef.h>
#include <stdint.h>
#include <array>
#include <vector>
#include "cast/streaming/public/encoded_frame.h"
#include "openssl/aes.h"
#include "platform/base/span.h"
namespace openscreen::cast {
class FrameCrypto;
// A subclass of EncodedFrame that represents an EncodedFrame with encrypted
// payload data, and owns the buffer storing the encrypted payload data. Use
// FrameCrypto (below) to explicitly convert between EncryptedFrames and
// EncodedFrames.
struct EncryptedFrame : public EncodedFrame {
EncryptedFrame();
~EncryptedFrame();
EncryptedFrame(EncryptedFrame&&) noexcept;
EncryptedFrame& operator=(EncryptedFrame&&);
protected:
// Since only FrameCrypto is trusted to generate the
// payload data, it is allowed direct access to the storage.
friend class FrameCrypto;
// Note: EncodedFrame::data must be updated whenever any mutations are
// performed on this member!
std::vector<uint8_t> owned_data_;
};
// Encrypts EncodedFrames before sending, or decrypts EncryptedFrames that have
// been received.
class FrameCrypto {
public:
using ChunkList = std::span<const ByteView>;
// Construct with the given 16-bytes AES key and IV mask. Both arguments
// should be randomly-generated for each new streaming session.
// GenerateRandomBytes() can be used to create them.
FrameCrypto(const std::array<uint8_t, 16>& aes_key,
const std::array<uint8_t, 16>& cast_iv_mask);
~FrameCrypto();
EncryptedFrame Encrypt(const EncodedFrame& encoded_frame) const;
// Decrypts `chunks` into `out`. `out` must have a sufficiently-sized
// data buffer.
void Decrypt(FrameId frame_id, ChunkList chunks, ByteBuffer out) const;
private:
// The 244-byte AES_KEY struct, derived from the `aes_key` passed to the ctor,
// and initialized by boringssl's AES_set_encrypt_key() function.
const AES_KEY aes_key_;
// Random bytes used in the custom heuristic to generate a different
// initialization vector for each frame.
const std::array<uint8_t, 16> cast_iv_mask_;
// AES-CTR is symmetric. Thus, the "meat" of both Encrypt() and Decrypt() is
// the same.
void Crypt(FrameId frame_id, ChunkList chunks, ByteBuffer out) const;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_FRAME_CRYPTO_H_

View file

@ -0,0 +1,15 @@
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_MESSAGE_CONSTANTS_H_
#define CAST_STREAMING_IMPL_MESSAGE_CONSTANTS_H_
namespace openscreen::cast {
// RTP extension strings.
inline constexpr char kInputEventsRtpExtension[] = "input_events";
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_MESSAGE_CONSTANTS_H_

View file

@ -0,0 +1,54 @@
// Copyright 2019 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/ntp_time.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
namespace {
// The number of seconds between 1 January 1900 and 1 January 1970.
constexpr NtpSeconds kTimeBetweenNtpEpochAndUnixEpoch(2208988800);
} // namespace
NtpTimeConverter::NtpTimeConverter(Clock::time_point now,
std::chrono::seconds since_unix_epoch)
: start_time_(now),
since_ntp_epoch_(
std::chrono::duration_cast<NtpSeconds>(since_unix_epoch) +
kTimeBetweenNtpEpochAndUnixEpoch) {}
NtpTimeConverter::~NtpTimeConverter() = default;
NtpTimestamp NtpTimeConverter::ToNtpTimestamp(
Clock::time_point time_point) const {
const Clock::duration time_since_start = time_point - start_time_;
const auto whole_seconds =
std::chrono::duration_cast<NtpSeconds>(time_since_start);
const auto remainder =
std::chrono::duration_cast<NtpFraction>(time_since_start - whole_seconds);
return AssembleNtpTimestamp(since_ntp_epoch_ + whole_seconds, remainder);
}
Clock::time_point NtpTimeConverter::ToLocalTime(NtpTimestamp timestamp) const {
auto ntp_seconds = NtpSecondsPart(timestamp);
// Year 2036 wrap-around check: If the NTP timestamp appears to be a
// point-in-time before 1970, assume the 2036 wrap-around has occurred, and
// adjust to compensate.
if (ntp_seconds <= kTimeBetweenNtpEpochAndUnixEpoch) {
constexpr NtpSeconds kNtpSecondsPerEra{INT64_C(1) << 32};
ntp_seconds += kNtpSecondsPerEra;
}
const auto whole_seconds = ntp_seconds - since_ntp_epoch_;
const auto seconds_since_start =
Clock::to_duration(whole_seconds) + start_time_;
const auto remainder = Clock::to_duration(NtpFractionPart(timestamp));
return seconds_since_start + remainder;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,73 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_NTP_TIME_H_
#define CAST_STREAMING_IMPL_NTP_TIME_H_
#include <stdint.h>
#include "platform/api/time.h"
namespace openscreen::cast {
// NTP timestamps are 64-bit timestamps that consist of two 32-bit parts: 1) The
// number of seconds since 1 January 1900; and 2) The fraction of the second,
// where 0 maps to 0x00000000 and each unit increment represents another 2^-32
// seconds.
//
// Note that it is part of the design of NTP for the seconds part to roll around
// on 7 February 2036.
using NtpTimestamp = uint64_t;
// NTP fixed-point time math: Declare two std::chrono::duration types with the
// bit-width necessary to reliably perform all conversions to/from NTP format.
using NtpSeconds = std::chrono::duration<int64_t, std::chrono::seconds::period>;
using NtpFraction =
std::chrono::duration<int64_t, std::ratio<1, INT64_C(0x100000000)>>;
constexpr NtpSeconds NtpSecondsPart(NtpTimestamp timestamp) {
return NtpSeconds(timestamp >> 32);
}
constexpr NtpFraction NtpFractionPart(NtpTimestamp timestamp) {
return NtpFraction(timestamp & 0xffffffff);
}
constexpr NtpTimestamp AssembleNtpTimestamp(NtpSeconds seconds,
NtpFraction fraction) {
return (static_cast<uint64_t>(seconds.count()) << 32) |
static_cast<uint32_t>(fraction.count());
}
// Converts between Clock::time_points and NtpTimestamps. The class is
// instantiated with the current Clock time and the current wall clock time, and
// these are used to determine a fixed origin reference point for all
// conversions. Thus, to avoid introducing unintended timing-related behaviors,
// only one NtpTimeConverter instance should be used for converting all the NTP
// timestamps in the same streaming session.
class NtpTimeConverter {
public:
NtpTimeConverter(
Clock::time_point now,
std::chrono::seconds since_unix_epoch = GetWallTimeSinceUnixEpoch());
~NtpTimeConverter();
NtpTimestamp ToNtpTimestamp(Clock::time_point time_point) const;
Clock::time_point ToLocalTime(NtpTimestamp timestamp) const;
private:
// The time point on the platform clock's timeline that corresponds to
// approximately the same time point on the NTP timeline. Note that it is
// acceptable for the granularity of the NTP seconds value to be whole seconds
// here: Both a Cast Streaming Sender and Receiver will assume their clocks
// can be off (with respect to each other) by even a large amount; and all
// that matters is that time ticks forward at a reasonable pace from some
// initial point.
const Clock::time_point start_time_;
const NtpSeconds since_ntp_epoch_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_NTP_TIME_H_

View file

@ -0,0 +1,39 @@
// Copyright 2019 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/packet_util.h"
#include "cast/streaming/impl/rtcp_common.h"
#include "cast/streaming/impl/rtp_defines.h"
namespace openscreen::cast {
std::pair<ApparentPacketType, Ssrc> InspectPacketForRouting(ByteView packet) {
// Check for RTP packets first, since they are more frequent.
if (packet.size() >= kRtpPacketMinValidSize &&
packet[0] == kRtpRequiredFirstByte &&
IsRtpPayloadType(packet[1] & kRtpPayloadTypeMask)) {
constexpr int kOffsetToSsrcField = 8;
return std::make_pair(
ApparentPacketType::RTP,
Ssrc{ReadBigEndian<uint32_t>(packet.data() + kOffsetToSsrcField)});
}
// While RTCP packets are valid if they consist of just the RTCP Common
// Header, all the RTCP packet types processed by this implementation will
// also have a SSRC field immediately following the header. This is important
// for routing the packet to the correct parser instance.
constexpr int kRtcpPacketMinAcceptableSize =
kRtcpCommonHeaderSize + sizeof(uint32_t);
if (packet.size() >= kRtcpPacketMinAcceptableSize &&
RtcpCommonHeader::Parse(packet).has_value()) {
return std::make_pair(
ApparentPacketType::RTCP,
Ssrc{ReadBigEndian<uint32_t>(packet.data() + kRtcpCommonHeaderSize)});
}
return std::make_pair(ApparentPacketType::UNKNOWN, Ssrc{0});
}
} // namespace openscreen::cast

View file

@ -0,0 +1,60 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_PACKET_UTIL_H_
#define CAST_STREAMING_IMPL_PACKET_UTIL_H_
#include <utility>
#include "cast/streaming/ssrc.h"
#include "platform/base/span.h"
#include "util/big_endian.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
// Reads a field from the start of the given span and advances the span to point
// just after the field.
template <typename Integer>
inline Integer ConsumeField(ByteView& in) {
OSP_CHECK_GE(in.size(), sizeof(Integer));
const Integer result = ReadBigEndian<Integer>(in.data());
in = in.subspan(sizeof(Integer));
return result;
}
// Writes a field at the start of the given span and advances the span to point
// just after the field.
template <typename Integer>
inline void AppendField(Integer value, ByteBuffer& out) {
WriteBigEndian<Integer>(value, out.data());
out = out.subspan(sizeof(Integer));
}
// Returns a bitmask for a field having the given number of bits. For example,
// FieldBitmask<uint8_t>(5) returns 0b00011111.
template <typename Integer>
constexpr Integer FieldBitmask(unsigned field_size_in_bits) {
return (Integer{1} << field_size_in_bits) - 1;
}
// Reserves `num_bytes` from the beginning of the given span, returning the
// reserved space.
inline ByteBuffer ReserveSpace(int num_bytes, ByteBuffer& out) {
const ByteBuffer reserved = out.subspan(0, num_bytes);
out = out.subspan(num_bytes);
return reserved;
}
// Performs a quick-scan of the packet data for the purposes of routing it to an
// appropriate parser. Identifies whether the packet is a RTP packet, RTCP
// packet, or unknown; and provides the originator's SSRC. This only performs a
// very quick scan of the packet data, and does not guarantee that a full parse
// will later succeed.
enum class ApparentPacketType { UNKNOWN, RTP, RTCP };
std::pair<ApparentPacketType, Ssrc> InspectPacketForRouting(ByteView packet);
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_PACKET_UTIL_H_

View file

@ -0,0 +1,241 @@
// Copyright 2019 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/rtcp_common.h"
#include <algorithm>
#include <limits>
#include "cast/streaming/impl/packet_util.h"
#include "util/saturate_cast.h"
namespace openscreen::cast {
RtcpCommonHeader::RtcpCommonHeader() = default;
RtcpCommonHeader::~RtcpCommonHeader() = default;
void RtcpCommonHeader::AppendFields(ByteBuffer& buffer) const {
OSP_CHECK_GE(buffer.size(), kRtcpCommonHeaderSize);
uint8_t byte0 = kRtcpRequiredVersionAndPaddingBits
<< kRtcpReportCountFieldNumBits;
switch (packet_type) {
case RtcpPacketType::kSenderReport:
case RtcpPacketType::kReceiverReport:
OSP_CHECK_LE(with.report_count,
FieldBitmask<int>(kRtcpReportCountFieldNumBits));
byte0 |= with.report_count;
break;
case RtcpPacketType::kSourceDescription:
OSP_UNIMPLEMENTED();
break;
case RtcpPacketType::kApplicationDefined:
case RtcpPacketType::kPayloadSpecific:
switch (with.subtype) {
case RtcpSubtype::kPictureLossIndicator:
case RtcpSubtype::kFeedback:
case RtcpSubtype::kReceiverLog:
byte0 |= static_cast<uint8_t>(with.subtype);
break;
// We should not be creating application or payload specific packets
// with an unknown or null subtype -- they will just be ignored.
case RtcpSubtype::kNull:
OSP_NOTREACHED();
}
break;
case RtcpPacketType::kExtendedReports:
break;
case RtcpPacketType::kNull:
OSP_NOTREACHED();
}
AppendField<uint8_t>(byte0, buffer);
AppendField<uint8_t>(static_cast<uint8_t>(packet_type), buffer);
// The size of the packet must be evenly divisible by the 32-bit word size.
OSP_CHECK_EQ(0, payload_size % sizeof(uint32_t));
AppendField<uint16_t>(payload_size / sizeof(uint32_t), buffer);
}
// static
std::optional<RtcpCommonHeader> RtcpCommonHeader::Parse(ByteView buffer) {
if (buffer.size() < kRtcpCommonHeaderSize) {
return std::nullopt;
}
const uint8_t byte0 = ConsumeField<uint8_t>(buffer);
if ((byte0 >> kRtcpReportCountFieldNumBits) !=
kRtcpRequiredVersionAndPaddingBits) {
return std::nullopt;
}
const uint8_t report_count_or_subtype =
byte0 & FieldBitmask<uint8_t>(kRtcpReportCountFieldNumBits);
const uint8_t byte1 = ConsumeField<uint8_t>(buffer);
if (!IsRtcpPacketType(byte1)) {
return std::nullopt;
}
// Optionally set `header.with.report_count` or `header.with.subtype`,
// depending on the packet type.
RtcpCommonHeader header;
header.packet_type = static_cast<RtcpPacketType>(byte1);
switch (header.packet_type) {
case RtcpPacketType::kSenderReport:
case RtcpPacketType::kReceiverReport:
header.with.report_count = report_count_or_subtype;
break;
case RtcpPacketType::kApplicationDefined:
case RtcpPacketType::kPayloadSpecific:
switch (static_cast<RtcpSubtype>(report_count_or_subtype)) {
case RtcpSubtype::kPictureLossIndicator:
case RtcpSubtype::kReceiverLog:
case RtcpSubtype::kFeedback:
header.with.subtype =
static_cast<RtcpSubtype>(report_count_or_subtype);
break;
default: // Unknown subtype.
header.with.subtype = RtcpSubtype::kNull;
break;
}
break;
default:
// Neither `header.with.report_count` nor `header.with.subtype` are used.
break;
}
header.payload_size =
static_cast<int>(ConsumeField<uint16_t>(buffer)) * sizeof(uint32_t);
return header;
}
RtcpReportBlock::RtcpReportBlock() = default;
RtcpReportBlock::~RtcpReportBlock() = default;
void RtcpReportBlock::AppendFields(ByteBuffer& buffer) const {
OSP_CHECK_GE(buffer.size(), kRtcpReportBlockSize);
AppendField<uint32_t>(ssrc, buffer);
OSP_CHECK_GE(packet_fraction_lost_numerator,
std::numeric_limits<uint8_t>::min());
OSP_CHECK_LE(packet_fraction_lost_numerator,
std::numeric_limits<uint8_t>::max());
OSP_CHECK_GE(cumulative_packets_lost, 0);
OSP_CHECK_LE(cumulative_packets_lost,
FieldBitmask<int>(kRtcpCumulativePacketsFieldNumBits));
AppendField<uint32_t>(
(static_cast<int>(packet_fraction_lost_numerator)
<< kRtcpCumulativePacketsFieldNumBits) |
(static_cast<int>(cumulative_packets_lost) &
FieldBitmask<uint32_t>(kRtcpCumulativePacketsFieldNumBits)),
buffer);
AppendField<uint32_t>(extended_high_sequence_number, buffer);
const int64_t jitter_ticks = jitter / RtpTimeDelta::FromTicks(1);
OSP_CHECK_GE(jitter_ticks, 0);
OSP_CHECK_LE(jitter_ticks, int64_t{std::numeric_limits<uint32_t>::max()});
AppendField<uint32_t>(jitter_ticks, buffer);
AppendField<uint32_t>(last_status_report_id, buffer);
const int64_t delay_ticks = delay_since_last_report.count();
OSP_CHECK_GE(delay_ticks, 0);
OSP_CHECK_LE(delay_ticks, int64_t{std::numeric_limits<uint32_t>::max()});
AppendField<uint32_t>(delay_ticks, buffer);
}
void RtcpReportBlock::SetPacketFractionLostNumerator(
int64_t num_apparently_sent,
int64_t num_received) {
if (num_apparently_sent <= 0) {
packet_fraction_lost_numerator = 0;
return;
}
// The following computes the fraction of packets lost as "one minus
// `num_received` divided by `num_apparently_sent`" and scales by 256 (the
// kPacketFractionLostDenominator). It's valid for `num_received` to be
// greater than `num_apparently_sent` in some cases (e.g., if duplicate
// packets were received from the network).
const int64_t numerator =
((num_apparently_sent - num_received) * kPacketFractionLostDenominator) /
num_apparently_sent;
// Since the value must be in the range [0,255], just do a saturate_cast
// to the uint8_t type to clamp.
packet_fraction_lost_numerator = saturate_cast<uint8_t>(numerator);
}
void RtcpReportBlock::SetCumulativePacketsLost(int64_t num_apparently_sent,
int64_t num_received) {
const int64_t num_lost = num_apparently_sent - num_received;
// Clamp to valid range supported by the wire format (and RTP spec).
//
// Note that `num_lost` can be negative if duplicate packets were received.
// The RFC spec (https://tools.ietf.org/html/rfc3550#section-6.4.1) states
// this should result in a clamped, "zero loss" value.
cumulative_packets_lost = static_cast<int>(
std::min(std::max<int64_t>(num_lost, 0),
FieldBitmask<int64_t>(kRtcpCumulativePacketsFieldNumBits)));
}
void RtcpReportBlock::SetDelaySinceLastReport(
Clock::duration local_clock_delay) {
// Clamp to valid range supported by the wire format (and RTP spec). The
// bounds checking is done in terms of Clock::duration, since doing the checks
// after the duration_cast may allow overflow to occur in the duration_cast
// math (well, only for unusually large inputs).
constexpr Delay kMaxValidReportedDelay(std::numeric_limits<uint32_t>::max());
constexpr auto kMaxValidLocalClockDelay =
Clock::to_duration(kMaxValidReportedDelay);
if (local_clock_delay > kMaxValidLocalClockDelay) {
delay_since_last_report = kMaxValidReportedDelay;
return;
}
if (local_clock_delay <= Clock::duration::zero()) {
delay_since_last_report = Delay::zero();
return;
}
// If this point is reached, then the `local_clock_delay` is representable as
// a Delay within the valid range.
delay_since_last_report =
std::chrono::duration_cast<Delay>(local_clock_delay);
}
// static
std::optional<RtcpReportBlock> RtcpReportBlock::ParseOne(ByteView buffer,
int report_count,
Ssrc ssrc) {
if (static_cast<int>(buffer.size()) < (kRtcpReportBlockSize * report_count)) {
return std::nullopt;
}
std::optional<RtcpReportBlock> result;
for (int block = 0; block < report_count; ++block) {
if (ConsumeField<uint32_t>(buffer) != ssrc) {
// Skip-over report block meant for some other recipient.
buffer = buffer.subspan(kRtcpReportBlockSize - sizeof(uint32_t));
continue;
}
RtcpReportBlock& report_block = result.emplace();
report_block.ssrc = ssrc;
const auto second_word = ConsumeField<uint32_t>(buffer);
report_block.packet_fraction_lost_numerator =
second_word >> kRtcpCumulativePacketsFieldNumBits;
report_block.cumulative_packets_lost =
second_word &
FieldBitmask<uint32_t>(kRtcpCumulativePacketsFieldNumBits);
report_block.extended_high_sequence_number = ConsumeField<uint32_t>(buffer);
report_block.jitter =
RtpTimeDelta::FromTicks(ConsumeField<uint32_t>(buffer));
report_block.last_status_report_id = ConsumeField<uint32_t>(buffer);
report_block.delay_since_last_report =
RtcpReportBlock::Delay(ConsumeField<uint32_t>(buffer));
}
return result;
}
RtcpSenderReport::RtcpSenderReport() = default;
RtcpSenderReport::~RtcpSenderReport() = default;
} // namespace openscreen::cast

View file

@ -0,0 +1,203 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_RTCP_COMMON_H_
#define CAST_STREAMING_IMPL_RTCP_COMMON_H_
#include <stdint.h>
#include <optional>
#include <tuple>
#include <vector>
#include "cast/streaming/impl/ntp_time.h"
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/impl/statistics_common.h"
#include "cast/streaming/public/frame_id.h"
#include "cast/streaming/rtp_time.h"
#include "cast/streaming/ssrc.h"
#include "platform/base/span.h"
namespace openscreen::cast {
struct RtcpCommonHeader {
RtcpCommonHeader();
~RtcpCommonHeader();
RtcpPacketType packet_type = RtcpPacketType::kNull;
union {
// The number of report blocks if `packet_type` is kSenderReport or
// kReceiverReport.
int report_count;
// Indicates the type of an application-defined message if `packet_type` is
// kApplicationDefined or kPayloadSpecific.
RtcpSubtype subtype;
// Otherwise, not used.
} with{0};
// The size (in bytes) of the RTCP packet, not including the header.
int payload_size = 0;
// Serializes this header into the first `kRtcpCommonHeaderSize` bytes of the
// given `buffer` and adjusts `buffer` to point to the first byte after it.
void AppendFields(ByteBuffer& buffer) const;
// Parse from the 4-byte wire format in `buffer`. Returns nullopt if the data
// is corrupt.
static std::optional<RtcpCommonHeader> Parse(ByteView buffer);
};
// The middle 32-bits of the 64-bit NtpTimestamp field from the Sender Reports.
// This is used as an opaque identifier that the Receiver will use in its
// reports to refer to specific previous Sender Reports.
using StatusReportId = uint32_t;
constexpr StatusReportId ToStatusReportId(NtpTimestamp ntp_timestamp) {
return static_cast<uint32_t>(ntp_timestamp >> 16);
}
// One of these is optionally included with a Sender Report or a Receiver
// Report. See: https://tools.ietf.org/html/rfc3550#section-6.4.1
struct RtcpReportBlock {
RtcpReportBlock();
~RtcpReportBlock();
// The intended recipient of this report block.
Ssrc ssrc = 0;
// The fraction of RTP packets lost since the last report, specified as a
// variable numerator and fixed denominator. The numerator will always be in
// the range [0,255] since, semantically:
//
// a. Negative values are impossible.
// b. Values greater than 255 would indicate 100% packet loss, and so a
// report block would not be generated in the first place.
int packet_fraction_lost_numerator = 0;
static constexpr int kPacketFractionLostDenominator = 256;
// The total number of RTP packets lost since the start of the session. This
// value will always be in the range [0,2^24-1], as the wire format only
// provides 24 bits; so, wrap-around is possible.
int cumulative_packets_lost = 0;
// The highest sequence number received in any RTP packet. Wrap-around is
// possible.
uint32_t extended_high_sequence_number = 0;
// An estimate of the recent variance in RTP packet arrival times.
RtpTimeDelta jitter;
// The last Status Report received.
StatusReportId last_status_report_id{};
// The delay between when the peer received the most-recent Status Report and
// when this report was sent. The timebase is 65536 ticks per second and,
// because of the wire format, this value will always be in the range
// [0,65536) seconds.
using Delay = std::chrono::duration<int64_t, std::ratio<1, 65536>>;
Delay delay_since_last_report{};
// Convenience helper to compute/assign the `packet_fraction_lost_numerator`,
// based on the `num_apparently_sent` and `num_received` packet counts since
// the last report was sent.
void SetPacketFractionLostNumerator(int64_t num_apparently_sent,
int64_t num_received);
// Convenience helper to compute/assign the `cumulative_packets_lost`, based
// on the `num_apparently_sent` and `num_received` packet counts since the
// start of the entire session.
void SetCumulativePacketsLost(int64_t num_apparently_sent,
int64_t num_received);
// Convenience helper to convert the given `local_clock_delay` to the
// RtcpReportBlock::Delay timebase, then clamp and assign it to
// `delay_since_last_report`.
void SetDelaySinceLastReport(Clock::duration local_clock_delay);
// Serializes this report block in the first `kRtcpReportBlockSize` bytes of
// the given `buffer` and adjusts `buffer` to point to the first byte after
// it.
void AppendFields(ByteBuffer& buffer) const;
// Scans the wire-format report blocks in `buffer`, searching for one with the
// matching `ssrc` and, if found, returns the parse result. Returns nullopt if
// the data is corrupt or no report block with the matching SSRC was found.
static std::optional<RtcpReportBlock> ParseOne(ByteView buffer,
int report_count,
Ssrc ssrc);
};
struct RtcpSenderReport {
RtcpSenderReport();
~RtcpSenderReport();
// The point-in-time at which this report was sent, according to both: 1) the
// common reference clock shared by all RTP streams; 2) the RTP timestamp on
// the media capture/playout timeline. Together, these are used by a Receiver
// to achieve A/V synchronization across RTP streams for playout.
Clock::time_point reference_time{};
RtpTimeTicks rtp_timestamp;
// The total number of RTP packets transmitted since the start of the session
// (wrap-around is possible).
uint32_t send_packet_count = 0;
// The total number of payload bytes transmitted in RTP packets since the
// start of the session (wrap-around is possible).
uint32_t send_octet_count = 0;
// The report block, if present. While the RTCP spec allows for zero or
// multiple reports, Cast Streaming only uses zero or one.
std::optional<RtcpReportBlock> report_block;
};
// A pair of IDs that refers to a specific missing packet within a frame. If
// `packet_id` is kAllPacketsLost, then it represents all the packets of a
// frame.
struct PacketNack {
FrameId frame_id;
FramePacketId packet_id;
constexpr bool operator==(const PacketNack& other) const {
return frame_id == other.frame_id && packet_id == other.packet_id;
}
constexpr bool operator!=(const PacketNack& other) const {
return frame_id != other.frame_id || packet_id != other.packet_id;
}
constexpr bool operator<(const PacketNack& other) const {
return (frame_id < other.frame_id) ||
(frame_id == other.frame_id && packet_id < other.packet_id);
}
};
// Statistics events sent from the receiver over RTCP.
struct RtcpReceiverEventLogMessage {
// The statistics event type, may be either a receiver side frame event or
// packet event.
StatisticsEvent::Type type;
// The time at which this event occurred.
Clock::time_point timestamp;
// Only set for frame played out events.
// If this value is zero the frame is rendered on time.
// If this value is positive it means the frame is rendered late.
// If this value is negative it means the frame is rendered early.
Clock::duration delay;
// Only set for packet events.
// The ID of the packet associated with this event.
FramePacketId packet_id;
};
struct RtcpReceiverFrameLogMessage {
RtpTimeTicks rtp_timestamp;
std::vector<RtcpReceiverEventLogMessage> messages;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_RTCP_COMMON_H_

View file

@ -0,0 +1,25 @@
// Copyright 2019 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/rtcp_session.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
RtcpSession::RtcpSession(Ssrc sender_ssrc,
Ssrc receiver_ssrc,
Clock::time_point start_time)
: sender_ssrc_(sender_ssrc),
receiver_ssrc_(receiver_ssrc),
start_time_(start_time),
ntp_converter_(start_time) {
OSP_CHECK_NE(sender_ssrc_, kNullSsrc);
OSP_CHECK_NE(receiver_ssrc_, kNullSsrc);
OSP_CHECK_NE(sender_ssrc_, receiver_ssrc_);
}
RtcpSession::~RtcpSession() = default;
} // namespace openscreen::cast

View file

@ -0,0 +1,43 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_RTCP_SESSION_H_
#define CAST_STREAMING_IMPL_RTCP_SESSION_H_
#include "cast/streaming/impl/ntp_time.h"
#include "cast/streaming/ssrc.h"
namespace openscreen::cast {
// Session-level configuration and shared components for the RTCP messaging
// associated with a single Cast RTP stream. Multiple packet serialization and
// parsing components share a single RtcpSession instance for data consistency.
class RtcpSession {
public:
// `start_time` should be the current time, as it is used by NtpTimeConverter
// to set a fixed reference point between the local Clock and current "real
// world" wall time.
RtcpSession(Ssrc sender_ssrc,
Ssrc receiver_ssrc,
Clock::time_point start_time);
~RtcpSession();
Ssrc sender_ssrc() const { return sender_ssrc_; }
Ssrc receiver_ssrc() const { return receiver_ssrc_; }
const NtpTimeConverter& ntp_converter() const { return ntp_converter_; }
Clock::time_point start_time() const { return start_time_; }
private:
const Ssrc sender_ssrc_;
const Ssrc receiver_ssrc_;
Clock::time_point start_time_;
// Translates between system time (internal format) and NTP (wire format).
NtpTimeConverter ntp_converter_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_RTCP_SESSION_H_

View file

@ -0,0 +1,113 @@
// Copyright 2019 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/rtp_defines.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
RtpPayloadType GetPayloadType(AudioCodec codec, bool use_android_rtp_hack) {
if (use_android_rtp_hack) {
return RtpPayloadType::kAudioHackForAndroidTV;
}
switch (codec) {
case AudioCodec::kAac:
return RtpPayloadType::kAudioAac;
case AudioCodec::kOpus:
return RtpPayloadType::kAudioOpus;
case AudioCodec::kNotSpecified:
return RtpPayloadType::kAudioVarious;
default:
OSP_NOTREACHED();
}
}
RtpPayloadType GetPayloadType(VideoCodec codec, bool use_android_rtp_hack) {
if (use_android_rtp_hack) {
return RtpPayloadType::kVideoHackForAndroidTV;
}
switch (codec) {
// VP8 and VP9 share the same payload type.
case VideoCodec::kVp9:
case VideoCodec::kVp8:
return RtpPayloadType::kVideoVp8;
// H264 and HEVC/H265 share the same payload type.
case VideoCodec::kHevc: // fallthrough
case VideoCodec::kH264:
return RtpPayloadType::kVideoH264;
case VideoCodec::kAv1:
return RtpPayloadType::kVideoAv1;
case VideoCodec::kNotSpecified:
return RtpPayloadType::kVideoVarious;
default:
OSP_NOTREACHED();
}
}
StreamType ToStreamType(RtpPayloadType type, bool use_android_rtp_hack) {
if (use_android_rtp_hack) {
if (type == RtpPayloadType::kAudioHackForAndroidTV) {
return StreamType::kAudio;
}
if (type == RtpPayloadType::kVideoHackForAndroidTV) {
return StreamType::kVideo;
}
}
if (RtpPayloadType::kAudioFirst <= type &&
type <= RtpPayloadType::kAudioLast) {
return StreamType::kAudio;
}
if (RtpPayloadType::kVideoFirst <= type &&
type <= RtpPayloadType::kVideoLast) {
return StreamType::kVideo;
}
return StreamType::kUnknown;
}
bool IsRtpPayloadType(uint8_t raw_byte) {
switch (static_cast<RtpPayloadType>(raw_byte)) {
case RtpPayloadType::kAudioOpus:
case RtpPayloadType::kAudioAac:
case RtpPayloadType::kAudioPcm16:
case RtpPayloadType::kAudioVarious:
case RtpPayloadType::kVideoVp8:
case RtpPayloadType::kVideoH264:
case RtpPayloadType::kVideoVp9:
case RtpPayloadType::kVideoAv1:
case RtpPayloadType::kVideoVarious:
case RtpPayloadType::kAudioHackForAndroidTV:
// Note: RtpPayloadType::kVideoHackForAndroidTV has the same value as
// kAudioOpus.
return true;
case RtpPayloadType::kNull:
break;
}
return false;
}
bool IsRtcpPacketType(uint8_t raw_byte) {
switch (static_cast<RtcpPacketType>(raw_byte)) {
case RtcpPacketType::kSenderReport:
case RtcpPacketType::kReceiverReport:
case RtcpPacketType::kSourceDescription:
case RtcpPacketType::kApplicationDefined:
case RtcpPacketType::kPayloadSpecific:
case RtcpPacketType::kExtendedReports:
return true;
case RtcpPacketType::kNull:
break;
}
return false;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,382 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_RTP_DEFINES_H_
#define CAST_STREAMING_IMPL_RTP_DEFINES_H_
#include <stdint.h>
#include "cast/streaming/public/constants.h"
namespace openscreen::cast {
// Note: Cast Streaming uses a subset of the messages in the RTP/RTCP
// specification, but also adds some of its own extensions. See:
// https://tools.ietf.org/html/rfc3550
// Uniquely identifies one packet within a frame. These are sequence numbers,
// starting at 0. Each Cast RTP packet also includes the "last ID" so that a
// receiver always knows the range of valid FramePacketIds for a given frame.
using FramePacketId = uint16_t;
// A special FramePacketId value meant to represent "all packets lost" in Cast
// RTCP Feedback messages.
inline constexpr FramePacketId kAllPacketsLost = 0xffff;
inline constexpr FramePacketId kMaxAllowedFramePacketId = kAllPacketsLost - 1;
// The maximum size of any RTP or RTCP packet, in bytes. The calculation below
// is: Standard Ethernet MTU bytes minus IP header bytes minus UDP header bytes.
// The remainder is available for RTP/RTCP packet data (header + payload).
//
// A nice explanation of this: https://jvns.ca/blog/2017/02/07/mtu/
//
// Constants are provided here for UDP over IPv4 and IPv6 on Ethernet. Other
// transports and network mediums will need additional consideration, alternate
// calculations. Note that MTU is dynamic, depending on the path the packets
// take between two endpoints (the 1500 here is just a commonly-used value for
// LAN Ethernet).
inline constexpr int kMaxRtpPacketSizeForIpv4UdpOnEthernet = 1500 - 20 - 8;
inline constexpr int kMaxRtpPacketSizeForIpv6UdpOnEthernet = 1500 - 40 - 8;
// The Cast RTP packet header:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ^
// |V=2|P|X| CC=0 |M| PT | sequence number | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+RTP
// + RTP timestamp |Spec
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
// + synchronization source (SSRC) identifier | v
// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
// |K|R| EXT count | FID | PID | ^
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+Cast
// | Max PID | optional fields, extensions, Spec
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ then payload... v
//
// Byte 0: Version 2, no padding, no RTP extensions, no CSRCs.
// Byte 1: Marker bit indicates whether this is the last packet, followed by a
// 7-bit payload type.
// Byte 12: Key Frame bit, followed by "RFID will be provided" bit, followed by
// 6 bits specifying the number of extensions that will be provided.
// The minimum-possible valid size of a Cast RTP packet (i.e., no optional
// fields, extensions, nor payload).
inline constexpr int kRtpPacketMinValidSize = 18;
// All Cast RTP packets must carry the version 2 flag, not use padding, not use
// RTP extensions, and have zero CSRCs.
inline constexpr uint8_t kRtpRequiredFirstByte = 0b10000000;
// Bitmasks to isolate fields within byte 2 of the Cast RTP header.
inline constexpr uint8_t kRtpMarkerBitMask = 0b10000000;
inline constexpr uint8_t kRtpPayloadTypeMask = 0b01111111;
// Describes the content being transported over RTP streams. These are Cast
// Streaming specific assignments, within the "dynamic" range provided by
// IANA. Note that this Cast Streaming implementation does not manipulate
// already-encoded data, and so these payload types are only "informative" in
// purpose and can be used to check for corruption while parsing packets.
enum class RtpPayloadType : uint8_t {
kNull = 0,
kAudioFirst = 96,
kAudioOpus = 96,
kAudioAac = 97,
kAudioPcm16 = 98,
kAudioVarious = 99, // Codec being used is not fixed.
kAudioLast = kAudioVarious,
kVideoFirst = 100,
kVideoVp8 = 100,
kVideoH264 = 101,
kVideoVarious = 102, // Codec being used is not fixed.
kVideoVp9 = 103,
kVideoAv1 = 104,
kVideoLast = kVideoAv1,
// Some AndroidTV receivers require the payload type for audio to be 127, and
// video to be 96; regardless of the codecs actually being used. This is
// definitely out-of-spec, and inconsistent with the audio versus video range
// of values, but must be taken into account for backwards-compatibility.
kAudioHackForAndroidTV = 127,
kVideoHackForAndroidTV = 96,
};
// Returns the stream type associated with the RTP payload type.
StreamType ToStreamType(RtpPayloadType type, bool use_android_rtp_hack);
// Setting `use_android_rtp_hack` to true means that we match the legacy Chrome
// sender's behavior of always sending the audio and video hacks for AndroidTV,
// as some legacy android receivers require these.
// TODO(issuetracker.google.com/184438154): we need to figure out what receivers
// need this still, if any. The hack should be removed when possible.
RtpPayloadType GetPayloadType(AudioCodec codec, bool use_android_rtp_hack);
RtpPayloadType GetPayloadType(VideoCodec codec, bool use_android_rtp_hack);
// Returns true if the `raw_byte` can be type-casted to a RtpPayloadType, and is
// also not RtpPayloadType::kNull. The caller should mask the byte, to select
// the lower 7 bits, if applicable.
bool IsRtpPayloadType(uint8_t raw_byte);
// Bitmasks to isolate fields within byte 12 of the Cast RTP header.
inline constexpr uint8_t kRtpKeyFrameBitMask = 0b10000000;
inline constexpr uint8_t kRtpHasReferenceFrameIdBitMask = 0b01000000;
inline constexpr uint8_t kRtpExtensionCountMask = 0b00111111;
// Cast extensions. This implementation supports only the Adaptive Latency
// extension, and ignores all others:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | TYPE = 1 | Ext data SIZE = 2 |Playout Delay (unsigned millis)|
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// The Adaptive Latency extension permits changing the fixed end-to-end playout
// delay of a single RTP stream.
inline constexpr uint8_t kAdaptiveLatencyRtpExtensionType = 1;
inline constexpr int kNumExtensionDataSizeFieldBits = 10;
// RTCP Common Header:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |V=2|P|RC/Subtyp| Packet Type | Length |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpCommonHeaderSize = 4;
// All RTCP packets must carry the version 2 flag and not use padding.
inline constexpr uint8_t kRtcpRequiredVersionAndPaddingBits = 0b100;
inline constexpr int kRtcpReportCountFieldNumBits = 5;
// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml
enum class RtcpPacketType : uint8_t {
kNull = 0,
kSenderReport = 200,
kReceiverReport = 201,
kSourceDescription = 202,
kApplicationDefined = 204,
kPayloadSpecific = 206,
kExtendedReports = 207,
};
// Returns true if the `raw_byte` can be type-casted to a RtcpPacketType, and is
// also not RtcpPacketType::kNull.
bool IsRtcpPacketType(uint8_t raw_byte);
// Supported subtype values in the RTCP Common Header when the packet type is
// kApplicationDefined or kPayloadSpecific.
enum class RtcpSubtype : uint8_t {
kNull = 0,
kPictureLossIndicator = 1,
kReceiverLog = 2,
kFeedback = 15,
};
// RTCP Sender Report:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Sender |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | |
// | NTP Timestamp |
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | RTP Timestamp |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Sender's Packet Count |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Sender's Octet Count |
// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
// ...Followed by zero or more "Report Blocks"...
inline constexpr int kRtcpSenderReportSize = 24;
// RTCP Receiver Report:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Receiver |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// ...Followed by zero or more "Report Blocks"...
inline constexpr int kRtcpReceiverReportSize = 4;
// RTCP Report Block. For Cast Streaming, zero or one of these accompanies a
// Sender or Receiver Report, which is different than the RTCP spec (which
// allows zero or more).
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | "To" SSRC |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Fraction Lost | Cumulative Number of Packets Lost |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | [32-bit extended] Highest Sequence Number Received |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Interarrival Jitter Mean Absolute Deviation (in RTP Timebase) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Middle 32-bits of NTP Timestamp from last Sender Report |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Delay since last Sender Report (1/65536 sec timebase) |
// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
inline constexpr int kRtcpReportBlockSize = 24;
inline constexpr int kRtcpCumulativePacketsFieldNumBits = 24;
// Cast Feedback Message:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Receiver |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Sender |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Unique identifier 'C' 'A' 'S' 'T' |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | CkPt Frame ID | # Loss Fields | Current Playout Delay (msec) |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpFeedbackHeaderSize = 16;
inline constexpr uint32_t kRtcpCastIdentifierWord =
(uint32_t{'C'} << 24) | (uint32_t{'A'} << 16) | (uint32_t{'S'} << 8) |
uint32_t{'T'};
//
// "Checkpoint Frame ID" indicates that all frames prior to and including this
// one have been fully received. Unfortunately, the Frame ID is truncated to its
// lower 8 bits in the packet, and 8 bits is not really enough: If a RTCP packet
// is received very late (e.g., more than 1.2 seconds late for 100 FPS audio),
// the Checkpoint Frame ID here will be mis-interpreted as representing a
// higher-numbered frame than what was intended. This could make the sender's
// tracking of "completely received" frames inconsistent, and Cast Streaming
// would live-lock. However, this design issue has been baked into the spec and
// millions of deployments over several years, and so there's no changing it
// now. See kMaxUnackedFrames in constants.h.
//
// "# Loss fields" indicates the number of packet-level NACK words, 0 to 255:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | w/in Frame ID | Lost Frame Packet ID | PID BitVector |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpFeedbackLossFieldSize = 4;
//
// "Within Frame ID" is a truncated-to-8-bits frame ID field and, when
// bit-expanded should always be interpreted to represent a value greater than
// the Checkpoint Frame ID. "Lost Frame Packet ID" is either a specific packet
// (within the frame) that has not been received, or kAllPacketsLost to indicate
// none the packets for the frame have been received yet. In the former case,
// "PID Bit Vector" then represents which of the next 8 packets are also
// missing.
//
// Finally, all of the above is optionally followed by a frame-level ACK bit
// vector:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Unique identifier 'C' 'S' 'T' '2' |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |Feedback Count | # BVectOctets | ACK BitVect (2 to 254 bytes)...
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ → zero-padded to word boundary
inline constexpr int kRtcpFeedbackAckHeaderSize = 6;
inline constexpr uint32_t kRtcpCst2IdentifierWord =
(uint32_t{'C'} << 24) | (uint32_t{'S'} << 16) | (uint32_t{'T'} << 8) |
uint32_t{'2'};
inline constexpr int kRtcpMinAckBitVectorOctets = 2;
inline constexpr int kRtcpMaxAckBitVectorOctets = 254;
//
// "Feedback Count" is a wrap-around counter indicating the number of Cast
// Feedbacks that have been sent before this one. "# Bit Vector Octets"
// indicates the number of bytes of ACK bit vector following. Cast RTCP
// alignment/padding requirements (to 4-byte boundaries) dictates the following
// rules for generating the ACK bit vector:
//
// 1. There must be at least 2 bytes of ACK bit vector, if only to pad the 6
// byte header with two more bytes.
// 2. If more than 2 bytes are needed, they must be added 4 at a time to
// maintain the 4-byte alignment of the overall RTCP packet.
// 3. The total number of octets may not exceed 255; but, because of #2, 254
// is effectively the limit.
// 4. The first bit in the first octet represents "Checkpoint Frame ID" plus
// two. "Plus two" and not "plus one" because otherwise the "Checkpoint
// Frame ID" should have been a greater value!
// RTCP Extended Report:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Report Author |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpExtendedReportHeaderSize = 4;
//
// ...followed by zero or more Blocks:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Block Type | Reserved = 0 | Block Length |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | ..."Block Length" words of report data... |
// + +
// + +
inline constexpr int kRtcpExtendedReportBlockHeaderSize = 4;
//
// Cast Streaming only uses Receiver Reference Time Reports:
// https://tools.ietf.org/html/rfc3611#section-4.4. So, the entire block would
// be:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Block Type=4 | Reserved = 0 | Block Length = 2 |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | NTP Timestamp |
// | |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr uint8_t kRtcpReceiverReferenceTimeReportBlockType = 4;
inline constexpr int kRtcpReceiverReferenceTimeReportBlockSize = 8;
// Cast Picture Loss Indicator Message:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Receiver |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | SSRC of Sender |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpPictureLossIndicatorHeaderSize = 8;
// The Cast Receiver RTCP frame log message is an application specific
// extension that contains receiver side statistics about the Receiver Session.
// The message format is:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | RTP Timestamp |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Event Count | Event Timestamp |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpReceiverFrameLogMessageHeaderSize = 8;
//
// Followed by a list of zero or more event blocks:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Delay Delta or Packet ID | Type | Event Timestamp |
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
inline constexpr int kRtcpReceiverFrameLogMessageBlockSize = 4;
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_RTP_DEFINES_H_

View file

@ -0,0 +1,133 @@
// Copyright 2019 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/rtp_packetizer.h"
#include <algorithm>
#include <limits>
#include <random>
#include "cast/streaming/impl/packet_util.h"
#include "platform/api/time.h"
#include "util/big_endian.h"
#include "util/integer_division.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
namespace {
// Returns a random sequence number to start with. The reason for using a random
// number instead of zero is unclear, but this has existed both in several
// versions of the Cast Streaming spec and in other implementations for many
// years.
uint16_t GenerateRandomSequenceNumberStart() {
// Use a statically-allocated generator, instantiated upon first use, and
// seeded with the current time tick count. This generator was chosen because
// it is light-weight and does not need to produce unguessable (nor
// crypto-secure) values.
static std::minstd_rand generator(static_cast<std::minstd_rand::result_type>(
Clock::now().time_since_epoch().count()));
return std::uniform_int_distribution<uint16_t>()(generator);
}
} // namespace
RtpPacketizer::RtpPacketizer(RtpPayloadType payload_type,
Ssrc sender_ssrc,
int max_packet_size)
: payload_type_7bits_(static_cast<uint8_t>(payload_type)),
sender_ssrc_(sender_ssrc),
max_packet_size_(max_packet_size),
sequence_number_(GenerateRandomSequenceNumberStart()) {
OSP_CHECK(IsRtpPayloadType(payload_type_7bits_));
OSP_CHECK_GT(max_packet_size_, kMaxRtpHeaderSize);
}
RtpPacketizer::~RtpPacketizer() = default;
ByteBuffer RtpPacketizer::GeneratePacket(const EncryptedFrame& frame,
FramePacketId packet_id,
ByteBuffer buffer) {
OSP_CHECK_GE(static_cast<int>(buffer.size()), max_packet_size_);
const int num_packets = ComputeNumberOfPackets(frame);
OSP_CHECK_GT(num_packets, 0);
OSP_CHECK_LT(int{packet_id}, num_packets);
const bool is_last_packet = int{packet_id} == (num_packets - 1);
// Compute the size of this packet, which is the number of bytes of header
// plus the number of bytes of payload. Note that the optional Adaptive
// Latency information is only added to the first packet.
int packet_size = kBaseRtpHeaderSize;
const bool include_adaptive_latency_change =
(packet_id == 0 &&
frame.new_playout_delay > std::chrono::milliseconds(0));
if (include_adaptive_latency_change) {
OSP_CHECK_LE(frame.new_playout_delay.count(),
int{std::numeric_limits<uint16_t>::max()});
packet_size += kAdaptiveLatencyHeaderSize;
}
int data_chunk_size = max_payload_size();
const int data_chunk_start = data_chunk_size * int{packet_id};
if (is_last_packet) {
data_chunk_size = static_cast<int>(frame.data.size()) - data_chunk_start;
}
packet_size += data_chunk_size;
OSP_CHECK_LE(packet_size, max_packet_size_);
const ByteBuffer packet(buffer.data(), packet_size);
// RTP Header.
AppendField<uint8_t>(kRtpRequiredFirstByte, buffer);
AppendField<uint8_t>(
(is_last_packet ? kRtpMarkerBitMask : 0) | payload_type_7bits_, buffer);
AppendField<uint16_t>(sequence_number_++, buffer);
AppendField<uint32_t>(frame.rtp_timestamp.lower_32_bits(), buffer);
AppendField<uint32_t>(sender_ssrc_, buffer);
// Cast Header.
AppendField<uint8_t>(
((frame.dependency == EncodedFrame::Dependency::kKeyFrame)
? kRtpKeyFrameBitMask
: 0) |
kRtpHasReferenceFrameIdBitMask |
(include_adaptive_latency_change ? 1 : 0),
buffer);
AppendField<uint8_t>(frame.frame_id.lower_8_bits(), buffer);
AppendField<uint16_t>(packet_id, buffer);
AppendField<uint16_t>(num_packets - 1, buffer);
AppendField<uint8_t>(frame.referenced_frame_id.lower_8_bits(), buffer);
// Extension of Cast Header for Adaptive Latency change.
if (include_adaptive_latency_change) {
AppendField<uint16_t>(
(kAdaptiveLatencyRtpExtensionType << kNumExtensionDataSizeFieldBits) |
sizeof(uint16_t),
buffer);
AppendField<uint16_t>(frame.new_playout_delay.count(), buffer);
}
// Copy the encrypted payload data into the packet.
auto data_chunk = frame.data.subspan(data_chunk_start, data_chunk_size);
std::copy(data_chunk.begin(), data_chunk.end(), buffer.data());
return packet;
}
int RtpPacketizer::ComputeNumberOfPackets(const EncryptedFrame& frame) const {
// The total number of packets is computed by assuming the payload will be
// split-up across as few packets as possible.
int num_packets = DividePositivesRoundingUp(
static_cast<int>(frame.data.size()), max_payload_size());
// Edge case: There must always be at least one packet, even when there are no
// payload bytes. Some audio codecs, for example, use zero bytes to represent
// a period of silence.
num_packets = std::max(1, num_packets);
// Ensure that the entire range of FramePacketIds can be represented.
return num_packets <= int{kMaxAllowedFramePacketId} ? num_packets : -1;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,79 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_RTP_PACKETIZER_H_
#define CAST_STREAMING_IMPL_RTP_PACKETIZER_H_
#include <stdint.h>
#include "cast/streaming/impl/frame_crypto.h"
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/ssrc.h"
#include "platform/base/span.h"
namespace openscreen::cast {
// Transforms a logical sequence of EncryptedFrames into RTP packets for
// transmission. A single instance of RtpPacketizer should be used for all the
// frames in a Cast RTP stream having the same SSRC.
class RtpPacketizer {
public:
// `payload_type` describes the type of the media content for the RTP stream
// from the sender having the given `sender_ssrc`.
//
// The `max_packet_size` argument depends on the optimal over-the-wire size of
// packets for the network medium being used. See discussion in rtp_defines.h
// for further info.
RtpPacketizer(RtpPayloadType payload_type,
Ssrc sender_ssrc,
int max_packet_size);
~RtpPacketizer();
// Wire-format one of the RTP packets for the given frame, which must only be
// transmitted once. This method should be called in the same sequence that
// packets will be transmitted. This also means that, if a packet needs to be
// re-transmitted, this method should be called to generate it again. Returns
// the subspan of `buffer` that contains the packet. `buffer` must be at least
// as large as the `max_packet_size` passed to the constructor.
ByteBuffer GeneratePacket(const EncryptedFrame& frame,
FramePacketId packet_id,
ByteBuffer buffer);
// Given `frame`, compute the total number of packets over which the whole
// frame will be split-up. Returns -1 if the frame is too large and cannot be
// packetized.
int ComputeNumberOfPackets(const EncryptedFrame& frame) const;
// See rtp_defines.h for wire-format diagram.
static constexpr int kBaseRtpHeaderSize =
// Plus one byte, because this implementation always includes the 8-bit
// Reference Frame ID field.
kRtpPacketMinValidSize + 1;
static constexpr int kAdaptiveLatencyHeaderSize = 4;
static constexpr int kMaxRtpHeaderSize =
kBaseRtpHeaderSize + kAdaptiveLatencyHeaderSize;
private:
int max_payload_size() const {
// Start with the configured max packet size, then subtract reserved space
// for packet header fields. The rest can be allocated to the payload.
return max_packet_size_ - kMaxRtpHeaderSize;
}
// The validated ctor RtpPayloadType arg, in wire-format form.
const uint8_t payload_type_7bits_;
const Ssrc sender_ssrc_;
const int max_packet_size_;
// Incremented each time GeneratePacket() is called. Every packet, even those
// re-transmitted, must have different sequence numbers (within wrap-around
// concerns) per the RTP spec.
uint16_t sequence_number_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_RTP_PACKETIZER_H_

View file

@ -0,0 +1,686 @@
// Copyright 2026 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/sender_impl.h"
#include <algorithm>
#include <chrono>
#include <ratio>
#include <utility>
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/impl/statistics_common.h"
#include "cast/streaming/public/session_config.h"
#include "platform/base/trivial_clock_traits.h"
#include "util/chrono_helpers.h"
#include "util/osp_logging.h"
#include "util/std_util.h"
#include "util/string_util.h"
#include "util/trace_logging.h"
namespace openscreen::cast {
namespace {
// The minimum amount of media the Sender keeps in-flight, regardless of the
// measured network round-trip time. This keeps the encoder pipeline flowing on
// low-latency networks (roughly two video frames at 30 FPS). See
// crbug.com/498035450.
constexpr Clock::duration kMinSenderInFlight =
Clock::to_duration(milliseconds(66));
} // namespace
using clock_operators::operator<<;
SenderImpl::SenderImpl(Environment& environment,
SenderPacketRouter& packet_router,
SessionConfig config,
RtpPayloadType rtp_payload_type)
: config_(config),
packet_router_(packet_router),
rtcp_session_(config.sender_ssrc,
config.receiver_ssrc,
environment.now()),
rtcp_parser_(rtcp_session_, *this),
sender_report_builder_(rtcp_session_),
rtp_packetizer_(rtp_payload_type,
config.sender_ssrc,
packet_router_->max_packet_size()),
rtp_timebase_(config.rtp_timebase),
crypto_(config.aes_secret_key, config.aes_iv_mask),
statistics_dispatcher_(environment),
target_playout_delay_(config.target_playout_delay) {
OSP_CHECK_NE(rtcp_session_.sender_ssrc(), rtcp_session_.receiver_ssrc());
OSP_CHECK_GT(rtp_timebase_, 0);
OSP_CHECK_GT(target_playout_delay_, milliseconds::zero());
pending_sender_report_.reference_time = SenderPacketRouter::kNever;
packet_router_->OnSenderCreated(rtcp_session_.receiver_ssrc(), this);
}
SenderImpl::~SenderImpl() {
packet_router_->OnSenderDestroyed(rtcp_session_.receiver_ssrc());
}
void SenderImpl::SetObserver(openscreen::cast::Sender::Observer* observer) {
OSP_CHECK_NE(observer_, observer);
observer_ = observer;
}
size_t SenderImpl::GetInFlightFrameCount() const {
return num_frames_in_flight_;
}
Clock::duration SenderImpl::GetInFlightMediaDuration(
RtpTimeTicks next_frame_rtp_timestamp) const {
if (num_frames_in_flight_ == 0) {
return Clock::duration::zero(); // No frames are currently in-flight.
}
const PendingFrameSlot& oldest_slot = get_slot_for(checkpoint_frame_id_ + 1);
// Note: The oldest slot's frame cannot have been canceled because the
// protocol does not allow ACK'ing this particular frame without also moving
// the checkpoint forward. See "CST2 feedback" discussion in rtp_defines.h.
OSP_CHECK(oldest_slot.is_active_for_frame(checkpoint_frame_id_ + 1));
return (next_frame_rtp_timestamp - oldest_slot.frame->rtp_timestamp)
.ToDuration<Clock::duration>(rtp_timebase_);
}
Clock::duration SenderImpl::GetMaxInFlightMediaDuration() const {
// The Sender keeps only enough media in-flight to drive the loss-detection
// and retransmit feedback loop, which takes on the order of two network
// round-trips (one to detect a loss via NACK, one to retransmit). A small
// floor (`kMinSenderInFlight`) keeps the encoder pipeline flowing on
// low-latency networks where 2*RTT is negligible.
//
// The result is capped at a third of the playout delay window so that the
// majority of the budget is reserved for the Receiver, which needs buffer to
// absorb NACK retransmissions. Bounding the Sender this way also makes it
// drop frames earlier during congestion (saving bandwidth and CPU) rather
// than over-buffering. See crbug.com/498035450.
//
// Note: the upper bound is held at or above `kMinSenderInFlight` so the
// std::clamp() bounds remain well-ordered even for very small playout delays.
const Clock::duration max_in_flight = std::max(
kMinSenderInFlight, Clock::to_duration(target_playout_delay_) / 3);
return std::clamp(round_trip_time_ * 2, kMinSenderInFlight, max_in_flight);
}
bool SenderImpl::NeedsKeyFrame() const {
return last_enqueued_key_frame_id_ <= picture_lost_at_frame_id_;
}
FrameId SenderImpl::GetNextFrameId() const {
return last_enqueued_frame_id_ + 1;
}
Clock::duration SenderImpl::GetCurrentRoundTripTime() const {
return round_trip_time_;
}
openscreen::cast::Sender::EnqueueFrameResult SenderImpl::EnqueueFrame(
const EncodedFrame& frame) {
// Assume the fields of the `frame` have all been set correctly, with
// monotonically increasing timestamps and a valid pointer to the data.
OSP_CHECK_EQ(frame.frame_id, GetNextFrameId());
OSP_CHECK_GE(frame.referenced_frame_id, FrameId::first());
if (frame.frame_id != FrameId::first()) {
OSP_CHECK_GT(frame.rtp_timestamp, pending_sender_report_.rtp_timestamp);
if (frame.reference_time <= pending_sender_report_.reference_time) {
OSP_DLOG_WARN << "Frame " << frame.frame_id
<< " has non-monotonic reference_time: "
<< frame.reference_time
<< " <= " << pending_sender_report_.reference_time;
}
}
OSP_CHECK(frame.data.data());
const auto capture_begin_time =
(frame.capture_begin_time > Clock::time_point::min())
? frame.capture_begin_time
: Clock::now();
TRACE_FLOW_BEGIN_WITH_TIME(TraceCategory::kSender, "Frame.Capture",
frame.frame_id, capture_begin_time);
if (frame.capture_end_time > Clock::time_point::min()) {
TRACE_FLOW_STEP_WITH_TIME(TraceCategory::kSender, "Frame.Capture.End",
frame.frame_id, frame.capture_end_time);
}
TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Encode.End", frame.frame_id);
// Check whether enqueuing the frame would exceed the design limit for the
// span of FrameIds. Even if `num_frames_in_flight_` is less than
// kMaxUnackedFrames, it's the span of FrameIds that is restricted.
if ((frame.frame_id - checkpoint_frame_id_) > kMaxUnackedFrames) {
return REACHED_ID_SPAN_LIMIT;
}
// Check whether enqueuing the frame would exceed the current maximum media
// duration limit.
if (GetInFlightMediaDuration(frame.rtp_timestamp) >
GetMaxInFlightMediaDuration()) {
return MAX_DURATION_IN_FLIGHT;
}
// Encrypt the frame and initialize the slot tracking its sending.
PendingFrameSlot& slot = get_slot_for(frame.frame_id);
OSP_CHECK(!slot.frame);
slot.frame = crypto_.Encrypt(frame);
const int packet_count = rtp_packetizer_.ComputeNumberOfPackets(*slot.frame);
if (packet_count <= 0) {
slot.frame.reset();
return PAYLOAD_TOO_LARGE;
}
slot.send_flags.Resize(packet_count, BitVector::SET);
slot.packet_sent_times.assign(packet_count, SenderPacketRouter::kNever);
// Officially record the "enqueue."
++num_frames_in_flight_;
last_enqueued_frame_id_ = slot.frame->frame_id;
OSP_CHECK_LE(
num_frames_in_flight_,
static_cast<size_t>(last_enqueued_frame_id_ - checkpoint_frame_id_));
if (slot.frame->dependency == EncodedFrame::Dependency::kKeyFrame) {
last_enqueued_key_frame_id_ = slot.frame->frame_id;
}
TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Enqueued", frame.frame_id);
// Update the target playout delay, if necessary.
if (slot.frame->new_playout_delay > milliseconds::zero()) {
target_playout_delay_ = slot.frame->new_playout_delay;
playout_delay_change_at_frame_id_ = slot.frame->frame_id;
}
// Update the lip-sync information for the next Sender Report, ensuring that
// the reference time is monotonically increasing.
pending_sender_report_.reference_time =
frame.frame_id == FrameId::first()
? slot.frame->reference_time
: std::max(slot.frame->reference_time,
pending_sender_report_.reference_time);
pending_sender_report_.rtp_timestamp = slot.frame->rtp_timestamp;
// If the round trip time hasn't been computed yet, immediately send a RTCP
// packet (i.e., before the RTP packets are sent). The RTCP packet will
// provide a Sender Report which contains the required lip-sync information
// the Receiver needs for timing the media playout.
//
// Detail: Working backwards, if the round trip time is not known, then this
// Sender has never processed a Receiver Report. Thus, the Receiver has never
// provided a Receiver Report, which it can only do after having processed a
// Sender Report from this Sender. Thus, this Sender really needs to send
// that, right now!
if (round_trip_time_ == Clock::duration::zero()) {
packet_router_->RequestRtcpSend(rtcp_session_.receiver_ssrc());
}
// Re-activate RTP sending if it was suspended.
packet_router_->RequestRtpSend(rtcp_session_.receiver_ssrc());
statistics_dispatcher_.DispatchEnqueueEvents(config_.stream_type, frame);
return OK;
}
void SenderImpl::CancelInFlightData() {
TRACE_DEFAULT_SCOPED1(
TraceCategory::kSender, "frames_in_flight",
std::to_string(last_enqueued_frame_id_ - checkpoint_frame_id_));
while (checkpoint_frame_id_ < last_enqueued_frame_id_) {
++checkpoint_frame_id_;
CancelPendingFrame(checkpoint_frame_id_, /*was_acked*/ false);
}
DispatchCancellations();
}
void SenderImpl::ReportFrameDropEvent(FrameId frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point drop_time) {
statistics_dispatcher_.DispatchFrameDropEvent(config_.stream_type, frame_id,
rtp_timestamp, drop_time);
}
void SenderImpl::OnReceivedRtcpPacket(Clock::time_point arrival_time,
ByteView packet) {
rtcp_packet_arrival_time_ = arrival_time;
// This call to Parse() invoke zero or more of the OnReceiverXYZ() methods in
// the current call stack:
if (rtcp_parser_.Parse(packet, last_enqueued_frame_id_)) {
packet_router_->OnRtcpReceived(arrival_time, round_trip_time_);
}
}
ByteBuffer SenderImpl::GetRtcpPacketForImmediateSend(
Clock::time_point send_time,
ByteBuffer buffer) {
if (pending_sender_report_.reference_time == SenderPacketRouter::kNever) {
// Cannot send a report if one is not available (i.e., a frame has never
// been enqueued).
return buffer.subspan(0, 0);
}
// The Sender Report to be sent is a snapshot of the "pending Sender Report,"
// but with its timestamp fields modified. First, the reference time is set to
// the RTCP packet's send time. Then, the corresponding RTP timestamp is
// translated to match (for lip-sync).
RtcpSenderReport sender_report = pending_sender_report_;
sender_report.reference_time = send_time;
sender_report.rtp_timestamp += RtpTimeDelta::FromDuration(
sender_report.reference_time - pending_sender_report_.reference_time,
rtp_timebase_);
return sender_report_builder_.BuildPacket(sender_report, buffer).first;
}
ByteBuffer SenderImpl::GetRtpPacketForImmediateSend(Clock::time_point send_time,
ByteBuffer buffer) {
ChosenPacket chosen = ChooseNextRtpPacketNeedingSend();
// If no packets need sending (i.e., all packets have been sent at least once
// and do not need to be re-sent yet), check whether a Kickstart packet should
// be sent. It's possible that there has been complete packet loss of some
// frames, and the Receiver may not be aware of the existence of the latest
// frame(s). Kickstarting is the only way the Receiver can discover the newer
// frames it doesn't know about.
if (!chosen) {
const ChosenPacketAndWhen kickstart = ChooseKickstartPacket();
if (kickstart.when > send_time) {
// Nothing to send, so return "empty" signal to the packet router. The
// packet router will suspend RTP sending until this Sender explicitly
// resumes it.
return buffer.subspan(0, 0);
}
chosen = kickstart;
OSP_CHECK(chosen);
}
const ByteBuffer result = rtp_packetizer_.GeneratePacket(
*chosen.slot->frame, chosen.packet_id, buffer);
chosen.slot->send_flags.Clear(chosen.packet_id);
chosen.slot->packet_sent_times[chosen.packet_id] = send_time;
++pending_sender_report_.send_packet_count;
// According to RFC3550, the octet count does not include the RTP header. The
// following is just a good approximation, however, because the header size
// will very infrequently be 4 bytes greater (see
// RtpPacketizer::kAdaptiveLatencyHeaderSize). No known Cast Streaming
// Receiver implementations use this for anything, and so this should be fine.
const int approximate_octet_count =
static_cast<int>(result.size()) - RtpPacketizer::kBaseRtpHeaderSize;
OSP_CHECK_GE(approximate_octet_count, 0);
pending_sender_report_.send_octet_count += approximate_octet_count;
return result;
}
Clock::time_point SenderImpl::GetRtpResumeTime() {
if (ChooseNextRtpPacketNeedingSend()) {
return Alarm::kImmediately;
}
return ChooseKickstartPacket().when;
}
RtpTimeTicks SenderImpl::GetLastRtpTimestamp() const {
return {};
}
StreamType SenderImpl::GetStreamType() const {
return config_.stream_type;
}
void SenderImpl::OnReceiverReferenceTimeAdvanced(
Clock::time_point reference_time) {
// Not used.
}
// static
Clock::duration SenderImpl::SmoothRoundTripTime(Clock::duration estimate,
Clock::duration measurement) {
// Measurements typically have high variance, so smooth them with an
// exponentially-weighted moving average. The filter is asymmetric ("fast
// attack, slow decay"): it reacts quickly to upward spikes so the Sender
// notices congestion onset promptly and backs off, but decays slowly on the
// way down so a single low sample doesn't collapse the estimate. See
// crbug.com/498036656.
if (estimate == Clock::duration::zero()) {
return measurement;
}
if (measurement > estimate) {
// Spike / congestion onset: give the new measurement half the weight so the
// estimate climbs quickly.
return (estimate + measurement) / 2;
}
// Recovery: give the new measurement 1/8 weight (and the old estimate 7/8) to
// de-noise, since downward measurements are typically the network settling
// rather than a sustained improvement.
constexpr int kInertia = 7;
return (kInertia * estimate + measurement) / (kInertia + 1);
}
void SenderImpl::OnReceiverReport(const RtcpReportBlock& receiver_report) {
OSP_CHECK_NE(rtcp_packet_arrival_time_, SenderPacketRouter::kNever);
const Clock::duration total_delay =
rtcp_packet_arrival_time_ -
sender_report_builder_.GetRecentReportTime(
receiver_report.last_status_report_id, rtcp_packet_arrival_time_);
const auto non_network_delay =
Clock::to_duration(receiver_report.delay_since_last_report);
// Round trip time measurement: This is the time elapsed since the Sender
// Report was sent, minus the time the Receiver did other stuff before sending
// the Receiver Report back.
//
// If the round trip time seems to be less than or equal to zero, assume clock
// imprecision by one or both peers caused a bad value to be calculated. The
// true value is likely very close to zero (i.e., this is ideal network
// behavior); and so just represent this as 75 µs, an optimistic
// wired-Ethernet LAN ping time.
constexpr auto kNearZeroRoundTripTime = Clock::to_duration(microseconds(75));
static_assert(kNearZeroRoundTripTime > Clock::duration::zero(),
"More precision in Clock::duration needed!");
const Clock::duration measurement =
std::max(total_delay - non_network_delay, kNearZeroRoundTripTime);
// Validate the measurement by using the current target playout delay as a
// "reasonable upper-bound." It's certainly possible that the actual network
// round-trip time could exceed the target playout delay, but that would mean
// the current network performance is totally inadequate for streaming anyway.
// We cap the measurement here instead of ignoring it so the Sender still
// backs off its estimates during severe network congestion.
Clock::duration clamped_measurement = measurement;
if (clamped_measurement > target_playout_delay_) {
OSP_LOG_WARN << "Capping round-trip time measurement (" << measurement
<< ") to the current target playout delay ("
<< target_playout_delay_ << ").";
clamped_measurement = target_playout_delay_;
}
round_trip_time_ = SmoothRoundTripTime(round_trip_time_, clamped_measurement);
TRACE_SCOPED1(TraceCategory::kSender, "UpdatedRoundTripTime",
"round_trip_time", ToString(round_trip_time_));
}
void SenderImpl::OnCastReceiverFrameLogMessages(
std::vector<RtcpReceiverFrameLogMessage> messages) {
statistics_dispatcher_.DispatchFrameLogMessages(config_.stream_type,
messages);
}
void SenderImpl::OnReceiverIndicatesPictureLoss() {
TRACE_DEFAULT_SCOPED1(TraceCategory::kSender, "last_received_frame_id",
picture_lost_at_frame_id_.ToString());
// The Receiver will continue the PLI notifications until it has received a
// key frame. Thus, if a key frame is already in-flight, don't make a state
// change that would cause this Sender to force another expensive key frame.
if (checkpoint_frame_id_ < last_enqueued_key_frame_id_) {
return;
}
picture_lost_at_frame_id_ = checkpoint_frame_id_;
if (observer_) {
observer_->OnPictureLost();
}
// Note: It may seem that all pending frames should be canceled until
// EnqueueFrame() is called with a key frame. However:
//
// 1. The Receiver should still be the main authority on what frames/packets
// are being ACK'ed and NACK'ed.
//
// 2. It may be desirable for the Receiver to be "limping along" in the
// meantime. For example, video may be corrupted but mostly watchable,
// and so it's best for the Sender to continue sending the non-key frames
// until the Receiver indicates otherwise.
}
void SenderImpl::OnReceiverCheckpoint(FrameId frame_id,
milliseconds playout_delay) {
TRACE_DEFAULT_SCOPED2(TraceCategory::kSender, "frame_id", frame_id.ToString(),
"playout_delay", ToString(playout_delay));
if (frame_id > last_enqueued_frame_id_) {
TRACE_SET_RESULT(Error::Code::kParameterOutOfRange);
OSP_LOG_ERROR
<< "Ignoring checkpoint for " << latest_expected_frame_id_
<< " because this Sender could not have sent any frames after "
<< last_enqueued_frame_id_ << '.';
return;
}
// CompoundRtcpParser should guarantee this:
OSP_CHECK_GE(playout_delay, milliseconds::zero());
while (checkpoint_frame_id_ < frame_id) {
++checkpoint_frame_id_;
PendingFrameSlot& slot = get_slot_for(checkpoint_frame_id_);
if (slot.is_active_for_frame(checkpoint_frame_id_)) {
const RtpTimeTicks rtp_timestamp = slot.frame->rtp_timestamp;
statistics_dispatcher_.DispatchAckEvent(
config_.stream_type, rtp_timestamp, checkpoint_frame_id_);
CancelPendingFrame(checkpoint_frame_id_, /*was_acked*/ true);
TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Acked",
checkpoint_frame_id_);
}
}
latest_expected_frame_id_ = std::max(latest_expected_frame_id_, frame_id);
DispatchCancellations();
if (playout_delay != target_playout_delay_ &&
frame_id >= playout_delay_change_at_frame_id_) {
OSP_LOG_WARN << "Sender's target playout delay (" << target_playout_delay_
<< ") disagrees with the Receiver's (" << playout_delay << ")";
}
}
void SenderImpl::OnReceiverHasFrames(std::vector<FrameId> acks) {
OSP_DCHECK(!acks.empty() && AreElementsSortedAndUnique(acks));
TRACE_DEFAULT_SCOPED1(TraceCategory::kSender, "frame_ids",
string_util::Join(acks));
if (acks.back() > last_enqueued_frame_id_) {
TRACE_SET_RESULT(Error::Code::kParameterOutOfRange);
OSP_LOG_ERROR << "Ignoring individual frame ACKs: ACKing frame "
<< latest_expected_frame_id_
<< " is invalid because this Sender could not have sent any "
"frames after "
<< last_enqueued_frame_id_ << '.';
return;
}
for (FrameId id : acks) {
TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Acked", id);
PendingFrameSlot& slot = get_slot_for(id);
if (slot.is_active_for_frame(id)) {
const RtpTimeTicks rtp_timestamp = slot.frame->rtp_timestamp;
statistics_dispatcher_.DispatchAckEvent(config_.stream_type,
rtp_timestamp, id);
}
CancelPendingFrame(id, /*was_acked*/ true);
}
latest_expected_frame_id_ = std::max(latest_expected_frame_id_, acks.back());
DispatchCancellations();
}
void SenderImpl::OnReceiverIsMissingPackets(std::vector<PacketNack> nacks) {
TRACE_DEFAULT_SCOPED1(TraceCategory::kSender, "number_of_packets",
std::to_string(nacks.size()));
OSP_DCHECK(!nacks.empty() && AreElementsSortedAndUnique(nacks));
OSP_CHECK_NE(rtcp_packet_arrival_time_, SenderPacketRouter::kNever);
// This is a point-in-time threshold that indicates whether each NACK will
// trigger a packet retransmit. The threshold is based on the network round
// trip time because a Receiver's NACK may have been issued while the needed
// packet was in-flight from the Sender. In such cases, the Receiver's NACK is
// likely stale and this Sender should not redundantly re-transmit the packet
// again.
const Clock::time_point too_recent_a_send_time =
rtcp_packet_arrival_time_ - round_trip_time_;
// Iterate over all the NACKs...
bool need_to_send = false;
for (auto nack_it = nacks.begin(); nack_it != nacks.end();) {
// Find the slot associated with the NACK's frame ID.
const FrameId frame_id = nack_it->frame_id;
PendingFrameSlot* slot = nullptr;
if (frame_id <= last_enqueued_frame_id_) {
PendingFrameSlot& candidate_slot = get_slot_for(frame_id);
if (candidate_slot.is_active_for_frame(frame_id)) {
slot = &candidate_slot;
}
}
// If no slot was found (i.e., the NACK is invalid) for the frame, skip-over
// all other NACKs for the same frame. While it seems to be a bug that the
// Receiver would attempt to NACK a frame that does not yet exist, this can
// happen in rare cases where RTCP packets arrive out-of-order (i.e., the
// network shuffled them).
if (!slot) {
TRACE_SCOPED1(TraceCategory::kSender, "MissingNackSlot", "frame_id",
frame_id.ToString());
for (++nack_it; nack_it != nacks.end() && nack_it->frame_id == frame_id;
++nack_it) {
}
continue;
}
latest_expected_frame_id_ = std::max(latest_expected_frame_id_, frame_id);
const auto HandleIndividualNack = [&](FramePacketId packet_id) {
if (slot->packet_sent_times[packet_id] <= too_recent_a_send_time) {
slot->send_flags.Set(packet_id);
need_to_send = true;
}
};
const FramePacketId range_end = slot->packet_sent_times.size();
if (nack_it->packet_id == kAllPacketsLost) {
for (FramePacketId packet_id = 0; packet_id < range_end; ++packet_id) {
HandleIndividualNack(packet_id);
}
++nack_it;
} else {
do {
if (nack_it->packet_id < range_end) {
HandleIndividualNack(nack_it->packet_id);
} else {
OSP_LOG_WARN
<< "Ignoring NACK for packet that doesn't exist in frame "
<< frame_id << ": " << static_cast<int>(nack_it->packet_id);
}
++nack_it;
} while (nack_it != nacks.end() && nack_it->frame_id == frame_id);
}
}
if (need_to_send) {
packet_router_->RequestRtpSend(rtcp_session_.receiver_ssrc());
}
}
SenderImpl::ChosenPacket SenderImpl::ChooseNextRtpPacketNeedingSend() {
// Find the oldest packet needing to be sent (or re-sent).
for (FrameId frame_id = checkpoint_frame_id_ + 1;
frame_id <= last_enqueued_frame_id_; ++frame_id) {
PendingFrameSlot& slot = get_slot_for(frame_id);
if (!slot.is_active_for_frame(frame_id)) {
continue; // Frame was canceled. None of its packets need to be sent.
}
const FramePacketId packet_id = slot.send_flags.FindFirstSet();
if (packet_id < slot.send_flags.size()) {
return {&slot, packet_id};
}
}
return {}; // Nothing needs to be sent.
}
SenderImpl::ChosenPacketAndWhen SenderImpl::ChooseKickstartPacket() {
if (latest_expected_frame_id_ >= last_enqueued_frame_id_) {
// Since the Receiver must know about all of the frames currently queued, no
// Kickstart packet is necessary.
return {};
}
// The Kickstart packet is always in the last-enqueued frame, so that the
// Receiver will know about every frame the Sender has. However, which packet
// should be chosen? Any would do, since all packets contain the frame's total
// packet count. For historical reasons, all sender implementations have
// always just sent the last packet; and so that tradition is continued here.
ChosenPacketAndWhen chosen;
chosen.slot = &get_slot_for(last_enqueued_frame_id_);
// Note: This frame cannot have been canceled since
// `latest_expected_frame_id_` hasn't yet reached this point.
OSP_CHECK(chosen.slot->is_active_for_frame(last_enqueued_frame_id_));
chosen.packet_id = chosen.slot->send_flags.size() - 1;
const Clock::time_point time_last_sent =
chosen.slot->packet_sent_times[chosen.packet_id];
// Sanity-check: This method should not be called to choose a packet while
// there are still unsent packets.
OSP_CHECK_NE(time_last_sent, SenderPacketRouter::kNever);
// The desired Kickstart interval is a fraction of the total
// `target_playout_delay_`. The reason for the specific ratio here is based on
// lost knowledge (from legacy implementations); but it makes sense (i.e., to
// be a good "network citizen") to be less aggressive for larger playout delay
// windows, and more aggressive for shorter ones to avoid too-late packet
// arrivals.
using kWaitFraction = std::ratio<1, 20>;
const Clock::duration desired_kickstart_interval =
Clock::to_duration(target_playout_delay_) * kWaitFraction::num /
kWaitFraction::den;
// The actual interval used is increased, if current network performance
// warrants waiting longer. Don't send a Kickstart packet until no NACKs
// have been received for two network round-trip periods.
constexpr int kLowerBoundRoundTrips = 2;
const Clock::duration kickstart_interval = std::max(
desired_kickstart_interval, round_trip_time_ * kLowerBoundRoundTrips);
chosen.when = time_last_sent + kickstart_interval;
return chosen;
}
void SenderImpl::CancelPendingFrame(FrameId frame_id, bool was_acked) {
TRACE_FLOW_END(TraceCategory::kSender, "Frame.Cancelled", frame_id);
PendingFrameSlot& slot = get_slot_for(frame_id);
if (!slot.is_active_for_frame(frame_id)) {
return; // Frame was already canceled.
}
if (was_acked) {
packet_router_->OnPayloadReceived(
slot.frame->data.size(), rtcp_packet_arrival_time_, round_trip_time_);
}
slot.frame.reset();
OSP_CHECK_GT(num_frames_in_flight_, 0);
--num_frames_in_flight_;
if (observer_) {
pending_cancellations_.emplace_back(frame_id);
}
}
void SenderImpl::DispatchCancellations() {
if (observer_) {
for (const FrameId id : pending_cancellations_) {
observer_->OnFrameCanceled(id);
}
}
pending_cancellations_.clear();
// At this point, there should either be no frames in flight, or the frame
// immediately after `checkpoint_frame_id_` must be valid.
OSP_DCHECK((num_frames_in_flight_ == 0) ||
get_slot_for(checkpoint_frame_id_ + 1)
.is_active_for_frame(checkpoint_frame_id_ + 1));
}
SenderImpl::PendingFrameSlot::PendingFrameSlot() = default;
SenderImpl::PendingFrameSlot::~PendingFrameSlot() = default;
} // namespace openscreen::cast

View file

@ -0,0 +1,252 @@
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_SENDER_IMPL_H_
#define CAST_STREAMING_IMPL_SENDER_IMPL_H_
#include <stdint.h>
#include <array>
#include <chrono>
#include <optional>
#include <vector>
#include "cast/streaming/impl/compound_rtcp_parser.h"
#include "cast/streaming/impl/frame_crypto.h"
#include "cast/streaming/impl/rtcp_common.h"
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/impl/rtp_packetizer.h"
#include "cast/streaming/impl/sender_report_builder.h"
#include "cast/streaming/impl/statistics_dispatcher.h"
#include "cast/streaming/public/constants.h"
#include "cast/streaming/public/frame_id.h"
#include "cast/streaming/public/sender.h"
#include "cast/streaming/public/session_config.h"
#include "cast/streaming/rtp_time.h"
#include "cast/streaming/sender_packet_router.h"
#include "platform/api/time.h"
#include "platform/base/span.h"
#include "util/bit_vector.h"
#include "util/raw_ptr.h"
#include "util/raw_ref.h"
namespace openscreen::cast {
class Environment;
// The Cast Streaming Sender, a peer corresponding to some Cast Streaming
// Receiver at the other end of a network link. See class level comments for
// Receiver for a high-level overview.
class SenderImpl final : public Sender,
public SenderPacketRouter::Sender,
public CompoundRtcpParser::Client {
public:
// Constructs a Sender that attaches to the given `environment`-provided
// resources and `packet_router`. The `config` contains the settings that were
// agreed-upon by both sides from the OFFER/ANSWER exchange (i.e., the part of
// the overall end-to-end connection process that occurs before Cast Streaming
// is started). The `rtp_payload_type` does not affect the behavior of this
// Sender. It is simply passed along to a Receiver in the RTP packet stream.
SenderImpl(Environment& environment,
SenderPacketRouter& packet_router,
SessionConfig config,
RtpPayloadType rtp_payload_type);
~SenderImpl() final;
// Sender overrides.
const SessionConfig& config() const override { return config_; }
void SetObserver(Observer* observer) override;
size_t GetInFlightFrameCount() const override;
Clock::duration GetInFlightMediaDuration(
RtpTimeTicks next_frame_rtp_timestamp) const override;
Clock::duration GetMaxInFlightMediaDuration() const override;
bool NeedsKeyFrame() const override;
FrameId GetNextFrameId() const override;
Clock::duration GetCurrentRoundTripTime() const override;
[[nodiscard]] EnqueueFrameResult EnqueueFrame(
const EncodedFrame& frame) override;
void CancelInFlightData() override;
void ReportFrameDropEvent(FrameId frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point drop_time) override;
// Smooths a new round-trip-time `measurement` into the running `estimate`
// using an asymmetric "fast attack, slow decay" filter: the estimate climbs
// quickly on an upward spike (so the Sender notices congestion onset) but
// decays slowly (so a single low sample does not collapse it). A zero
// `estimate` adopts the measurement directly. Static and exposed for testing.
static Clock::duration SmoothRoundTripTime(Clock::duration estimate,
Clock::duration measurement);
private:
// Tracking/Storage for frames that are ready-to-send, and until they are
// fully received at the other end.
struct PendingFrameSlot {
// The frame to send, or nullopt if this slot is not in use.
std::optional<EncryptedFrame> frame;
// Represents which packets need to be sent. Elements are indexed by
// FramePacketId. A set bit means a packet needs to be sent (or re-sent).
BitVector send_flags;
// The time when each of the packets was last sent, or
// `SenderPacketRouter::kNever` if the packet has not been sent yet.
// Elements are indexed by FramePacketId. This is used to avoid
// re-transmitting any given packet too frequently.
std::vector<Clock::time_point> packet_sent_times;
PendingFrameSlot();
~PendingFrameSlot();
bool is_active_for_frame(FrameId frame_id) const {
return frame && frame->frame_id == frame_id;
}
};
// Return value from the ChooseXYZ() helper methods.
struct ChosenPacket {
raw_ptr<PendingFrameSlot> slot = nullptr;
FramePacketId packet_id{};
explicit operator bool() const { return !!slot; }
};
// An extension of ChosenPacket that also includes the point-in-time when the
// packet should be sent.
struct ChosenPacketAndWhen : public ChosenPacket {
Clock::time_point when = SenderPacketRouter::kNever;
};
// SenderPacketRouter::Sender implementation.
void OnReceivedRtcpPacket(Clock::time_point arrival_time,
ByteView packet) final;
ByteBuffer GetRtcpPacketForImmediateSend(Clock::time_point send_time,
ByteBuffer buffer) final;
ByteBuffer GetRtpPacketForImmediateSend(Clock::time_point send_time,
ByteBuffer buffer) final;
Clock::time_point GetRtpResumeTime() final;
RtpTimeTicks GetLastRtpTimestamp() const final;
StreamType GetStreamType() const final;
// CompoundRtcpParser::Client implementation.
void OnReceiverReferenceTimeAdvanced(Clock::time_point reference_time) final;
void OnReceiverReport(const RtcpReportBlock& receiver_report) final;
void OnCastReceiverFrameLogMessages(
std::vector<RtcpReceiverFrameLogMessage> messages) final;
void OnReceiverIndicatesPictureLoss() final;
void OnReceiverCheckpoint(FrameId frame_id,
std::chrono::milliseconds playout_delay) final;
void OnReceiverHasFrames(std::vector<FrameId> acks) final;
void OnReceiverIsMissingPackets(std::vector<PacketNack> nacks) final;
// Helper to choose which packet to send, from those that have been flagged as
// "need to send." Returns a "false" result if nothing needs to be sent.
ChosenPacket ChooseNextRtpPacketNeedingSend();
// Helper that returns the packet that should be used to kick-start the
// Receiver, and the time at which the packet should be sent. Returns a kNever
// result if kick-starting is not needed.
ChosenPacketAndWhen ChooseKickstartPacket();
// Cancels sending (or resending) the given frame once it is known to have
// been either:
// 1. Cancelled by the sender (was_acked must be false);
// 2. Fully received based on the ACK feedback in a receiver RTCP report
// (was_acked must be true);
// 3. The receiver sent a checkpoint frame ID (was_acked must be true).
//
// This clears the corresponding entry in `pending_frames_` and
// adds `frame_id` to the list of pending cancellations to be dispatched as
// part of DispatchCancellations().
//
// NOTE: Every frame_id ends up being "cancelled" at least once.
void CancelPendingFrame(FrameId frame_id, bool was_acked);
// Must be called after one or a series of CancelPendingFrame() calls in order
// to notify the observer, if any, about cancellations.
void DispatchCancellations();
// Inline helper to return the slot that would contain the tracking info for
// the given `frame_id`.
const PendingFrameSlot& get_slot_for(FrameId frame_id) const {
return pending_frames_[(frame_id - FrameId::first()) %
pending_frames_.size()];
}
PendingFrameSlot& get_slot_for(FrameId frame_id) {
return pending_frames_[(frame_id - FrameId::first()) %
pending_frames_.size()];
}
const SessionConfig config_;
const raw_ref<SenderPacketRouter> packet_router_;
RtcpSession rtcp_session_;
CompoundRtcpParser rtcp_parser_;
SenderReportBuilder sender_report_builder_;
RtpPacketizer rtp_packetizer_;
const int rtp_timebase_;
FrameCrypto crypto_;
StatisticsDispatcher statistics_dispatcher_;
// Ring buffer of PendingFrameSlots. The frame having FrameId x will always
// be slotted at position x % pending_frames_.size(). Use get_slot_for() to
// access the correct slot for a given FrameId.
std::array<PendingFrameSlot, kMaxUnackedFrames> pending_frames_ = {};
// A count of the number of frames in-flight (i.e., the number of active
// entries in `pending_frames_`).
size_t num_frames_in_flight_ = 0;
// The ID of the last frame enqueued.
FrameId last_enqueued_frame_id_ = FrameId::leader();
// Indicates that all of the packets for all frames up to and including this
// FrameId have been successfully received (or otherwise do not need to be
// re-transmitted).
FrameId checkpoint_frame_id_ = FrameId::leader();
// The ID of the latest frame the Receiver seems to be aware of.
FrameId latest_expected_frame_id_ = FrameId::leader();
// The target playout delay for the last-enqueued frame. This is auto-updated
// when a frame is enqueued that changes the delay.
std::chrono::milliseconds target_playout_delay_;
FrameId playout_delay_change_at_frame_id_ = FrameId::first();
// The exact arrival time of the last RTCP packet.
Clock::time_point rtcp_packet_arrival_time_ = SenderPacketRouter::kNever;
// The near-term average round trip time. This is updated with each Sender
// Report → Receiver Report round trip. This is initially zero, indicating the
// round trip time has not been measured yet.
Clock::duration round_trip_time_ = {};
// Maintain current stats in a Sender Report that is ready for sending at any
// time. This includes up-to-date lip-sync information, and packet and byte
// count stats.
RtcpSenderReport pending_sender_report_;
// These are used to determine whether a key frame needs to be sent to the
// Receiver. When the Receiver provides a picture loss notification, the
// current checkpoint frame ID is stored in `picture_lost_at_frame_id_`. Then,
// while `last_enqueued_key_frame_id_` is less than or equal to
// `picture_lost_at_frame_id_`, the Sender knows it still needs to send a key
// frame to resolve the picture loss condition. In all other cases, the
// Receiver is either in a good state or is in the process of receiving the
// key frame that will make that happen.
FrameId picture_lost_at_frame_id_ = FrameId::leader();
FrameId last_enqueued_key_frame_id_ = FrameId::leader();
// The current observer (optional).
raw_ptr<Observer> observer_ = nullptr;
// Because the observer may take action when frames are cancelled, such as
// calling APIs like EnqueueFrame(), `this` must be in a good state before
// the observer is notified of any pending frame cancellations.
std::vector<FrameId> pending_cancellations_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_SENDER_IMPL_H_

View file

@ -0,0 +1,79 @@
// Copyright 2019 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/sender_report_builder.h"
#include "cast/streaming/impl/packet_util.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
SenderReportBuilder::SenderReportBuilder(RtcpSession& session)
: session_(session) {}
SenderReportBuilder::~SenderReportBuilder() = default;
std::pair<ByteBuffer, StatusReportId> SenderReportBuilder::BuildPacket(
const RtcpSenderReport& sender_report,
ByteBuffer buffer) const {
OSP_CHECK_GE(buffer.size(), kRequiredBufferSize);
uint8_t* const packet_begin = buffer.data();
RtcpCommonHeader header;
header.packet_type = RtcpPacketType::kSenderReport;
header.payload_size = kRtcpSenderReportSize;
if (sender_report.report_block) {
header.with.report_count = 1;
header.payload_size += kRtcpReportBlockSize;
} else {
header.with.report_count = 0;
}
header.AppendFields(buffer);
AppendField<uint32_t>(session_->sender_ssrc(), buffer);
const NtpTimestamp ntp_timestamp =
session_->ntp_converter().ToNtpTimestamp(sender_report.reference_time);
AppendField<uint64_t>(ntp_timestamp, buffer);
AppendField<uint32_t>(sender_report.rtp_timestamp.lower_32_bits(), buffer);
AppendField<uint32_t>(sender_report.send_packet_count, buffer);
AppendField<uint32_t>(sender_report.send_octet_count, buffer);
if (sender_report.report_block) {
sender_report.report_block->AppendFields(buffer);
}
uint8_t* const packet_end = buffer.data();
return std::make_pair(ByteBuffer(packet_begin, packet_end - packet_begin),
ToStatusReportId(ntp_timestamp));
}
Clock::time_point SenderReportBuilder::GetRecentReportTime(
StatusReportId report_id,
Clock::time_point on_or_before) const {
// Assumption: The `report_id` is the middle 32 bits of a 64-bit NtpTimestamp.
static_assert(ToStatusReportId(NtpTimestamp{0x0192a3b4c5d6e7f8}) ==
StatusReportId{0xa3b4c5d6},
"FIXME: ToStatusReportId() implementation changed.");
// Compute the maximum possible NtpTimestamp. Then, use its uppermost 16 bits
// and the 32 bits from the report_id to produce a reconstructed NtpTimestamp.
const NtpTimestamp max_timestamp =
session_->ntp_converter().ToNtpTimestamp(on_or_before);
// max_timestamp: HH......
// report_id: LLLL
// ↓↓ ↙↙↙↙
// reconstructed: HHLLLL00
NtpTimestamp reconstructed = (max_timestamp & (uint64_t{0xffff} << 48)) |
(static_cast<uint64_t>(report_id) << 16);
// If the reconstructed timestamp is greater than the maximum one, rollover
// of the lower 48 bits occurred. Subtract one from the upper 16 bits to
// rectify that.
if (reconstructed > max_timestamp) {
reconstructed -= uint64_t{1} << 48;
}
return session_->ntp_converter().ToLocalTime(reconstructed);
}
} // namespace openscreen::cast

View file

@ -0,0 +1,50 @@
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_
#define CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_
#include <stdint.h>
#include <utility>
#include "cast/streaming/impl/rtcp_common.h"
#include "cast/streaming/impl/rtcp_session.h"
#include "cast/streaming/impl/rtp_defines.h"
#include "platform/api/time.h"
#include "platform/base/span.h"
#include "util/raw_ref.h"
namespace openscreen::cast {
// Builds RTCP packets containing one Sender Report.
class SenderReportBuilder {
public:
explicit SenderReportBuilder(RtcpSession& session);
~SenderReportBuilder();
// Serializes the given `sender_report` as a RTCP packet and writes it to
// `buffer` (which must be kRequiredBufferSize in size). Returns the subspan
// of `buffer` that contains the result and a StatusReportId the receiver
// might use in its own reports to reference this specific report.
std::pair<ByteBuffer, StatusReportId> BuildPacket(
const RtcpSenderReport& sender_report,
ByteBuffer buffer) const;
// Returns the approximate reference time from a recently-built Sender Report,
// based on the given `report_id` and maximum possible reference time.
Clock::time_point GetRecentReportTime(StatusReportId report_id,
Clock::time_point on_or_before) const;
// The required size (in bytes) of the buffer passed to BuildPacket().
static constexpr int kRequiredBufferSize =
kRtcpCommonHeaderSize + kRtcpSenderReportSize + kRtcpReportBlockSize;
private:
const raw_ref<RtcpSession> session_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_

View file

@ -0,0 +1,573 @@
// 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/statistics_analyzer.h"
#include <algorithm>
#include "cast/streaming/impl/statistics_common.h"
#include "platform/base/trivial_clock_traits.h"
#include "util/chrono_helpers.h"
namespace openscreen::cast {
using openscreen::clock_operators::operator<<;
namespace {
constexpr Clock::duration kAnalysisInterval = std::chrono::milliseconds(500);
constexpr size_t kMaxRecentPacketInfoMapSize = 1000;
constexpr size_t kMaxRecentFrameInfoMapSize = 200;
constexpr int kDefaultMaxLatencyBucketMs = 800;
constexpr int kDefaultBucketWidthMs = 20;
double InMilliseconds(Clock::duration duration) {
return static_cast<double>(to_milliseconds(duration).count());
}
bool IsReceiverEvent(StatisticsEvent::Type event) {
return event == StatisticsEvent::Type::kFrameAckSent ||
event == StatisticsEvent::Type::kFrameDecoded ||
event == StatisticsEvent::Type::kFramePlayedOut ||
event == StatisticsEvent::Type::kPacketReceived;
}
} // namespace
StatisticsAnalyzer::StatisticsAnalyzer(
SenderStatsClient* stats_client,
ClockNowFunctionPtr now,
TaskRunner& task_runner,
std::unique_ptr<ClockOffsetEstimator> offset_estimator)
: stats_client_(stats_client),
offset_estimator_(std::move(offset_estimator)),
now_(now),
alarm_(now, task_runner),
start_time_(now()) {
statistics_collector_ = std::make_unique<StatisticsCollector>(now_);
InitHistograms();
}
StatisticsAnalyzer::~StatisticsAnalyzer() = default;
void StatisticsAnalyzer::ScheduleAnalysis() {
Clock::time_point next_analysis_time = now_() + kAnalysisInterval;
alarm_.Schedule([this] { AnalyzeStatistics(); }, next_analysis_time);
}
void StatisticsAnalyzer::InitHistograms() {
for (auto& histogram : histograms_.audio) {
histogram =
SimpleHistogram(0, kDefaultMaxLatencyBucketMs, kDefaultBucketWidthMs);
}
for (auto& histogram : histograms_.video) {
histogram =
SimpleHistogram(0, kDefaultMaxLatencyBucketMs, kDefaultBucketWidthMs);
}
}
void StatisticsAnalyzer::AnalyzeStatistics() {
ProcessFrameEvents(statistics_collector_->TakeRecentFrameEvents());
ProcessPacketEvents(statistics_collector_->TakeRecentPacketEvents());
SendStatistics();
ScheduleAnalysis();
}
void StatisticsAnalyzer::SendStatistics() {
if (!stats_client_) {
return;
}
const Clock::time_point end_time = now_();
stats_client_->OnStatisticsUpdated(SenderStats{
.audio_statistics =
ConstructStatisticsList(end_time, StatisticsEvent::MediaType::kAudio),
.audio_histograms = histograms_.audio,
.video_statistics =
ConstructStatisticsList(end_time, StatisticsEvent::MediaType::kVideo),
.video_histograms = histograms_.video});
}
void StatisticsAnalyzer::ProcessFrameEvents(
const std::vector<FrameEvent>& frame_events) {
for (FrameEvent frame_event : frame_events) {
offset_estimator_->OnFrameEvent(frame_event);
FrameStatsMap& frame_stats_map = frame_stats_.Get(frame_event.media_type);
auto it = frame_stats_map.find(frame_event.type);
if (it == frame_stats_map.end()) {
frame_stats_map.insert(std::make_pair(
frame_event.type,
FrameStatsAggregate{.event_counter = 1,
.sum_size = frame_event.size,
.sum_delay = frame_event.delay_delta}));
} else {
++(it->second.event_counter);
it->second.sum_size += frame_event.size;
it->second.sum_delay += frame_event.delay_delta;
}
RecordEventTimes(frame_event);
RecordFrameLatencies(frame_event);
}
}
void StatisticsAnalyzer::ProcessPacketEvents(
const std::vector<PacketEvent>& packet_events) {
for (PacketEvent packet_event : packet_events) {
offset_estimator_->OnPacketEvent(packet_event);
PacketStatsMap& packet_stats_map =
packet_stats_.Get(packet_event.media_type);
auto it = packet_stats_map.find(packet_event.type);
if (it == packet_stats_map.end()) {
packet_stats_map.insert(
std::make_pair(packet_event.type,
PacketStatsAggregate{.event_counter = 1,
.sum_size = packet_event.size}));
} else {
++(it->second.event_counter);
it->second.sum_size += packet_event.size;
}
RecordEventTimes(packet_event);
if (packet_event.type == StatisticsEvent::Type::kPacketSentToNetwork ||
packet_event.type == StatisticsEvent::Type::kPacketReceived) {
RecordPacketLatencies(packet_event);
} else if (packet_event.type ==
StatisticsEvent::Type::kPacketRetransmitted) {
// We only measure network latency for packets that are not retransmitted.
ErasePacketInfo(packet_event);
}
}
}
void StatisticsAnalyzer::RecordFrameLatencies(const FrameEvent& frame_event) {
FrameInfoMap& frame_infos = recent_frame_infos_.Get(frame_event.media_type);
// Event is too old, don't bother.
const bool map_is_full = frame_infos.size() == kMaxRecentFrameInfoMapSize;
if (map_is_full && frame_event.rtp_timestamp <= frame_infos.begin()->first) {
return;
}
auto it = frame_infos.find(frame_event.rtp_timestamp);
if (it == frame_infos.end()) {
if (map_is_full) {
frame_infos.erase(frame_infos.begin());
}
auto emplace_result =
frame_infos.emplace(frame_event.rtp_timestamp, FrameInfo{});
OSP_CHECK(emplace_result.second);
it = emplace_result.first;
}
switch (frame_event.type) {
case StatisticsEvent::Type::kFrameCaptureBegin:
it->second.capture_begin_time = frame_event.timestamp;
break;
case StatisticsEvent::Type::kFrameCaptureEnd: {
it->second.capture_end_time = frame_event.timestamp;
if (it->second.capture_begin_time != Clock::time_point::min()) {
const Clock::duration capture_latency =
frame_event.timestamp - it->second.capture_begin_time;
AddToLatencyAggregrate(StatisticType::kAvgCaptureLatencyMs,
capture_latency, frame_event.media_type);
AddToHistogram(HistogramType::kCaptureLatencyMs, frame_event.media_type,
InMilliseconds(capture_latency));
}
} break;
case StatisticsEvent::Type::kFrameEncoded: {
it->second.encode_end_time = frame_event.timestamp;
if (it->second.capture_end_time != Clock::time_point::min()) {
const Clock::duration encode_latency =
frame_event.timestamp - it->second.capture_end_time;
AddToLatencyAggregrate(StatisticType::kAvgEncodeTimeMs, encode_latency,
frame_event.media_type);
AddToHistogram(HistogramType::kEncodeTimeMs, frame_event.media_type,
InMilliseconds(encode_latency));
}
} break;
// Frame latency is the time from when the frame is encoded until the
// receiver ack for the frame is sent.
case StatisticsEvent::Type::kFrameAckSent: {
const auto adjusted_timestamp =
ToSenderTimestamp(frame_event.timestamp, frame_event.media_type);
if (!adjusted_timestamp) {
return;
}
if (it->second.encode_end_time != Clock::time_point::min()) {
const Clock::duration frame_latency =
*adjusted_timestamp - it->second.encode_end_time;
AddToLatencyAggregrate(StatisticType::kAvgFrameLatencyMs, frame_latency,
frame_event.media_type);
}
} break;
case StatisticsEvent::Type::kFramePlayedOut: {
const auto adjusted_timestamp =
ToSenderTimestamp(frame_event.timestamp, frame_event.media_type);
if (!adjusted_timestamp) {
return;
}
if (it->second.capture_begin_time != Clock::time_point::min()) {
const Clock::duration e2e_latency =
*adjusted_timestamp - it->second.capture_begin_time;
AddToLatencyAggregrate(StatisticType::kAvgEndToEndLatencyMs,
e2e_latency, frame_event.media_type);
AddToHistogram(HistogramType::kEndToEndLatencyMs,
frame_event.media_type, InMilliseconds(e2e_latency));
}
// Positive delay means the frame is late.
if (frame_event.delay_delta > Clock::duration::zero()) {
session_stats_.Get(frame_event.media_type).late_frame_counter += 1;
AddToHistogram(HistogramType::kFrameLatenessMs, frame_event.media_type,
InMilliseconds(frame_event.delay_delta));
}
} break;
default:
break;
}
}
void StatisticsAnalyzer::RecordPacketLatencies(
const PacketEvent& packet_event) {
FrameInfoMap& frame_infos = recent_frame_infos_.Get(packet_event.media_type);
// Queueing latency is the time from when a frame is encoded to when the
// packet is first sent.
if (packet_event.type == StatisticsEvent::Type::kPacketSentToNetwork) {
const auto it = frame_infos.find(packet_event.rtp_timestamp);
// We have an encode end time for a frame associated with this packet.
if (it != frame_infos.end()) {
const Clock::duration queueing_latency =
packet_event.timestamp - it->second.encode_end_time;
AddToLatencyAggregrate(StatisticType::kAvgQueueingLatencyMs,
queueing_latency, packet_event.media_type);
AddToHistogram(HistogramType::kQueueingLatencyMs, packet_event.media_type,
InMilliseconds(queueing_latency));
}
}
StatisticsAnalyzer::PacketKey key =
std::make_pair(packet_event.rtp_timestamp, packet_event.packet_id);
PacketInfoMap& packet_infos =
recent_packet_infos_.Get(packet_event.media_type);
const auto it = packet_infos.find(key);
if (it == packet_infos.end()) {
packet_infos.insert(
std::make_pair(key, PacketInfo{.timestamp = packet_event.timestamp,
.type = packet_event.type}));
if (packet_infos.size() > kMaxRecentPacketInfoMapSize) {
packet_infos.erase(packet_infos.begin());
}
} else { // We know when this packet was sent, and when it arrived.
PacketInfo value = it->second;
StatisticsEvent::Type recorded_type = value.type;
Clock::time_point packet_sent_time;
Clock::time_point packet_received_time;
if (recorded_type == StatisticsEvent::Type::kPacketSentToNetwork &&
packet_event.type == StatisticsEvent::Type::kPacketReceived) {
packet_sent_time = value.timestamp;
packet_received_time = packet_event.timestamp;
} else if (recorded_type == StatisticsEvent::Type::kPacketReceived &&
packet_event.type ==
StatisticsEvent::Type::kPacketSentToNetwork) {
packet_sent_time = packet_event.timestamp;
packet_received_time = value.timestamp;
} else {
return;
}
packet_infos.erase(it);
// Use the offset estimator directly since we are trying to calculate the
// average network latency.
const std::optional<Clock::duration> receiver_offset =
offset_estimator_->GetEstimatedOffset();
if (!receiver_offset) {
return;
}
packet_received_time -= *receiver_offset;
const auto latency = packet_received_time - packet_sent_time;
AddToLatencyAggregrate(StatisticType::kAvgNetworkLatencyMs, latency,
packet_event.media_type);
AddToHistogram(HistogramType::kNetworkLatencyMs, packet_event.media_type,
InMilliseconds(latency));
// Packet latency is the time from when a frame is encoded until when the
// packet is received.
const auto frame_it = frame_infos.find(packet_event.rtp_timestamp);
if (frame_it != frame_infos.end()) {
const Clock::duration packet_latency =
packet_received_time - frame_it->second.encode_end_time;
AddToLatencyAggregrate(StatisticType::kAvgPacketLatencyMs, packet_latency,
packet_event.media_type);
AddToHistogram(HistogramType::kPacketLatencyMs, packet_event.media_type,
InMilliseconds(packet_latency));
}
}
}
void StatisticsAnalyzer::RecordEventTimes(const StatisticsEvent& event) {
SessionStats& session_stats = session_stats_.Get(event.media_type);
Clock::time_point sender_timestamp = event.timestamp;
if (IsReceiverEvent(event.type)) {
const auto latency = offset_estimator_->GetEstimatedLatency();
if (latency) {
const Clock::time_point estimated_sent_time =
event.received_timestamp - *latency;
session_stats.last_response_received_time = std::max(
session_stats.last_response_received_time, estimated_sent_time);
}
const auto result = ToSenderTimestamp(event.timestamp, event.media_type);
if (!result) {
return;
}
sender_timestamp = *result;
}
session_stats.first_event_time =
std::min(session_stats.first_event_time, sender_timestamp);
session_stats.last_event_time =
std::max(session_stats.last_event_time, sender_timestamp);
}
void StatisticsAnalyzer::ErasePacketInfo(const PacketEvent& packet_event) {
const StatisticsAnalyzer::PacketKey key =
std::make_pair(packet_event.rtp_timestamp, packet_event.packet_id);
PacketInfoMap& packet_infos =
recent_packet_infos_.Get(packet_event.media_type);
packet_infos.erase(key);
}
void StatisticsAnalyzer::AddToLatencyAggregrate(
StatisticType latency_stat,
Clock::duration latency_delta,
StatisticsEvent::MediaType media_type) {
LatencyStatsMap& latency_stats = latency_stats_.Get(media_type);
auto it = latency_stats.find(latency_stat);
if (it == latency_stats.end()) {
latency_stats.insert(std::make_pair(
latency_stat, LatencyStatsAggregate{.data_point_counter = 1,
.sum_latency = latency_delta}));
} else {
++(it->second.data_point_counter);
it->second.sum_latency += latency_delta;
}
}
void StatisticsAnalyzer::AddToHistogram(HistogramType histogram,
StatisticsEvent::MediaType media_type,
int64_t sample) {
histograms_.Get(media_type)[static_cast<int>(histogram)].Add(sample);
}
SenderStats::StatisticsList StatisticsAnalyzer::ConstructStatisticsList(
Clock::time_point end_time,
StatisticsEvent::MediaType media_type) {
SenderStats::StatisticsList stats_list;
PopulateFrameCountStat(StatisticsEvent::Type::kFrameDroppedByEncoder,
StatisticType::kNumFramesDroppedByEncoder, media_type,
stats_list);
PopulateFrameCountStat(StatisticsEvent::Type::kFrameCaptureEnd,
StatisticType::kNumFramesCaptured, media_type,
stats_list);
// kEnqueueFps
PopulateFpsStat(StatisticsEvent::Type::kFrameEncoded,
StatisticType::kEnqueueFps, media_type, end_time, stats_list);
constexpr StatisticType kSupportedLatencyStats[] = {
StatisticType::kAvgEncodeTimeMs, StatisticType::kAvgCaptureLatencyMs,
StatisticType::kAvgQueueingLatencyMs, StatisticType::kAvgNetworkLatencyMs,
StatisticType::kAvgPacketLatencyMs, StatisticType::kAvgFrameLatencyMs,
StatisticType::kAvgEndToEndLatencyMs,
};
for (StatisticType type : kSupportedLatencyStats) {
PopulateAvgLatencyStat(type, media_type, stats_list);
}
// kEncodeRateKbps
PopulateFrameBitrateStat(StatisticsEvent::Type::kFrameEncoded,
StatisticType::kEncodeRateKbps, media_type, end_time,
stats_list);
// kPacketTransmissionRateKbps
PopulatePacketBitrateStat(StatisticsEvent::Type::kPacketSentToNetwork,
StatisticType::kPacketTransmissionRateKbps,
media_type, end_time, stats_list);
// kNumPacketsSent
PopulatePacketCountStat(StatisticsEvent::Type::kPacketSentToNetwork,
StatisticType::kNumPacketsSent, media_type,
stats_list);
// kNumPacketsReceived
PopulatePacketCountStat(StatisticsEvent::Type::kPacketReceived,
StatisticType::kNumPacketsReceived, media_type,
stats_list);
// kTimeSinceLastReceiverResponseMs
// kFirstEventTimeMs
// kLastEventTimeMs
// kNumLateFrames
PopulateSessionStats(media_type, end_time, stats_list);
return stats_list;
}
void StatisticsAnalyzer::PopulatePacketCountStat(
StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
SenderStats::StatisticsList& stats_list) {
PacketStatsMap& stats_map = packet_stats_.Get(media_type);
auto it = stats_map.find(event);
if (it != stats_map.end()) {
stats_list[static_cast<int>(stat)] = it->second.event_counter;
}
}
void StatisticsAnalyzer::PopulateFrameCountStat(
StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
SenderStats::StatisticsList& stats_list) {
FrameStatsMap& stats_map = frame_stats_.Get(media_type);
const auto it = stats_map.find(event);
if (it != stats_map.end()) {
stats_list[static_cast<int>(stat)] = it->second.event_counter;
}
}
void StatisticsAnalyzer::PopulateFpsStat(
StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list) {
FrameStatsMap& stats_map = frame_stats_.Get(media_type);
const auto it = stats_map.find(event);
if (it != stats_map.end()) {
const Clock::duration duration = end_time - start_time_;
if (duration != Clock::duration::zero()) {
const int count = it->second.event_counter;
const double fps = (count / InMilliseconds(duration)) * 1000;
stats_list[static_cast<int>(stat)] = fps;
}
}
}
void StatisticsAnalyzer::PopulateAvgLatencyStat(
StatisticType stat,
StatisticsEvent::MediaType media_type,
SenderStats::StatisticsList& stats_list
) {
LatencyStatsMap& latency_map = latency_stats_.Get(media_type);
const auto it = latency_map.find(stat);
if (it != latency_map.end() && it->second.data_point_counter > 0) {
const double avg_latency =
InMilliseconds(it->second.sum_latency) / it->second.data_point_counter;
stats_list[static_cast<int>(stat)] = avg_latency;
}
}
void StatisticsAnalyzer::PopulateFrameBitrateStat(
StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list) {
FrameStatsMap& stats_map = frame_stats_.Get(media_type);
const auto it = stats_map.find(event);
if (it != stats_map.end()) {
const Clock::duration duration = end_time - start_time_;
if (duration != Clock::duration::zero()) {
const double kbps = it->second.sum_size / InMilliseconds(duration) * 8;
stats_list[static_cast<int>(stat)] = kbps;
}
}
}
void StatisticsAnalyzer::PopulatePacketBitrateStat(
StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list) {
PacketStatsMap& stats_map = packet_stats_.Get(media_type);
auto it = stats_map.find(event);
if (it != stats_map.end()) {
const Clock::duration duration = end_time - start_time_;
if (duration != Clock::duration::zero()) {
const double kbps = it->second.sum_size / InMilliseconds(duration) * 8;
stats_list[static_cast<int>(stat)] = kbps;
}
}
}
void StatisticsAnalyzer::PopulateSessionStats(
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list) {
SessionStats& session_stats = session_stats_.Get(media_type);
if (session_stats.first_event_time != Clock::time_point::min()) {
stats_list[static_cast<int>(StatisticType::kFirstEventTimeMs)] =
InMilliseconds(session_stats.first_event_time.time_since_epoch());
}
if (session_stats.last_event_time != Clock::time_point::min()) {
stats_list[static_cast<int>(StatisticType::kLastEventTimeMs)] =
InMilliseconds(session_stats.last_event_time.time_since_epoch());
}
if (session_stats.last_response_received_time != Clock::time_point::min()) {
stats_list[static_cast<int>(
StatisticType::kTimeSinceLastReceiverResponseMs)] =
InMilliseconds(end_time - session_stats.last_response_received_time);
}
stats_list[static_cast<int>(StatisticType::kNumLateFrames)] =
session_stats.late_frame_counter;
}
std::optional<Clock::time_point> StatisticsAnalyzer::ToSenderTimestamp(
Clock::time_point receiver_timestamp,
StatisticsEvent::MediaType media_type) const {
const std::optional<Clock::duration> receiver_offset =
offset_estimator_->GetEstimatedOffset();
if (!receiver_offset) {
return {};
}
return receiver_timestamp - *receiver_offset;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,208 @@
// 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.
#ifndef CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_
#define CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_
#include <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "cast/streaming/impl/clock_offset_estimator.h"
#include "cast/streaming/impl/statistics_collector.h"
#include "cast/streaming/public/statistics.h"
#include "platform/api/time.h"
#include "util/alarm.h"
#include "util/raw_ptr.h"
namespace openscreen::cast {
class StatisticsAnalyzer {
public:
StatisticsAnalyzer(SenderStatsClient* stats_client,
ClockNowFunctionPtr now,
TaskRunner& task_runner,
std::unique_ptr<ClockOffsetEstimator> offset_estimator);
~StatisticsAnalyzer();
void ScheduleAnalysis();
// Get the statistics collector managed by this analyzer.
StatisticsCollector* statistics_collector() {
return statistics_collector_.get();
}
private:
struct FrameStatsAggregate {
int event_counter;
uint32_t sum_size;
Clock::duration sum_delay;
};
struct PacketStatsAggregate {
int event_counter;
uint32_t sum_size;
};
struct LatencyStatsAggregate {
int data_point_counter;
Clock::duration sum_latency;
};
struct FrameInfo {
Clock::time_point capture_begin_time = Clock::time_point::min();
Clock::time_point capture_end_time = Clock::time_point::min();
Clock::time_point encode_end_time = Clock::time_point::min();
};
struct PacketInfo {
Clock::time_point timestamp;
StatisticsEvent::Type type;
};
struct SessionStats {
Clock::time_point first_event_time = Clock::time_point::max();
Clock::time_point last_event_time = Clock::time_point::min();
Clock::time_point last_response_received_time = Clock::time_point::min();
int late_frame_counter = 0;
};
// Named std::pair equivalent for audio + video classes.
template <typename T>
struct AVPair {
T audio;
T video;
const T& Get(StatisticsEvent::MediaType media_type) const {
if (media_type == StatisticsEvent::MediaType::kAudio) {
return audio;
}
OSP_CHECK(media_type == StatisticsEvent::MediaType::kVideo);
return video;
}
T& Get(StatisticsEvent::MediaType media_type) {
return const_cast<T&>(const_cast<const AVPair*>(this)->Get(media_type));
}
};
using FrameStatsMap = std::map<StatisticsEvent::Type, FrameStatsAggregate>;
using PacketStatsMap = std::map<StatisticsEvent::Type, PacketStatsAggregate>;
using LatencyStatsMap = std::map<StatisticType, LatencyStatsAggregate>;
using FrameInfoMap = std::map<RtpTimeTicks, FrameInfo>;
using PacketKey = std::pair<RtpTimeTicks, uint16_t>;
using PacketInfoMap = std::map<PacketKey, PacketInfo>;
// Initialize the stats histograms with the preferred min, max, and width.
void InitHistograms();
// Takes the Frame and Packet events from the `collector_`, and processes them
// into a form expected by `stats_client_`. Then sends the stats, and
// schedules a future analysis.
void AnalyzeStatistics();
// Constructs a stats list, and sends it to `stats_client_`;
void SendStatistics();
// Handles incoming stat events, and adds their infos to all of the proper
// stats maps / aggregates.
void ProcessFrameEvents(const std::vector<FrameEvent>& frame_events);
void ProcessPacketEvents(const std::vector<PacketEvent>& packet_events);
void RecordFrameLatencies(const FrameEvent& frame_event);
void RecordPacketLatencies(const PacketEvent& packet_event);
void RecordEventTimes(const StatisticsEvent& event);
void ErasePacketInfo(const PacketEvent& packet_event);
void AddToLatencyAggregrate(StatisticType latency_stat,
Clock::duration latency_delta,
StatisticsEvent::MediaType media_type);
void AddToHistogram(HistogramType histogram,
StatisticsEvent::MediaType media_type,
int64_t sample);
// Creates a stats list, and populates the entries based on stored stats info
// / aggregates for each stat field.
SenderStats::StatisticsList ConstructStatisticsList(
Clock::time_point end_time,
StatisticsEvent::MediaType media_type);
void PopulatePacketCountStat(StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
SenderStats::StatisticsList& stats_list);
void PopulateFrameCountStat(StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
SenderStats::StatisticsList& stats_list);
void PopulateFpsStat(StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list);
void PopulateAvgLatencyStat(StatisticType stat,
StatisticsEvent::MediaType media_type,
SenderStats::StatisticsList& stats_list);
void PopulateFrameBitrateStat(StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list);
void PopulatePacketBitrateStat(StatisticsEvent::Type event,
StatisticType stat,
StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list);
void PopulateSessionStats(StatisticsEvent::MediaType media_type,
Clock::time_point end_time,
SenderStats::StatisticsList& stats_list);
// Calculates the offset between the sender and receiver clocks and returns
// the sender-side version of this receiver timestamp, if possible.
std::optional<Clock::time_point> ToSenderTimestamp(
Clock::time_point receiver_timestamp,
StatisticsEvent::MediaType media_type) const;
// The statistics client to which we report analyzed statistics.
const raw_ptr<SenderStatsClient> stats_client_;
// The statistics collector from which we take the un-analyzed stats packets.
std::unique_ptr<StatisticsCollector> statistics_collector_;
// Keeps track of the best-guess clock offset between the sender and receiver.
std::unique_ptr<ClockOffsetEstimator> offset_estimator_;
// Keep track of time and events for this analyzer.
ClockNowFunctionPtr now_;
Alarm alarm_;
Clock::time_point start_time_;
// Maps of frame / packet infos used for stats that rely on seeing multiple
// events. For example, network latency is the calculated time difference
// between went a packet is sent, and when it is received.
AVPair<FrameInfoMap> recent_frame_infos_;
AVPair<PacketInfoMap> recent_packet_infos_;
// Aggregate statistics.
AVPair<FrameStatsMap> frame_stats_;
AVPair<PacketStatsMap> packet_stats_;
AVPair<LatencyStatsMap> latency_stats_;
// Stats that relate to the entirety of the session. For example, total late
// frames, or time of last event.
AVPair<SessionStats> session_stats_;
// Histograms.
AVPair<SenderStats::HistogramsList> histograms_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_

View file

@ -0,0 +1,74 @@
// 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/statistics_collector.h"
#include <stdint.h>
#include <limits>
#include <utility>
#include "cast/streaming/public/environment.h"
#include "util/big_endian.h"
namespace openscreen::cast {
StatisticsCollector::StatisticsCollector(ClockNowFunctionPtr now) : now_(now) {}
StatisticsCollector::~StatisticsCollector() = default;
void StatisticsCollector::CollectPacketSentEvent(ByteView packet,
PacketMetadata metadata) {
PacketEvent event;
// Populate the new PacketEvent by parsing the wire-format `packet`.
event.timestamp = now_();
event.type = StatisticsEvent::Type::kPacketSentToNetwork;
BigEndianReader reader(packet.data(), packet.size());
bool success = reader.Skip(4);
uint32_t truncated_rtp_timestamp = 0;
success &= reader.Read<uint32_t>(&truncated_rtp_timestamp);
success &= reader.Skip(4);
event.rtp_timestamp = metadata.rtp_timestamp.Expand(truncated_rtp_timestamp);
event.media_type = StatisticsEvent::ToMediaType(metadata.stream_type);
success &= reader.Skip(2);
success &= reader.Read<uint16_t>(&event.packet_id);
success &= reader.Read<uint16_t>(&event.max_packet_id);
// Check that the cast is safe.
// TODO(issuetracker.google.com/3576782): move to checked casts when ready.
static_assert(static_cast<uint64_t>(std::numeric_limits<uint32_t>::max()) <=
static_cast<uint64_t>(std::numeric_limits<size_t>::max()),
"invalid type cast assumption");
OSP_CHECK_LE(packet.size(),
static_cast<size_t>(std::numeric_limits<uint32_t>::max()));
event.size = static_cast<uint32_t>(packet.size());
OSP_CHECK(success);
recent_packet_events_.emplace_back(event);
}
void StatisticsCollector::CollectPacketEvent(PacketEvent event) {
recent_packet_events_.emplace_back(event);
}
void StatisticsCollector::CollectFrameEvent(FrameEvent event) {
recent_frame_events_.emplace_back(event);
}
std::vector<PacketEvent> StatisticsCollector::TakeRecentPacketEvents() {
std::vector<PacketEvent> out;
recent_packet_events_.swap(out);
return out;
}
std::vector<FrameEvent> StatisticsCollector::TakeRecentFrameEvents() {
std::vector<FrameEvent> out;
recent_frame_events_.swap(out);
return out;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,64 @@
// 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.
#ifndef CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_
#define CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_
#include <vector>
#include "cast/streaming/impl/statistics_common.h"
#include "platform/api/time.h"
#include "platform/base/span.h"
namespace openscreen::cast {
// This POD struct contains helpful information about a given packet that is
// not stored directly on the packet itself.
struct PacketMetadata {
// The stream type (audio, video, unknown) of this packet.
StreamType stream_type;
// The RTP timestamp associated with this packet.
RtpTimeTicks rtp_timestamp;
};
// This class is responsible for gathering packet and frame statistics using its
// Collect*() methods, that can then be taken by consumers using the Take*()
// methods.
class StatisticsCollector {
public:
explicit StatisticsCollector(ClockNowFunctionPtr now);
~StatisticsCollector();
// Informs the collector that a packet has been sent. The collector will then
// generate a packet event that is then added to `recent_packet_events_`.
void CollectPacketSentEvent(ByteView packet, PacketMetadata metadata);
// Informs the collector that a packet event has occurred. This event is then
// added to `recent_packet_events_`.
void CollectPacketEvent(PacketEvent event);
// Informs the collector that a frame event has occurred. This event is then
// added to `recent_frame_events_`.
void CollectFrameEvent(FrameEvent event);
// Returns the current collection of packet events stored in
// `recent_packet_events_`. After calling this method, `recent_packet_events_`
// is reset to an empty vector.
std::vector<PacketEvent> TakeRecentPacketEvents();
// Returns the current collection of frame events stored in
// `recent_frame_events_`. After calling this method, `recent_frame_events_`
// is reset to an empty vector.
std::vector<FrameEvent> TakeRecentFrameEvents();
private:
ClockNowFunctionPtr now_;
std::vector<PacketEvent> recent_packet_events_;
std::vector<FrameEvent> recent_frame_events_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_

View file

@ -0,0 +1,115 @@
// 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/statistics_common.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
// static
StatisticsEvent::Type StatisticsEvent::FromWireType(WireType wire_type) {
switch (wire_type) {
case WireType::kAudioAckSent:
case WireType::kVideoAckSent:
case WireType::kUnifiedAckSent:
return Type::kFrameAckSent;
case WireType::kAudioPlayoutDelay:
case WireType::kVideoRenderDelay:
case WireType::kUnifiedRenderDelay:
return Type::kFramePlayedOut;
case WireType::kAudioFrameDecoded:
case WireType::kVideoFrameDecoded:
case WireType::kUnifiedFrameDecoded:
return Type::kFrameDecoded;
case WireType::kAudioPacketReceived:
case WireType::kVideoPacketReceived:
case WireType::kUnifiedPacketReceived:
return Type::kPacketReceived;
default:
OSP_VLOG << "Unexpected RTCP log message received: "
<< static_cast<int>(wire_type);
return Type::kUnknown;
}
}
// static
StatisticsEvent::WireType StatisticsEvent::ToWireType(Type type) {
switch (type) {
case Type::kUnknown:
return WireType::kUnknown;
case Type::kFrameAckSent:
return WireType::kUnifiedAckSent;
case Type::kFramePlayedOut:
return WireType::kUnifiedRenderDelay;
case Type::kFrameDecoded:
return WireType::kUnifiedFrameDecoded;
case Type::kPacketReceived:
return WireType::kUnifiedPacketReceived;
default:
OSP_VLOG << "Unknown RTCP log message event type: "
<< static_cast<int>(type);
return WireType::kUnknown;
}
}
// static
StatisticsEvent::MediaType StatisticsEvent::ToMediaType(StreamType type) {
switch (type) {
case StreamType::kUnknown:
return MediaType::kUnknown;
case StreamType::kAudio:
return MediaType::kAudio;
case StreamType::kVideo:
return MediaType::kVideo;
}
OSP_NOTREACHED();
}
StatisticsEvent::StatisticsEvent(const StatisticsEvent& other) = default;
StatisticsEvent::StatisticsEvent(StatisticsEvent&& other) noexcept = default;
StatisticsEvent& StatisticsEvent::operator=(const StatisticsEvent& other) =
default;
StatisticsEvent& StatisticsEvent::operator=(StatisticsEvent&& other) = default;
bool StatisticsEvent::operator==(const StatisticsEvent& other) const {
return frame_id == other.frame_id && type == other.type &&
media_type == other.media_type &&
rtp_timestamp == other.rtp_timestamp && size == other.size &&
timestamp == other.timestamp &&
received_timestamp == other.received_timestamp;
}
FrameEvent::FrameEvent(const FrameEvent& other) = default;
FrameEvent::FrameEvent(FrameEvent&& other) noexcept = default;
FrameEvent& FrameEvent::operator=(const FrameEvent& other) = default;
FrameEvent& FrameEvent::operator=(FrameEvent&& other) = default;
bool FrameEvent::operator==(const FrameEvent& other) const {
return StatisticsEvent::operator==(other) && width == other.width &&
height == other.height && delay_delta == other.delay_delta &&
key_frame == other.key_frame && target_bitrate == other.target_bitrate;
}
PacketEvent::PacketEvent(const PacketEvent& other) = default;
PacketEvent::PacketEvent(PacketEvent&& other) noexcept = default;
PacketEvent& PacketEvent::operator=(const PacketEvent& other) = default;
PacketEvent& PacketEvent::operator=(PacketEvent&& other) = default;
bool PacketEvent::operator==(const PacketEvent& other) const {
return StatisticsEvent::operator==(other) && packet_id == other.packet_id &&
max_packet_id == other.max_packet_id;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,223 @@
// 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.
#ifndef CAST_STREAMING_IMPL_STATISTICS_COMMON_H_
#define CAST_STREAMING_IMPL_STATISTICS_COMMON_H_
#include <stddef.h>
#include <stdint.h>
#include "cast/streaming/public/constants.h"
#include "cast/streaming/public/frame_id.h"
#include "cast/streaming/rtp_time.h"
#include "platform/api/time.h"
namespace openscreen::cast {
struct StatisticsEvent {
enum class Type : int {
kUnknown = 0,
// Sender side frame events.
kFrameCaptureBegin = 1,
kFrameCaptureEnd = 2,
kFrameEncoded = 3,
kFrameAckReceived = 4,
// Receiver side frame events.
kFrameAckSent = 5,
kFrameDecoded = 6,
kFramePlayedOut = 7,
// Sender side packet events.
kPacketSentToNetwork = 8,
kPacketRetransmitted = 9,
kPacketRtxRejected = 10,
// Receiver side packet events.
kPacketReceived = 11,
kFrameDroppedByEncoder = 15,
kNumOfEvents = kFrameDroppedByEncoder + 1
};
// Serialized values for the statistics events for use by the RTCP builder
// and parser logic. *Do not modify existing values* since they are shared by
// both libcast-based devices as well as a variety of legacy implementations.
//
// NOTE: Events 1 to 8 have been replaced with events 11 to 14 (e.g.
// kAudioAckSent and kVideoAckSent merged into a single event kAckSent).
// Events 9 and 10 (to log duplicated packets) have been fully removed. Future
// events may reuse those values.
enum class WireType : uint8_t {
kUnknown = 0,
// Legacy audio event types.
kAudioAckSent = 1,
kAudioPlayoutDelay = 2,
kAudioFrameDecoded = 3,
kAudioPacketReceived = 4,
// Legacy video event types.
kVideoAckSent = 5,
kVideoRenderDelay = 6,
kVideoFrameDecoded = 7,
kVideoPacketReceived = 8,
// New unified event types.
kUnifiedAckSent = 11,
kUnifiedRenderDelay = 12,
kUnifiedFrameDecoded = 13,
kUnifiedPacketReceived = 14,
kNumOfEvents = kUnifiedPacketReceived + 1
};
enum class MediaType : int { kUnknown = 0, kAudio = 1, kVideo = 2 };
static Type FromWireType(WireType wire_type);
static WireType ToWireType(Type type);
static MediaType ToMediaType(StreamType type);
constexpr StatisticsEvent(FrameId frame_id,
Type type,
MediaType media_type,
RtpTimeTicks rtp_timestamp,
uint32_t size,
Clock::time_point timestamp,
Clock::time_point received_timestamp)
: frame_id(frame_id),
type(type),
media_type(media_type),
rtp_timestamp(rtp_timestamp),
size(size),
timestamp(timestamp),
received_timestamp(received_timestamp) {}
constexpr StatisticsEvent() = default;
StatisticsEvent(const StatisticsEvent& other);
StatisticsEvent(StatisticsEvent&& other) noexcept;
StatisticsEvent& operator=(const StatisticsEvent& other);
StatisticsEvent& operator=(StatisticsEvent&& other);
~StatisticsEvent() = default;
bool operator==(const StatisticsEvent& other) const;
// The frame this event is associated with.
FrameId frame_id;
// The type of this frame event.
Type type = Type::kUnknown;
// Whether this was audio or video (or unknown).
MediaType media_type = MediaType::kUnknown;
// The RTP timestamp of the frame this event is associated with.
RtpTimeTicks rtp_timestamp;
// Size of this packet, or the frame it is associated with.
// Note: we use uint32_t instead of size_t for byte count because this struct
// is sent over IPC which could span 32 & 64 bit processes.
uint32_t size = 0;
// Time of event logged.
Clock::time_point timestamp;
// Time that the event was received by the sender. Only set for receiver-side
// events.
Clock::time_point received_timestamp;
};
struct FrameEvent : public StatisticsEvent {
constexpr FrameEvent(FrameId frame_id_in,
Type type_in,
MediaType media_type_in,
RtpTimeTicks rtp_timestamp_in,
uint32_t size_in,
Clock::time_point timestamp_in,
Clock::time_point received_timestamp_in,
int width,
int height,
Clock::duration delay_delta,
bool key_frame,
int target_bitrate)
: StatisticsEvent(frame_id_in,
type_in,
media_type_in,
rtp_timestamp_in,
size_in,
timestamp_in,
received_timestamp_in),
width(width),
height(height),
delay_delta(delay_delta),
key_frame(key_frame),
target_bitrate(target_bitrate) {}
constexpr FrameEvent() = default;
FrameEvent(const FrameEvent& other);
FrameEvent(FrameEvent&& other) noexcept;
FrameEvent& operator=(const FrameEvent& other);
FrameEvent& operator=(FrameEvent&& other);
~FrameEvent() = default;
bool operator==(const FrameEvent& other) const;
// Resolution of the frame. Only set for video FRAME_CAPTURE_END events.
int width = 0;
int height = 0;
// Only set for FRAME_PLAYOUT events.
// If this value is zero the frame is rendered on time.
// If this value is positive it means the frame is rendered late.
// If this value is negative it means the frame is rendered early.
Clock::duration delay_delta{};
// Whether the frame is a key frame. Only set for video FRAME_ENCODED event.
bool key_frame = false;
// The requested target bitrate of the encoder at the time the frame is
// encoded. Only set for video FRAME_ENCODED event.
int target_bitrate = 0;
};
struct PacketEvent : public StatisticsEvent {
constexpr PacketEvent(FrameId frame_id_in,
Type type_in,
MediaType media_type_in,
RtpTimeTicks rtp_timestamp_in,
uint32_t size_in,
Clock::time_point timestamp_in,
Clock::time_point received_timestamp_in,
uint16_t packet_id,
uint16_t max_packet_id)
: StatisticsEvent(frame_id_in,
type_in,
media_type_in,
rtp_timestamp_in,
size_in,
timestamp_in,
received_timestamp_in),
packet_id(packet_id),
max_packet_id(max_packet_id) {}
constexpr PacketEvent() = default;
PacketEvent(const PacketEvent& other);
PacketEvent(PacketEvent&& other) noexcept;
PacketEvent& operator=(const PacketEvent& other);
PacketEvent& operator=(PacketEvent&& other);
~PacketEvent() = default;
bool operator==(const PacketEvent& other) const;
// The packet this event is associated with.
uint16_t packet_id = 0;
// The highest packet ID seen so far at time of event.
uint16_t max_packet_id = 0;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_COMMON_H_

View file

@ -0,0 +1,163 @@
// Copyright 2025 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/statistics_dispatcher.h"
#include <utility>
#include "cast/streaming/impl/rtcp_common.h"
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/impl/statistics_collector.h"
#include "cast/streaming/impl/statistics_common.h"
#include "cast/streaming/public/encoded_frame.h"
#include "cast/streaming/public/environment.h"
#include "cast/streaming/public/session_config.h"
#include "platform/base/trivial_clock_traits.h"
#include "util/chrono_helpers.h"
#include "util/osp_logging.h"
#include "util/std_util.h"
#include "util/trace_logging.h"
namespace openscreen::cast {
using clock_operators::operator<<;
StatisticsDispatcher::StatisticsDispatcher(Environment& environment)
: environment_(environment) {}
StatisticsDispatcher::~StatisticsDispatcher() = default;
void StatisticsDispatcher::DispatchEnqueueEvents(StreamType stream_type,
const EncodedFrame& frame) {
if (!environment_->statistics_collector()) {
return;
}
const auto media_type = StatisticsEvent::ToMediaType(stream_type);
// Submit a capture begin event.
FrameEvent capture_begin_event;
capture_begin_event.type = StatisticsEvent::Type::kFrameCaptureBegin;
capture_begin_event.media_type = media_type;
capture_begin_event.rtp_timestamp = frame.rtp_timestamp;
capture_begin_event.timestamp =
(frame.capture_begin_time > Clock::time_point::min())
? frame.capture_begin_time
: environment_->now();
environment_->statistics_collector()->CollectFrameEvent(
std::move(capture_begin_event));
// Submit a capture end event.
FrameEvent capture_end_event;
capture_end_event.type = StatisticsEvent::Type::kFrameCaptureEnd;
capture_end_event.media_type = media_type;
capture_end_event.rtp_timestamp = frame.rtp_timestamp;
capture_end_event.timestamp =
(frame.capture_end_time > Clock::time_point::min())
? frame.capture_end_time
: environment_->now();
environment_->statistics_collector()->CollectFrameEvent(
std::move(capture_end_event));
// Submit an encoded event.
FrameEvent encode_event;
encode_event.timestamp = environment_->now();
encode_event.type = StatisticsEvent::Type::kFrameEncoded;
encode_event.media_type = media_type;
encode_event.rtp_timestamp = frame.rtp_timestamp;
encode_event.frame_id = frame.frame_id;
encode_event.size = static_cast<uint32_t>(frame.data.size());
encode_event.key_frame =
frame.dependency == openscreen::cast::EncodedFrame::Dependency::kKeyFrame;
environment_->statistics_collector()->CollectFrameEvent(
std::move(encode_event));
}
void StatisticsDispatcher::DispatchAckEvent(StreamType stream_type,
RtpTimeTicks rtp_timestamp,
FrameId frame_id) {
if (!environment_->statistics_collector()) {
return;
}
FrameEvent ack_event;
ack_event.timestamp = environment_->now();
ack_event.type = StatisticsEvent::Type::kFrameAckReceived;
ack_event.media_type = StatisticsEvent::ToMediaType(stream_type);
ack_event.rtp_timestamp = rtp_timestamp;
ack_event.frame_id = frame_id;
environment_->statistics_collector()->CollectFrameEvent(std::move(ack_event));
}
void StatisticsDispatcher::DispatchFrameDropEvent(StreamType stream_type,
FrameId frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point drop_time) {
if (!environment_->statistics_collector()) {
return;
}
FrameEvent drop_event;
drop_event.timestamp = drop_time;
drop_event.type = StatisticsEvent::Type::kFrameDroppedByEncoder;
drop_event.media_type = StatisticsEvent::ToMediaType(stream_type);
drop_event.rtp_timestamp = rtp_timestamp;
drop_event.frame_id = frame_id;
environment_->statistics_collector()->CollectFrameEvent(
std::move(drop_event));
}
void StatisticsDispatcher::DispatchFrameLogMessages(
StreamType stream_type,
const std::vector<RtcpReceiverFrameLogMessage>& messages) {
if (!environment_->statistics_collector()) {
return;
}
const Clock::time_point now = environment_->now();
const auto media_type = StatisticsEvent::ToMediaType(stream_type);
for (const RtcpReceiverFrameLogMessage& log_message : messages) {
for (const RtcpReceiverEventLogMessage& event_message :
log_message.messages) {
switch (event_message.type) {
case StatisticsEvent::Type::kPacketReceived: {
PacketEvent event;
event.timestamp = event_message.timestamp;
event.received_timestamp = now;
event.type = event_message.type;
event.media_type = media_type;
event.rtp_timestamp = log_message.rtp_timestamp;
event.packet_id = event_message.packet_id;
environment_->statistics_collector()->CollectPacketEvent(
std::move(event));
} break;
case StatisticsEvent::Type::kFrameAckSent:
case StatisticsEvent::Type::kFrameDecoded:
case StatisticsEvent::Type::kFramePlayedOut: {
FrameEvent event;
event.timestamp = event_message.timestamp;
event.received_timestamp = now;
event.type = event_message.type;
event.media_type = media_type;
event.rtp_timestamp = log_message.rtp_timestamp;
if (event.type == StatisticsEvent::Type::kFramePlayedOut) {
event.delay_delta = event_message.delay;
}
environment_->statistics_collector()->CollectFrameEvent(
std::move(event));
} break;
default:
OSP_VLOG << "Received log message via RTCP that we did not expect, "
"StatisticsEvent::Type="
<< static_cast<int>(event_message.type);
break;
}
}
}
}
} // namespace openscreen::cast

View file

@ -0,0 +1,58 @@
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_
#define CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_
#include <vector>
#include "cast/streaming/impl/statistics_common.h"
#include "platform/api/time.h"
#include "platform/base/span.h"
#include "util/raw_ref.h"
namespace openscreen::cast {
class StatisticsCollector;
class Environment;
struct EncodedFrame;
struct RtcpReceiverFrameLogMessage;
// This class is responsible for dispatching statistics events.
class StatisticsDispatcher {
public:
explicit StatisticsDispatcher(Environment& environment);
StatisticsDispatcher(const StatisticsDispatcher&) = delete;
StatisticsDispatcher& operator=(const StatisticsDispatcher&) = delete;
StatisticsDispatcher(StatisticsDispatcher&&) noexcept = delete;
StatisticsDispatcher& operator=(StatisticsDispatcher&&) = delete;
~StatisticsDispatcher();
// Dispatches enqueue events for a given frame.
void DispatchEnqueueEvents(StreamType stream_type, const EncodedFrame& frame);
// Dispatches frame log messages.
void DispatchFrameLogMessages(
StreamType stream_type,
const std::vector<RtcpReceiverFrameLogMessage>& messages);
// Dispatches an ack event.
void DispatchAckEvent(StreamType stream_type,
RtpTimeTicks rtp_timestamp,
FrameId frame_id);
// Dispatches a frame dropped by encoder event.
void DispatchFrameDropEvent(StreamType stream_type,
FrameId frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point drop_time);
private:
const raw_ref<Environment> environment_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_