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,27 @@
// Copyright 2018 The Chromium Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google LLC nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,64 @@
# Vendoring notes
This directory is a **pruned subset** of [chromium/openscreen](https://chromium.googlesource.com/openscreen),
pinned at the commit in `PINNED_COMMIT`, licensed BSD-3-Clause (see `LICENSE`).
It contains only the files needed for the Cast Streaming *sender* data path
(RTP/RTCP send, OFFER/ANSWER message types, AES frame encryption) plus the
POSIX platform/util plumbing that path needs — not the receiver, not OSP
discovery, not remoting/RPC, not TLS (breadcast's TLS CASTV2 control channel
is handled by the existing `rust_cast`-based Rust code; this vendored code
only drives the UDP RTP/RTCP data path once `rust_cast` has already
negotiated OFFER/ANSWER and we know where to send packets).
It is compiled directly via the `cc` crate in `../build.rs`, not GN/Ninja —
there is no build system here to regenerate anything from.
## Rolling the pin
There are no release/API-stability guarantees upstream. To update:
1. Re-run the file selection against the new commit's `cast/streaming/BUILD.gn`
(`:common` + `:sender` targets, minus `public/sender_session.*` and
`public/rpc_messenger.*` — see "What's excluded" below) plus
`platform/BUILD.gn`'s `:base`/`:api`/`:standalone_impl` targets.
2. Re-apply the patches below (they're small; check if upstream has since
fixed the same problem and the patch can be dropped).
3. Update `PINNED_COMMIT` and rebuild.
## What's excluded and why
- **`public/sender_session.{h,cc}`, `public/rpc_messenger.{h,cc}`,
`public/receiver_session.*`, receiver-side files, `remoting.proto`/
`input.proto`** — `SenderSession` bundles mirroring negotiation together
with RPC/remoting/input support, which pulls in protobuf
(`input.pb.h`/`remoting.pb.h`) for no benefit here (breadcast only ever
does one-way video mirroring). Instead, `../src/session.cc` drives
`Offer`/`Answer`/`SessionConfig` directly — logic adapted from
`sender_session.cc`'s `CreateMirroringOffer`/`StartNegotiation`/
`SelectSenders`, minus everything RPC/remoting/audio/input-related.
- **All TLS support** (`platform/impl/tls_*`, `platform/impl/stream_socket*`) —
unused; see above.
- **`util/crypto/{certificate_utils,digest_sign,pem_helpers,rsa_private_key,
secure_hash,sha2}.*`** — only needed for OSP discovery / X.509 certificate
handling, not the RTP data path.
- **`util/scoped_wake_lock_mac.cc`** — macOS-only.
## Local patches (not upstream)
1. **`build/build_config.h`, `build/buildflag.h`** — stand-ins for Chromium's
GN-generated versions of these headers. Only define what the 3 callers
here actually check (`IS_POSIX`, `IS_LINUX`, `IS_APPLE`, `IS_ANDROID`),
hardcoded for Linux.
2. **`patches/aes_ctr128_compat.cc`** — `frame_crypto.cc` calls
`AES_ctr128_encrypt()`, a BoringSSL convenience wrapper not in system
OpenSSL's public headers. Reimplemented from scratch (standard CTR mode
over `AES_encrypt()`, which OpenSSL does still expose).
3. **`platform/impl/platform_client_posix.{h,cc}`** — stripped the
`TlsDataRouterPosix` member/accessor (see "All TLS support" above).
4. **`util/crypto/openssl_util.{h,cc}`** — dropped `SSLErrorCodeToError()`/
`GetSSLError()`, which reference BoringSSL's `SSL_error_description()`
(not in system OpenSSL). Unused for the same reason as (3).
5. **`util/base64.cc`** — upstream implements this on
`third_party/modp_b64`, which isn't fetched by a plain shallow clone
(pulled in separately via gclient/DEPS in a full Chromium checkout).
Reimplemented on `EVP_EncodeBlock`/`EVP_DecodeBlock` from system OpenSSL
instead, same public interface.

View file

@ -0,0 +1 @@
920129874c16365415a8a52fdbbc492d7623dc11

View file

@ -0,0 +1,19 @@
// Minimal stand-in for Chromium's build/build_config.h, providing just the
// BUILDFLAG(IS_*) values this vendored subset of openscreen actually checks
// (see ip_address.cc, logging_posix.cc, udp_socket_posix.cc). breadcast only
// ever builds this for Linux, so everything else is hardcoded off. Not part
// of upstream openscreen.
#ifndef BUILD_BUILD_CONFIG_H_
#define BUILD_BUILD_CONFIG_H_
#include "build/buildflag.h"
#define BUILDFLAG_IS_LINUX() (1)
#define BUILDFLAG_IS_POSIX() (1)
#define BUILDFLAG_IS_APPLE() (0)
#define BUILDFLAG_IS_ANDROID() (0)
#define BUILDFLAG_IS_WIN() (0)
#define BUILDFLAG_IS_CHROMEOS() (0)
#define BUILDFLAG_IS_IOS() (0)
#endif // BUILD_BUILD_CONFIG_H_

View file

@ -0,0 +1,17 @@
// Minimal stand-in for Chromium's generated build/buildflag.h.
//
// The real header is produced per-target by GN's buildflag_header() rule,
// which generates a `BUILDFLAG_<FLAG>()` macro per flag; BUILDFLAG(flag)
// then token-pastes "BUILDFLAG_" with the flag name and calls it. Since this
// vendored subset is compiled directly with the `cc` crate rather than
// GN/Ninja, build_config.h defines those BUILDFLAG_<FLAG>() macros directly
// for the Linux target breadcast always builds for. Not part of upstream
// openscreen.
#ifndef BUILD_BUILDFLAG_H_
#define BUILD_BUILDFLAG_H_
#define BUILDFLAG_CAT_INDIRECT(a, b) a##b
#define BUILDFLAG_CAT(a, b) BUILDFLAG_CAT_INDIRECT(a, b)
#define BUILDFLAG(flag) (BUILDFLAG_CAT(BUILDFLAG_, flag)())
#endif // BUILD_BUILDFLAG_H_

View file

@ -0,0 +1,52 @@
// 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_COMMON_PUBLIC_MESSAGE_PORT_H_
#define CAST_COMMON_PUBLIC_MESSAGE_PORT_H_
#include <string>
#include "platform/base/error.h"
namespace openscreen::cast {
// This interface is intended to provide an abstraction for communicating
// cast messages across a pipe with guaranteed delivery. This is used to
// decouple the cast streaming receiver and sender sessions from the
// network implementation.
class MessagePort {
public:
class Client {
public:
// Called whenever a message arrives on the message port.
virtual void OnMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message) = 0;
// Called whenever an error occurs on the message port.
virtual void OnError(const Error& error) = 0;
// Clients should expose a unique identifier used as the "source" of
// all messages sent on this message port.
virtual const std::string& source_id() = 0;
protected:
virtual ~Client() = default;
};
virtual ~MessagePort() = default;
// Set or reset the `MessagePort::Client` for this instance.
virtual void SetClient(Client& client) = 0;
virtual void ResetClient() = 0;
// Sends a message to a given `destination_id`.
virtual void PostMessage(const std::string& destination_id,
const std::string& message_namespace,
const std::string& message) = 0;
};
} // namespace openscreen::cast
#endif // CAST_COMMON_PUBLIC_MESSAGE_PORT_H_

View file

@ -0,0 +1,81 @@
// 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_CAPTURE_CONFIGS_H_
#define CAST_STREAMING_CAPTURE_CONFIGS_H_
#include <string>
#include <vector>
#include "cast/streaming/public/constants.h"
#include "cast/streaming/resolution.h"
#include "util/simple_fraction.h"
namespace openscreen::cast {
// A configuration set that can be used by the sender to capture audio, and the
// receiver to playback audio. Used by Cast Streaming to provide an offer to the
// receiver.
struct AudioCaptureConfig {
// Audio codec represented by this configuration.
AudioCodec codec = AudioCodec::kOpus;
// Number of channels used by this configuration.
int channels = kDefaultAudioChannels;
// Average bit rate in bits per second used by this configuration. A value
// of "zero" suggests that the bitrate should be automatically selected by
// the sender.
int bit_rate = 0;
// Sample rate for audio RTP timebase.
int sample_rate = kDefaultAudioSampleRate;
// Target playout delay in milliseconds.
std::chrono::milliseconds target_playout_delay = kDefaultTargetPlayoutDelay;
// The codec parameter for this configuration. Honors the format laid out
// in RFC 6381: https://datatracker.ietf.org/doc/html/rfc6381
// NOTE: the "profiles" parameter is not supported in our implementation.
std::string codec_parameter;
};
// A configuration set that can be used by the sender to capture video, as
// well as the receiver to playback video. Used by Cast Streaming to provide an
// offer to the receiver.
struct VideoCaptureConfig {
// Video codec represented by this configuration.
VideoCodec codec = VideoCodec::kVp8;
// Maximum frame rate in frames per second.
// For simple cases, the frame rate may be provided by simply setting the
// number to the desired value, e.g. 30 or 60FPS. Some common frame rates like
// 23.98 FPS (for NTSC compatibility) are represented as fractions, in this
// case 24000/1001.
SimpleFraction max_frame_rate{kDefaultFrameRate, 1};
// Number specifying the maximum bit rate for this stream. A value of
// zero means that the maximum bit rate should be automatically selected by
// the sender.
int max_bit_rate = 0;
// Resolutions to be offered to the receiver. At least one resolution
// must be provided.
std::vector<Resolution> resolutions;
// Target playout delay in milliseconds.
std::chrono::milliseconds target_playout_delay = kDefaultTargetPlayoutDelay;
// The codec parameter for this configuration. Honors the format laid out
// in RFC 6381: https://datatracker.ietf.org/doc/html/rfc6381.
// VP8 and VP9 codec parameter versions are defined here:
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#webm
// https://www.webmproject.org/vp9/mp4/#codecs-parameter-string
// NOTE: the "profiles" parameter is not supported in our implementation.
std::string codec_parameter;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_CAPTURE_CONFIGS_H_

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_

View file

@ -0,0 +1,47 @@
// 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/message_fields.h"
#include <array>
#include <utility>
#include "util/enum_name_table.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
namespace {
constexpr EnumNameTable<AudioCodec, 3> kAudioCodecNames{
{{"aac", AudioCodec::kAac},
{"opus", AudioCodec::kOpus},
{"REMOTE_AUDIO", AudioCodec::kNotSpecified}}};
constexpr EnumNameTable<VideoCodec, 6> kVideoCodecNames{
{{"h264", VideoCodec::kH264},
{"vp8", VideoCodec::kVp8},
{"hevc", VideoCodec::kHevc},
{"REMOTE_VIDEO", VideoCodec::kNotSpecified},
{"vp9", VideoCodec::kVp9},
{"av1", VideoCodec::kAv1}}};
} // namespace
const char* CodecToString(AudioCodec codec) {
return GetEnumName(kAudioCodecNames, codec).value();
}
ErrorOr<AudioCodec> StringToAudioCodec(std::string_view name) {
return GetEnum(kAudioCodecNames, name);
}
const char* CodecToString(VideoCodec codec) {
return GetEnumName(kVideoCodecNames, codec).value();
}
ErrorOr<VideoCodec> StringToVideoCodec(std::string_view name) {
return GetEnum(kVideoCodecNames, name);
}
} // namespace openscreen::cast

View file

@ -0,0 +1,59 @@
// 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_MESSAGE_FIELDS_H_
#define CAST_STREAMING_MESSAGE_FIELDS_H_
#include <string>
#include <string_view>
#include "cast/streaming/public/constants.h"
#include "platform/base/error.h"
namespace openscreen::cast {
/// NOTE: Constants here are all taken from the Cast V2: Mirroring Control
/// Protocol specification.
// Namespace for OFFER/ANSWER messages.
inline constexpr char kCastWebrtcNamespace[] =
"urn:x-cast:com.google.cast.webrtc";
inline constexpr char kCastRemotingNamespace[] =
"urn:x-cast:com.google.cast.remoting";
// JSON message field values specific to the Sender Session.
inline constexpr char kMessageType[] = "type";
// List of OFFER message fields.
inline constexpr char kMessageTypeOffer[] = "OFFER";
inline constexpr char kOfferMessageBody[] = "offer";
inline constexpr char kSequenceNumber[] = "seqNum";
inline constexpr char kCodecName[] = "codecName";
/// ANSWER message fields.
inline constexpr char kMessageTypeAnswer[] = "ANSWER";
inline constexpr char kAnswerMessageBody[] = "answer";
inline constexpr char kResult[] = "result";
inline constexpr char kResultOk[] = "ok";
inline constexpr char kResultError[] = "error";
inline constexpr char kErrorMessageBody[] = "error";
inline constexpr char kErrorCode[] = "code";
inline constexpr char kErrorDescription[] = "description";
// Other message fields.
inline constexpr char kRpcMessageBody[] = "rpc";
inline constexpr char kInputMessageBody[] = "input";
inline constexpr char kCapabilitiesMessageBody[] = "capabilities";
inline constexpr char kStatusMessageBody[] = "status";
// Conversion methods for codec message fields.
const char* CodecToString(AudioCodec codec);
ErrorOr<AudioCodec> StringToAudioCodec(std::string_view name);
const char* CodecToString(VideoCodec codec);
ErrorOr<VideoCodec> StringToVideoCodec(std::string_view name);
} // namespace openscreen::cast
#endif // CAST_STREAMING_MESSAGE_FIELDS_H_

View file

@ -0,0 +1,498 @@
// 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/public/answer_messages.h"
#include <string_view>
#include <utility>
#include "cast/streaming/public/constants.h"
#include "platform/base/error.h"
#include "util/enum_name_table.h"
#include "util/json/json_helpers.h"
#include "util/osp_logging.h"
#include "util/string_parse.h"
#include "util/string_util.h"
#include "util/stringprintf.h"
namespace openscreen::cast {
namespace {
/// Constraint properties.
// Audio constraints. See properties below.
constexpr char kAudio[] = "audio";
// Video constraints. See properties below.
constexpr char kVideo[] = "video";
// An optional field representing the minimum bits per second. If not specified
// by the receiver, the sender will use kDefaultAudioMinBitRate and
// kDefaultVideoMinBitRate, which represent the true operational minimum.
constexpr char kMinBitRate[] = "minBitRate";
// Maximum encoded bits per second. This is the lower of (1) the max capability
// of the decoder, or (2) the max data transfer rate.
constexpr char kMaxBitRate[] = "maxBitRate";
// Maximum supported end-to-end latency, in milliseconds. Proportional to the
// size of the data buffers in the receiver.
constexpr char kMaxDelay[] = "maxDelay";
/// Video constraint properties.
// Maximum pixel rate (width * height * framerate). Is often less than
// multiplying the fields in maxDimensions. This field is used to set the
// maximum processing rate.
constexpr char kMaxPixelsPerSecond[] = "maxPixelsPerSecond";
// Minimum dimensions. If omitted, the sender will assume a reasonable minimum
// with the same aspect ratio as maxDimensions, as close to 320*180 as possible.
// Should reflect the true operational minimum.
constexpr char kMinResolution[] = "minResolution";
// Maximum dimensions, not necessarily ideal dimensions.
constexpr char kMaxDimensions[] = "maxDimensions";
/// Audio constraint properties.
// Maximum supported sampling frequency (not necessarily ideal).
constexpr char kMaxSampleRate[] = "maxSampleRate";
// Maximum number of audio channels (1 is mono, 2 is stereo, etc.).
constexpr char kMaxChannels[] = "maxChannels";
/// Display description properties
// If this optional field is included in the ANSWER message, the receiver is
// attached to a fixed display that has the given dimensions and frame rate
// configuration. These may exceed, be the same, or be less than the values in
// constraints. If undefined, we assume the display is not fixed (e.g. a Google
// Hangouts UI panel).
constexpr char kDimensions[] = "dimensions";
// An optional field. When missing and dimensions are specified, the sender
// will assume square pixels and the dimensions imply the aspect ratio of the
// fixed display. WHen present and dimensions are also specified, implies the
// pixels are not square.
constexpr char kAspectRatio[] = "aspectRatio";
// The delimeter used for the aspect ratio format ("A:B").
constexpr char kAspectRatioDelimiter = ':';
// Sets the aspect ratio constraints. Value must be either "sender" or
// "receiver", see kScalingSender and kScalingReceiver below.
constexpr char kScaling[] = "scaling";
// scaling = "sender" means that the sender must provide video frames of a fixed
// aspect ratio. In this case, the dimensions object must be passed or an error
// case will occur.
constexpr char kScalingSender[] = "sender";
// scaling = "receiver" means that the sender may send arbitrarily sized frames,
// and the receiver will handle scaling and letterboxing as necessary.
constexpr char kScalingReceiver[] = "receiver";
/// Answer properties.
// A number specifying the UDP port used for all streams in this session.
// Must have a value between kUdpPortMin and kUdpPortMax.
constexpr char kUdpPort[] = "udpPort";
constexpr int kUdpPortMin = 1;
constexpr int kUdpPortMax = 65535;
// Numbers specifying the indexes chosen from the offer message.
constexpr char kSendIndexes[] = "sendIndexes";
// uint32_t values specifying the RTP SSRC values used to send the RTCP feedback
// of the stream indicated in kSendIndexes.
constexpr char kSsrcs[] = "ssrcs";
// Provides detailed maximum and minimum capabilities of the receiver for
// processing the selected streams. The sender may alter video resolution and
// frame rate throughout the session, and the constraints here determine how
// much data volume is allowed.
constexpr char kConstraints[] = "constraints";
// Provides details about the display on the receiver.
constexpr char kDisplay[] = "display";
// std::optional array of numbers specifying the indexes of streams that will
// send event logs through RTCP.
constexpr char kReceiverRtcpEventLog[] = "receiverRtcpEventLog";
// Optional array of numbers specifying the indexes of streams that will use
// DSCP values specified in the OFFER message for RTCP packets.
constexpr char kReceiverRtcpDscp[] = "receiverRtcpDscp";
// If this optional field is present the receiver supports the specific
// RTP extensions (such as adaptive playout delay).
constexpr char kRtpExtensions[] = "rtpExtensions";
EnumNameTable<AspectRatioConstraint, 2> kAspectRatioConstraintNames{
{{kScalingReceiver, AspectRatioConstraint::kVariable},
{kScalingSender, AspectRatioConstraint::kFixed}}};
Json::Value AspectRatioConstraintToJson(AspectRatioConstraint aspect_ratio) {
return Json::Value(GetEnumName(kAspectRatioConstraintNames, aspect_ratio)
.value(kScalingSender));
}
std::optional<AspectRatioConstraint> TryParseAspectRatioConstraint(
const Json::Value& value) {
std::string aspect_ratio;
if (!json::TryParseString(value, &aspect_ratio)) {
return std::nullopt;
}
ErrorOr<AspectRatioConstraint> constraint =
GetEnum(kAspectRatioConstraintNames, aspect_ratio);
if (constraint.is_error()) {
return std::nullopt;
}
return constraint.value();
}
template <typename T>
ErrorOr<std::optional<T>> ParseOptional(const Json::Value& value) {
if (!value) {
return std::optional<T>{};
}
auto out = T::TryParse(value);
if (out.is_error()) {
return out.error();
}
return std::optional<T>{std::move(out.value())};
}
} // namespace
// static
ErrorOr<AspectRatio> AspectRatio::TryParse(const Json::Value& value) {
std::string parsed_value;
if (!json::TryParseString(value, &parsed_value)) {
return Error(Error::Code::kJsonParseError, "Invalid aspect ratio string");
}
std::vector<std::string_view> fields =
string_util::Split(parsed_value, kAspectRatioDelimiter);
if (fields.size() != 2) {
return Error(Error::Code::kJsonParseError, "Invalid aspect ratio format");
}
AspectRatio out;
if (!string_parse::ParseAsciiNumber(fields[0], out.width) ||
!string_parse::ParseAsciiNumber(fields[1], out.height)) {
return Error(Error::Code::kJsonParseError, "Invalid aspect ratio values");
}
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid aspect ratio");
}
return out;
}
bool AspectRatio::IsValid() const {
return width > 0 && height > 0;
}
// static
ErrorOr<AudioConstraints> AudioConstraints::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError,
"Audio constraints is not a JSON object");
}
AudioConstraints out;
if (!json::TryParseInt(root[kMaxSampleRate], &out.max_sample_rate) ||
!json::TryParseInt(root[kMaxChannels], &out.max_channels) ||
!json::TryParseInt(root[kMaxBitRate], &out.max_bit_rate)) {
return Error(Error::Code::kJsonParseError, "Invalid audio constraints");
}
std::chrono::milliseconds max_delay;
if (json::TryParseMilliseconds(root[kMaxDelay], &max_delay)) {
out.max_delay = max_delay;
}
if (!json::TryParseInt(root[kMinBitRate], &out.min_bit_rate)) {
out.min_bit_rate = kDefaultAudioMinBitRate;
}
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid audio constraints");
}
return out;
}
Json::Value AudioConstraints::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root[kMaxSampleRate] = max_sample_rate;
root[kMaxChannels] = max_channels;
root[kMinBitRate] = min_bit_rate;
root[kMaxBitRate] = max_bit_rate;
if (max_delay.has_value()) {
root[kMaxDelay] = Json::Value::Int64(max_delay->count());
}
return root;
}
bool AudioConstraints::IsValid() const {
return max_sample_rate > 0 && max_channels > 0 && min_bit_rate > 0 &&
max_bit_rate >= min_bit_rate;
}
// static
ErrorOr<VideoConstraints> VideoConstraints::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError,
"Video constraints is not a JSON object");
}
VideoConstraints out;
auto max_dimensions = Dimensions::TryParse(root[kMaxDimensions]);
if (max_dimensions.is_error()) {
return max_dimensions.error();
}
out.max_dimensions = std::move(max_dimensions.value());
if (!json::TryParseInt(root[kMaxBitRate], &out.max_bit_rate)) {
return Error(Error::Code::kJsonParseError,
"Invalid video constraints: missing maxBitRate");
}
auto min_resolution = ParseOptional<Dimensions>(root[kMinResolution]);
if (min_resolution.is_error()) {
return min_resolution.error();
}
out.min_resolution = std::move(min_resolution.value());
std::chrono::milliseconds max_delay;
if (json::TryParseMilliseconds(root[kMaxDelay], &max_delay)) {
out.max_delay = max_delay;
}
double max_pixels_per_second;
if (json::TryParseDouble(root[kMaxPixelsPerSecond], &max_pixels_per_second)) {
out.max_pixels_per_second = max_pixels_per_second;
}
if (!json::TryParseInt(root[kMinBitRate], &out.min_bit_rate)) {
out.min_bit_rate = kDefaultVideoMinBitRate;
}
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid video constraints");
}
return out;
}
bool VideoConstraints::IsValid() const {
return max_pixels_per_second > 0 && min_bit_rate > 0 &&
max_bit_rate > min_bit_rate &&
(!max_delay.has_value() || max_delay->count() > 0) &&
max_dimensions.IsValid() &&
(!min_resolution.has_value() || min_resolution->IsValid()) &&
max_dimensions.frame_rate.numerator() > 0;
}
Json::Value VideoConstraints::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root[kMaxDimensions] = max_dimensions.ToJson();
root[kMinBitRate] = min_bit_rate;
root[kMaxBitRate] = max_bit_rate;
if (max_pixels_per_second.has_value()) {
root[kMaxPixelsPerSecond] = max_pixels_per_second.value();
}
if (min_resolution.has_value()) {
root[kMinResolution] = min_resolution->ToJson();
}
if (max_delay.has_value()) {
root[kMaxDelay] = Json::Value::Int64(max_delay->count());
}
return root;
}
// static
ErrorOr<Constraints> Constraints::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError,
"Constraints is not a JSON object");
}
Constraints out;
auto audio = AudioConstraints::TryParse(root[kAudio]);
if (audio.is_error()) {
return audio.error();
}
out.audio = std::move(audio.value());
auto video = VideoConstraints::TryParse(root[kVideo]);
if (video.is_error()) {
return video.error();
}
out.video = std::move(video.value());
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid constraints");
}
return out;
}
bool Constraints::IsValid() const {
return audio.IsValid() && video.IsValid();
}
Json::Value Constraints::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root[kAudio] = audio.ToJson();
root[kVideo] = video.ToJson();
return root;
}
// static
ErrorOr<DisplayDescription> DisplayDescription::TryParse(
const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError,
"Display description is not a JSON object");
}
DisplayDescription out;
auto dimensions = ParseOptional<Dimensions>(root[kDimensions]);
if (dimensions.is_error()) {
return dimensions.error();
}
out.dimensions = std::move(dimensions.value());
auto aspect_ratio = ParseOptional<AspectRatio>(root[kAspectRatio]);
if (aspect_ratio.is_error()) {
return aspect_ratio.error();
}
out.aspect_ratio = std::move(aspect_ratio.value());
auto constraint = TryParseAspectRatioConstraint(root[kScaling]);
if (constraint.has_value()) {
out.aspect_ratio_constraint = constraint.value();
} else {
out.aspect_ratio_constraint = std::nullopt;
}
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid display description");
}
return out;
}
bool DisplayDescription::IsValid() const {
// At least one of the properties must be set, and if a property is set
// it must be valid.
if (aspect_ratio.has_value() && !aspect_ratio->IsValid()) {
return false;
}
if (dimensions.has_value() && !dimensions->IsValid()) {
return false;
}
// Sender behavior is undefined if the aspect ratio is fixed but no
// dimensions or aspect ratio are provided.
if (aspect_ratio_constraint.has_value() &&
(aspect_ratio_constraint.value() == AspectRatioConstraint::kFixed) &&
!dimensions.has_value() && !aspect_ratio.has_value()) {
return false;
}
return aspect_ratio.has_value() || dimensions.has_value() ||
aspect_ratio_constraint.has_value();
}
Json::Value DisplayDescription::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
if (aspect_ratio.has_value()) {
root[kAspectRatio] =
StringFormat("{}{}{}", aspect_ratio->width, kAspectRatioDelimiter,
aspect_ratio->height);
}
if (dimensions.has_value()) {
root[kDimensions] = dimensions->ToJson();
}
if (aspect_ratio_constraint.has_value()) {
root[kScaling] =
AspectRatioConstraintToJson(aspect_ratio_constraint.value());
}
return root;
}
ErrorOr<Answer> Answer::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError, "Answer is not a JSON object");
}
Answer out;
if (!json::TryParseInt(root[kUdpPort], &out.udp_port) ||
!json::TryParseIntArray(root[kSendIndexes], &out.send_indexes) ||
!json::TryParseUintArray(root[kSsrcs], &out.ssrcs)) {
return Error(Error::Code::kJsonParseError,
"Invalid answer: missing or invalid mandatory fields");
}
auto constraints = ParseOptional<Constraints>(root[kConstraints]);
if (constraints.is_error()) {
return constraints.error();
}
out.constraints = std::move(constraints.value());
auto display = ParseOptional<DisplayDescription>(root[kDisplay]);
if (display.is_error()) {
return display.error();
}
out.display = std::move(display.value());
// These functions set to empty array if not present, so we can ignore
// the return value for optional values.
json::TryParseIntArray(root[kReceiverRtcpEventLog],
&out.receiver_rtcp_event_log);
json::TryParseIntArray(root[kReceiverRtcpDscp], &out.receiver_rtcp_dscp);
json::TryParseNestedStringArray(root[kRtpExtensions], &out.rtp_extensions);
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid answer");
}
return out;
}
bool Answer::IsValid() const {
if (ssrcs.empty() || send_indexes.empty()) {
return false;
}
// We don't know what the indexes used in the offer were here, so we just
// sanity check.
for (const int index : send_indexes) {
if (index < 0) {
return false;
}
}
if (constraints.has_value() && !constraints->IsValid()) {
return false;
}
if (display.has_value() && !display->IsValid()) {
return false;
}
return kUdpPortMin <= udp_port && udp_port <= kUdpPortMax;
}
Json::Value Answer::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
if (constraints.has_value()) {
root[kConstraints] = constraints->ToJson();
}
if (display.has_value()) {
root[kDisplay] = display->ToJson();
}
root[kUdpPort] = udp_port;
root[kSendIndexes] = json::PrimitiveVectorToJson(send_indexes);
root[kSsrcs] = json::PrimitiveVectorToJson(ssrcs);
// Some sender do not handle empty array properly, so we omit these fields
// if they are empty.
if (!receiver_rtcp_event_log.empty()) {
root[kReceiverRtcpEventLog] =
json::PrimitiveVectorToJson(receiver_rtcp_event_log);
}
if (!receiver_rtcp_dscp.empty()) {
root[kReceiverRtcpDscp] = json::PrimitiveVectorToJson(receiver_rtcp_dscp);
}
if (!rtp_extensions.empty()) {
root[kRtpExtensions] = json::NestedStringArrayToJson(rtp_extensions);
}
return root;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,122 @@
// 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_PUBLIC_ANSWER_MESSAGES_H_
#define CAST_STREAMING_PUBLIC_ANSWER_MESSAGES_H_
#include <array>
#include <chrono>
#include <cstdint>
#include <initializer_list>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "cast/streaming/resolution.h"
#include "cast/streaming/ssrc.h"
#include "json/value.h"
#include "platform/base/error.h"
#include "util/simple_fraction.h"
namespace openscreen::cast {
// For each of the below classes, though a number of methods are shared, the use
// of a shared base class has intentionally been avoided. This is to improve
// readability of the structs provided in this file by cutting down on the
// amount of obscuring boilerplate code. For each of the following struct
// definitions, the following method definitions are shared:
// (1) TryParse. Shall return a boolean indicating whether the out
// parameter is in a valid state after checking bounds and restrictions.
// (2) ToJson. Should return a proper JSON object. Assumes that IsValid()
// has been called already, OSP_CHECKs if not IsValid().
// (3) IsValid. Used by both TryParse and ToJson to ensure that the
// object is in a good state.
struct AudioConstraints {
static ErrorOr<AudioConstraints> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
int max_sample_rate = 0;
int max_channels = 0;
int min_bit_rate = 0; // optional
int max_bit_rate = 0;
std::optional<std::chrono::milliseconds> max_delay = {};
};
struct VideoConstraints {
static ErrorOr<VideoConstraints> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
std::optional<double> max_pixels_per_second = {};
std::optional<Dimensions> min_resolution = {};
Dimensions max_dimensions = {};
int min_bit_rate = 0; // optional
int max_bit_rate = 0;
std::optional<std::chrono::milliseconds> max_delay = {};
};
struct Constraints {
static ErrorOr<Constraints> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
AudioConstraints audio;
VideoConstraints video;
};
// Decides whether the Sender scales and letterboxes content to 16:9, or if
// it may send video frames of any arbitrary size and the Receiver must
// handle the presentation details.
enum class AspectRatioConstraint : uint8_t { kVariable = 0, kFixed };
struct AspectRatio {
static ErrorOr<AspectRatio> TryParse(const Json::Value& value);
bool IsValid() const;
bool operator==(const AspectRatio& other) const {
return width == other.width && height == other.height;
}
int width = 0;
int height = 0;
};
struct DisplayDescription {
static ErrorOr<DisplayDescription> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
// May exceed, be the same, or less than those mentioned in the
// video constraints.
std::optional<Dimensions> dimensions;
std::optional<AspectRatio> aspect_ratio = {};
std::optional<AspectRatioConstraint> aspect_ratio_constraint = {};
};
struct Answer {
static ErrorOr<Answer> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
int udp_port = 0;
std::vector<int> send_indexes;
std::vector<Ssrc> ssrcs;
// Constraints and display descriptions are optional fields, and maybe null in
// the valid case.
std::optional<Constraints> constraints;
std::optional<DisplayDescription> display;
std::vector<int> receiver_rtcp_event_log;
std::vector<int> receiver_rtcp_dscp;
// RTP extensions should be empty, but not null.
std::vector<std::vector<std::string>> rtp_extensions = {};
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_ANSWER_MESSAGES_H_

View file

@ -0,0 +1,157 @@
// 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/public/capture_recommendations.h"
#include <algorithm>
#include <utility>
#include "cast/streaming/public/answer_messages.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
namespace capture_recommendations {
namespace {
void ApplyDisplay(const DisplayDescription& description,
Recommendations* recommendations) {
recommendations->video.supports_scaling =
(description.aspect_ratio_constraint &&
(description.aspect_ratio_constraint.value() ==
AspectRatioConstraint::kVariable));
// We should never exceed the display's resolution, since it will always
// force scaling.
if (description.dimensions) {
recommendations->video.maximum = description.dimensions.value();
recommendations->video.bit_rate_limits.maximum =
recommendations->video.maximum.effective_bit_rate();
if (recommendations->video.maximum.width <
recommendations->video.minimum.width) {
recommendations->video.minimum =
recommendations->video.maximum.ToResolution();
}
}
// If the receiver gives us an aspect ratio that doesn't match the display
// resolution they give us, the behavior is undefined from the spec.
// Here we prioritize the aspect ratio, and the receiver can scale the frame
// as they wish.
double aspect_ratio = 0.0;
if (description.aspect_ratio) {
aspect_ratio = static_cast<double>(description.aspect_ratio->width) /
description.aspect_ratio->height;
recommendations->video.maximum.width =
recommendations->video.maximum.height * aspect_ratio;
} else if (description.dimensions) {
aspect_ratio = static_cast<double>(description.dimensions->width) /
description.dimensions->height;
} else {
return;
}
recommendations->video.minimum.width =
recommendations->video.minimum.height * aspect_ratio;
}
void ApplyConstraints(const Constraints& constraints,
Recommendations* recommendations) {
// Audio has no fields in the display description, so we can safely
// ignore the current recommendations when setting values here.
if (constraints.audio.max_delay.has_value()) {
recommendations->audio.max_delay = constraints.audio.max_delay.value();
}
recommendations->audio.max_channels = constraints.audio.max_channels;
recommendations->audio.max_sample_rate = constraints.audio.max_sample_rate;
recommendations->audio.bit_rate_limits = BitRateLimits{
std::max(constraints.audio.min_bit_rate, kDefaultAudioMinBitRate),
std::max(constraints.audio.max_bit_rate, kDefaultAudioMinBitRate)};
// With video, we take the intersection of values of the constraints and
// the display description.
if (constraints.video.max_delay.has_value()) {
recommendations->video.max_delay = constraints.video.max_delay.value();
}
if (constraints.video.max_pixels_per_second.has_value()) {
recommendations->video.max_pixels_per_second =
constraints.video.max_pixels_per_second.value();
}
recommendations->video.bit_rate_limits =
BitRateLimits{std::max(constraints.video.min_bit_rate,
recommendations->video.bit_rate_limits.minimum),
std::min(constraints.video.max_bit_rate,
recommendations->video.bit_rate_limits.maximum)};
Dimensions dimensions = constraints.video.max_dimensions;
if (dimensions.width <= kDefaultMinResolution.width) {
recommendations->video.maximum = {kDefaultMinResolution.width,
kDefaultMinResolution.height,
kDefaultFrameRate};
} else if (dimensions.width < recommendations->video.maximum.width) {
recommendations->video.maximum = std::move(dimensions);
}
if (constraints.video.min_resolution) {
const Resolution& min = constraints.video.min_resolution->ToResolution();
if (kDefaultMinResolution.width < min.width) {
recommendations->video.minimum = std::move(min);
}
}
}
// The receiver's video constraints, even when each is individually valid, can
// intersect with the display description to produce an inverted range: a
// minimum bit rate above the display-limited maximum, or a minimum resolution
// larger than the display. (Audio cannot invert: AudioConstraints::IsValid()
// already requires max_bit_rate >= min_bit_rate.) Resolve any such
// contradiction in favor of the maximum, which reflects what the
// receiver/display can actually handle.
void ClampVideoToWellOrderedRanges(Video& video) {
video.bit_rate_limits.minimum =
std::min(video.bit_rate_limits.minimum, video.bit_rate_limits.maximum);
video.minimum.width = std::min(video.minimum.width, video.maximum.width);
video.minimum.height =
std::min(video.minimum.height, video.maximum.height);
}
} // namespace
bool BitRateLimits::operator==(const BitRateLimits& other) const {
return std::tie(minimum, maximum) == std::tie(other.minimum, other.maximum);
}
bool Audio::operator==(const Audio& other) const {
return std::tie(bit_rate_limits, max_delay, max_channels, max_sample_rate) ==
std::tie(other.bit_rate_limits, other.max_delay, other.max_channels,
other.max_sample_rate);
}
bool Video::operator==(const Video& other) const {
return std::tie(bit_rate_limits, minimum, maximum, supports_scaling,
max_delay, max_pixels_per_second) ==
std::tie(other.bit_rate_limits, other.minimum, other.maximum,
other.supports_scaling, other.max_delay,
other.max_pixels_per_second);
}
bool Recommendations::operator==(const Recommendations& other) const {
return std::tie(audio, video) == std::tie(other.audio, other.video);
}
Recommendations GetRecommendations(const Answer& answer) {
Recommendations recommendations;
if (answer.display.has_value() && answer.display->IsValid()) {
ApplyDisplay(answer.display.value(), &recommendations);
}
if (answer.constraints.has_value() && answer.constraints->IsValid()) {
ApplyConstraints(answer.constraints.value(), &recommendations);
}
ClampVideoToWellOrderedRanges(recommendations.video);
return recommendations;
}
} // namespace capture_recommendations
} // namespace openscreen::cast

View file

@ -0,0 +1,151 @@
// 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_PUBLIC_CAPTURE_RECOMMENDATIONS_H_
#define CAST_STREAMING_PUBLIC_CAPTURE_RECOMMENDATIONS_H_
#include <chrono>
#include <cmath>
#include <memory>
#include <tuple>
#include "cast/streaming/public/constants.h"
#include "cast/streaming/resolution.h"
namespace openscreen::cast {
struct Answer;
// This namespace contains classes and functions to be used by senders for
// determining what constraints are recommended for the capture device, based on
// the limits reported by the receiver.
//
// A general note about recommendations: they are NOT maximum operational
// limits, instead they are targeted to provide a delightful cast experience.
// For example, if a receiver is connected to a 1080P display but cannot provide
// 1080P at a stable FPS with a good experience, 1080P will not be recommended.
namespace capture_recommendations {
// Default maximum delay for both audio and video. Used if the sender fails
// to provide any constraints.
inline constexpr std::chrono::milliseconds kDefaultMaxDelayMs(400);
// Bit rate limits, used for both audio and video streams.
struct BitRateLimits {
bool operator==(const BitRateLimits& other) const;
// Minimum bit rate, in bits per second.
int minimum;
// Maximum bit rate, in bits per second.
int maximum;
};
// The mirroring control protocol specifies 32kbps as the absolute minimum
// for audio. Depending on the type of audio content (narrowband, fullband,
// etc.) Opus specifically can perform very well at this bitrate.
// See: https://research.google/pubs/pub41650/
inline constexpr int kDefaultAudioMinBitRate = 32 * 1000;
// Opus generally sees little improvement above 192kbps, but some older codecs
// that we may consider supporting improve at up to 256kbps.
inline constexpr int kDefaultAudioMaxBitRate = 256 * 1000;
inline constexpr BitRateLimits kDefaultAudioBitRateLimits{
kDefaultAudioMinBitRate, kDefaultAudioMaxBitRate};
// While generally audio should be captured at the maximum sample rate, 16kHz is
// the recommended absolute minimum.
inline constexpr int kDefaultAudioMinSampleRate = 16000;
// Audio capture recommendations. Maximum delay is determined by buffer
// constraints, and capture bit rate may vary between limits as appropriate.
struct Audio {
bool operator==(const Audio& other) const;
// Represents the recommended bit rate range.
BitRateLimits bit_rate_limits = kDefaultAudioBitRateLimits;
// Represents the maximum audio delay, in milliseconds.
std::chrono::milliseconds max_delay = kDefaultMaxDelayMs;
// Represents the maximum number of audio channels.
int max_channels = kDefaultAudioChannels;
// Represents the maximum samples per second.
int max_sample_rate = kDefaultAudioSampleRate;
// Represents the absolute minimum samples per second. Generally speaking,
// audio should be captured at the maximum samples per second rate.
int min_sample_rate = kDefaultAudioMinSampleRate;
};
// The minimum dimensions are as close as possible to low-definition
// television, factoring in the receiver's aspect ratio if provided.
inline constexpr Resolution kDefaultMinResolution{kMinVideoWidth,
kMinVideoHeight};
// Currently mirroring only supports 1080P.
inline constexpr Dimensions kDefaultMaxResolution{1920, 1080,
kDefaultFrameRate};
// The mirroring spec suggests 300kbps as the absolute minimum bitrate.
inline constexpr int kDefaultVideoMinBitRate = 300 * 1000;
// The theoretical maximum pixels per second is the maximum bit rate
// divided by 8 (the max byte rate). In practice it should generally be
// less.
inline constexpr int kDefaultVideoMaxPixelsPerSecond =
kDefaultMaxResolution.effective_bit_rate() / 8;
// Our default limits are merely the product of the minimum and maximum
// dimensions, and are only used if the receiver fails to give better
// constraint information.
inline constexpr BitRateLimits kDefaultVideoBitRateLimits{
kDefaultVideoMinBitRate, kDefaultMaxResolution.effective_bit_rate()};
// Video capture recommendations.
struct Video {
bool operator==(const Video& other) const;
// Represents the recommended bit rate range.
BitRateLimits bit_rate_limits = kDefaultVideoBitRateLimits;
// Represents the recommended minimum resolution.
Resolution minimum = kDefaultMinResolution;
// Represents the recommended maximum resolution.
Dimensions maximum = kDefaultMaxResolution;
// Indicates whether the receiver can scale frames from a different aspect
// ratio, or if it needs to be done by the sender. Default is false, meaning
// that the sender is responsible for letterboxing.
bool supports_scaling = false;
// Represents the maximum video delay, in milliseconds.
std::chrono::milliseconds max_delay = kDefaultMaxDelayMs;
// Represents the maximum pixels per second, not necessarily correlated
// to bit rate.
int max_pixels_per_second = kDefaultVideoMaxPixelsPerSecond;
};
// Outputted recommendations for usage by capture devices. Note that we always
// return both audio and video (it is up to the sender to determine what
// streams actually get created). If the receiver doesn't give us any
// information for making recommendations, the defaults are used.
struct Recommendations {
bool operator==(const Recommendations& other) const;
// Audio specific recommendations.
Audio audio;
// Video specific recommendations.
Video video;
};
Recommendations GetRecommendations(const Answer& answer);
} // namespace capture_recommendations
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_CAPTURE_RECOMMENDATIONS_H_

View file

@ -0,0 +1,57 @@
// Copyright 2024 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/public/constants.h"
#include <ostream>
#include "util/osp_logging.h"
namespace openscreen::cast {
std::ostream& operator<<(std::ostream& os, VideoCodec codec) {
const char* str = nullptr;
switch (codec) {
case VideoCodec::kH264:
str = "H264";
break;
case VideoCodec::kVp8:
str = "VP8";
break;
case VideoCodec::kHevc:
str = "HEVC";
break;
case VideoCodec::kNotSpecified:
str = "NotSpecified";
break;
case VideoCodec::kVp9:
str = "VP9";
break;
case VideoCodec::kAv1:
str = "AV1";
break;
default:
OSP_NOTREACHED();
}
os << str;
return os;
}
std::ostream& operator<<(std::ostream& os, CastMode mode) {
const char* str = nullptr;
switch (mode) {
case CastMode::kMirroring:
str = "mirroring";
break;
case CastMode::kRemoting:
str = "remoting";
break;
default:
OSP_NOTREACHED();
}
os << str;
return os;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,122 @@
// 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_PUBLIC_CONSTANTS_H_
#define CAST_STREAMING_PUBLIC_CONSTANTS_H_
////////////////////////////////////////////////////////////////////////////////
// NOTE: This file should only contain constants that are reasonably globally
// used (i.e., by many modules, and in all or nearly all subdirs). Do NOT add
// non-POD constants, functions, interfaces, or any logic to this module,
// except for std::ostream operators on an as-needed basis.
////////////////////////////////////////////////////////////////////////////////
#include <chrono>
#include <ostream>
#include <ratio>
namespace openscreen::cast {
// Default target playout delay. The playout delay is the window of time between
// capture from the source until presentation at the receiver.
inline constexpr std::chrono::milliseconds kDefaultTargetPlayoutDelay(400);
// Default UDP port, bound at the Receiver, for Cast Streaming. An
// implementation is required to use the port specified by the Receiver in its
// ANSWER control message, which may or may not match this port number here.
inline constexpr int kDefaultCastStreamingPort = 2344;
// Default TCP port, bound at the TLS server socket level, for Cast Streaming.
// An implementation must use the port specified in the DNS-SD published record
// for connecting over TLS, which may or may not match this port number here.
inline constexpr int kDefaultCastPort = 8010;
// Target number of milliseconds between the sending of RTCP reports. Both
// senders and receivers regularly send RTCP reports to their peer.
inline constexpr std::chrono::milliseconds kRtcpReportInterval(500);
// This is an important system-wide constant. This limits how much history
// the implementation must retain in order to process the acknowledgements of
// past frames.
//
// This value is carefully choosen such that it fits in the 8-bits range for
// frame IDs. It is also less than half of the full 8-bits range such that
// logic can handle wrap around and compare two frame IDs meaningfully.
inline constexpr int kMaxUnackedFrames = 120;
// The network must support a packet size of at least this many bytes.
inline constexpr int kRequiredNetworkPacketSize = 256;
// The spec declares RTP timestamps must always have a timebase of 90000 ticks
// per second for video.
inline constexpr int kRtpVideoTimebase = 90000;
// Minimum resolution is 320x240.
inline constexpr int kMinVideoHeight = 240;
inline constexpr int kMinVideoWidth = 320;
// The default frame rate for capture options is 30FPS.
inline constexpr int kDefaultFrameRate = 30;
// The mirroring spec suggests 300kbps as the absolute minimum bitrate.
inline constexpr int kDefaultVideoMinBitRate = 300 * 1000;
// Default video max bitrate is based on 1080P @ 30FPS, which can be played back
// at good quality around 10mbps.
inline constexpr int kDefaultVideoMaxBitRate = 10 * 1000 * 1000;
// The mirroring control protocol specifies 32kbps as the absolute minimum
// for audio. Depending on the type of audio content (narrowband, fullband,
// etc.) Opus specifically can perform very well at this bitrate.
// See: https://research.google/pubs/pub41650/
inline constexpr int kDefaultAudioMinBitRate = 32 * 1000;
// Opus generally sees little improvement above 192kbps, but some older codecs
// that we may consider supporting improve at up to 256kbps.
inline constexpr int kDefaultAudioMaxBitRate = 256 * 1000;
// While generally audio should be captured at the maximum sample rate, 16kHz is
// the recommended absolute minimum.
inline constexpr int kDefaultAudioMinSampleRate = 16000;
// The default audio sample rate is 48kHz, slightly higher than standard
// consumer audio.
inline constexpr int kDefaultAudioSampleRate = 48000;
// The default audio number of channels is set to stereo.
inline constexpr int kDefaultAudioChannels = 2;
// Default maximum delay for both audio and video. Used if the sender fails
// to provide any constraints.
inline constexpr std::chrono::milliseconds kDefaultMaxDelayMs(1500);
// TODO(issuetracker.google.com/184189100): As part of updating remoting
// OFFER/ANSWER and capabilities exchange, remoting version should be updated
// to 3.
inline constexpr int kSupportedRemotingVersion = 2;
// Used for RTCP message support.
constexpr uint32_t kCastName = ('C' << 24) + ('A' << 16) + ('S' << 8) + 'T';
// Codecs known and understood by cast senders and receivers. Note: receivers
// are required to implement the following codecs to be Cast V2 compliant: H264,
// VP8, AAC, Opus. Senders have to implement at least one codec from this
// list for audio or video to start a session.
// `kNotSpecified` is used in remoting to indicate that the stream is being
// remoted and is not specified as part of the OFFER message (indicated as
// "REMOTE_AUDIO" or "REMOTE_VIDEO").
enum class AudioCodec { kAac, kOpus, kNotSpecified };
enum class VideoCodec { kH264, kVp8, kHevc, kNotSpecified, kVp9, kAv1 };
std::ostream& operator<<(std::ostream& os, VideoCodec codec);
// The type (audio, video, or unknown) of the stream.
enum class StreamType { kUnknown, kAudio, kVideo };
enum class CastMode : uint8_t { kMirroring, kRemoting };
std::ostream& operator<<(std::ostream& os, CastMode mode);
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_CONSTANTS_H_

View file

@ -0,0 +1,60 @@
// 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/public/encoded_frame.h"
namespace openscreen::cast {
EncodedFrame::EncodedFrame(Dependency dependency,
FrameId frame_id,
FrameId referenced_frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point reference_time,
std::chrono::milliseconds new_playout_delay,
Clock::time_point capture_begin_time,
Clock::time_point capture_end_time,
ByteView data)
: dependency(dependency),
frame_id(frame_id),
referenced_frame_id(referenced_frame_id),
rtp_timestamp(rtp_timestamp),
reference_time(reference_time),
new_playout_delay(new_playout_delay),
capture_begin_time(capture_begin_time),
capture_end_time(capture_end_time),
data(data) {}
EncodedFrame::EncodedFrame(Dependency dependency,
FrameId frame_id,
FrameId referenced_frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point reference_time,
std::chrono::milliseconds new_playout_delay,
ByteView data)
: dependency(dependency),
frame_id(frame_id),
referenced_frame_id(referenced_frame_id),
rtp_timestamp(rtp_timestamp),
reference_time(reference_time),
new_playout_delay(new_playout_delay),
data(data) {}
EncodedFrame::EncodedFrame() = default;
EncodedFrame::~EncodedFrame() = default;
EncodedFrame::EncodedFrame(EncodedFrame&&) noexcept = default;
EncodedFrame& EncodedFrame::operator=(EncodedFrame&&) = default;
void EncodedFrame::CopyMetadataTo(EncodedFrame* dest) const {
dest->dependency = this->dependency;
dest->frame_id = this->frame_id;
dest->referenced_frame_id = this->referenced_frame_id;
dest->rtp_timestamp = this->rtp_timestamp;
dest->reference_time = this->reference_time;
dest->new_playout_delay = this->new_playout_delay;
dest->capture_begin_time = this->capture_begin_time;
dest->capture_end_time = this->capture_end_time;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,119 @@
// 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_PUBLIC_ENCODED_FRAME_H_
#define CAST_STREAMING_PUBLIC_ENCODED_FRAME_H_
#include <stdint.h>
#include <chrono>
#include <vector>
#include "cast/streaming/public/frame_id.h"
#include "cast/streaming/rtp_time.h"
#include "platform/api/time.h"
#include "platform/base/span.h"
namespace openscreen::cast {
// A combination of metadata and data for one encoded frame. This can contain
// audio data or video data or other.
struct EncodedFrame {
enum class Dependency : int8_t {
// "null" value, used to indicate whether `dependency` has been set.
kUnknown,
// Not decodable without the reference frame indicated by
// `referenced_frame_id`.
kDependent,
// Independently decodable.
kIndependent,
// Independently decodable, and no future frames will depend on any frames
// before this one.
kKeyFrame,
};
EncodedFrame(Dependency dependency,
FrameId frame_id,
FrameId referenced_frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point reference_time,
std::chrono::milliseconds new_playout_delay,
Clock::time_point capture_begin_time,
Clock::time_point capture_end_time,
ByteView data);
// TODO(issuetracker.google.com/285905175): remove remaining optional fields
// (new_playout_delay) once Chrome provides the capture begin and end
// timestamps, so this constructor only provides the required fields.
EncodedFrame(Dependency dependency,
FrameId frame_id,
FrameId referenced_frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point reference_time,
std::chrono::milliseconds new_playout_delay,
ByteView data);
EncodedFrame();
EncodedFrame(const EncodedFrame&) = delete;
EncodedFrame& operator=(const EncodedFrame&) = delete;
EncodedFrame(EncodedFrame&&) noexcept;
EncodedFrame& operator=(EncodedFrame&&);
~EncodedFrame();
// Copies all members except `data` to `dest`. Does not modify |dest->data|.
void CopyMetadataTo(EncodedFrame* dest) const;
// This frame's dependency relationship with respect to other frames.
Dependency dependency = Dependency::kUnknown;
// The label associated with this frame. Implies an ordering relative to
// other frames in the same stream.
FrameId frame_id;
// The label associated with the frame upon which this frame depends. If
// this frame does not require any other frame in order to become decodable
// (e.g., key frames), `referenced_frame_id` must equal `frame_id`.
FrameId referenced_frame_id;
// The stream timestamp, on the timeline of the signal data. For example, RTP
// timestamps for audio are usually defined as the total number of audio
// samples encoded in all prior frames. A playback system uses this value to
// detect gaps in the stream, and otherwise stretch the signal to gradually
// re-align towards playout targets when too much drift has occurred (see
// `reference_time`, below).
RtpTimeTicks rtp_timestamp;
// The common reference clock timestamp for this frame. Over a sequence of
// frames, this time value is expected to drift with respect to the elapsed
// time implied by the RTP timestamps; and this may not necessarily increment
// with precise regularity.
//
// This value originates from a sender, and is the time at which the frame was
// captured/recorded. In the receiver context, this value is the computed
// target playout time, which is used for guiding the timing of presentation
// (see `rtp_timestamp`, above). It is also meant to be used to synchronize
// the presentation of multiple streams (e.g., audio and video), commonly
// known as "lip-sync." It is NOT meant to be a mandatory/exact playout time.
Clock::time_point reference_time;
// Playout delay for this and all future frames. Used by the Adaptive
// Playout delay extension. Non-positive values means no change.
std::chrono::milliseconds new_playout_delay{};
// Video capture begin/end timestamps. If set to a value other than
// Clock::time_point::min(), used for improved statistics gathering.
Clock::time_point capture_begin_time = Clock::time_point::min();
Clock::time_point capture_end_time = Clock::time_point::min();
// A buffer containing the encoded signal data for the frame. In the sender
// context, this points to the data to be sent. In the receiver context, this
// is set to the region of a client-provided buffer that was populated.
ByteView data;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_ENCODED_FRAME_H_

View file

@ -0,0 +1,171 @@
// 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/public/environment.h"
#include <algorithm>
#include <utility>
#include "cast/streaming/impl/rtp_defines.h"
#include "platform/api/task_runner.h"
#include "platform/base/span.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
Environment::PacketConsumer::~PacketConsumer() = default;
Environment::SocketSubscriber::~SocketSubscriber() = default;
Environment::Environment(ClockNowFunctionPtr now_function,
TaskRunner& task_runner,
const IPEndpoint& local_endpoint)
: now_function_(now_function), task_runner_(task_runner) {
OSP_CHECK(now_function_);
ErrorOr<std::unique_ptr<UdpSocket>> result =
UdpSocket::Create(*task_runner_, this, local_endpoint);
if (result.is_error()) {
OSP_LOG_ERROR << "Unable to create a UDP socket bound to " << local_endpoint
<< ": " << result.error();
return;
}
const_cast<std::unique_ptr<UdpSocket>&>(socket_) = std::move(result.value());
OSP_CHECK(socket_);
socket_->Bind();
}
Environment::~Environment() = default;
IPEndpoint Environment::GetBoundLocalEndpoint() const {
if (socket_) {
return socket_->GetLocalEndpoint();
}
return IPEndpoint{};
}
void Environment::SetSocketStateForTesting(SocketState state) {
state_ = state;
if (socket_subscriber_) {
switch (state_) {
case SocketState::kReady:
socket_subscriber_->OnSocketReady();
break;
case SocketState::kInvalid:
socket_subscriber_->OnSocketInvalid(Error::Code::kSocketFailure);
break;
default:
break;
}
}
}
void Environment::SetSocketSubscriber(SocketSubscriber* subscriber) {
socket_subscriber_ = subscriber;
}
void Environment::SetStatisticsCollector(StatisticsCollector* collector) {
statistics_collector_ = collector;
}
void Environment::ConsumeIncomingPackets(PacketConsumer* packet_consumer) {
OSP_CHECK(packet_consumer);
OSP_CHECK(!packet_consumer_);
packet_consumer_ = packet_consumer;
}
void Environment::DropIncomingPackets() {
packet_consumer_ = nullptr;
}
int Environment::GetMaxPacketSize() const {
// Return hard-coded values for UDP over wired Ethernet (which is a smaller
// MTU than typical defaults for UDP over 802.11 wireless). Performance would
// be more-optimized if the network were probed for the actual value. See
// discussion in rtp_defines.h.
switch (remote_endpoint_.address.version()) {
case IPAddress::Version::kV4:
return kMaxRtpPacketSizeForIpv4UdpOnEthernet;
case IPAddress::Version::kV6:
return kMaxRtpPacketSizeForIpv6UdpOnEthernet;
default:
OSP_NOTREACHED();
}
}
void Environment::SetDscp(UdpSocket::DscpMode mode) {
if (socket_) {
socket_->SetDscp(mode);
}
}
void Environment::SendPacket(ByteView packet, PacketMetadata metadata) {
OSP_CHECK(remote_endpoint_.address);
OSP_CHECK_NE(remote_endpoint_.port, 0);
if (socket_) {
socket_->SendMessage(packet, remote_endpoint_);
}
if (statistics_collector_) {
statistics_collector_->CollectPacketSentEvent(packet, metadata);
}
}
void Environment::OnBound(UdpSocket* socket) {
OSP_CHECK_EQ(socket, socket_.get());
state_ = SocketState::kReady;
if (socket_subscriber_) {
socket_subscriber_->OnSocketReady();
}
}
void Environment::OnError(UdpSocket* socket, const Error& error) {
OSP_CHECK_EQ(socket, socket_.get());
// Usually OnError() is only called for non-recoverable Errors. However,
// OnSendError() and OnRead() delegate to this method, to handle their hard
// error cases as well. So, return early here if `error` is recoverable.
if (error.ok() || error.code() == Error::Code::kAgain) {
return;
}
state_ = SocketState::kInvalid;
if (socket_subscriber_) {
socket_subscriber_->OnSocketInvalid(error);
} else {
// Default behavior when there are no subscribers.
OSP_LOG_ERROR << "For UDP socket bound to " << socket_->GetLocalEndpoint()
<< ": " << error;
}
}
void Environment::OnSendError(UdpSocket* socket, const Error& error) {
OnError(socket, error);
}
void Environment::OnRead(UdpSocket* socket,
ErrorOr<UdpPacket> packet_or_error) {
if (!packet_consumer_) {
return;
}
if (packet_or_error.is_error()) {
OnError(socket, packet_or_error.error());
return;
}
// Ideally, the arrival time would come from the operating system's network
// stack (e.g., by using the SO_TIMESTAMP sockopt on POSIX systems). However,
// there would still be the problem of mapping the timestamp to a value in
// terms of Clock::time_point. So, just sample the Clock here and call that
// the "arrival time." While this can add variance within the system, it
// should be minimal, assuming not too much time has elapsed between the
// actual packet receive event and the when this code here is executing.
const Clock::time_point arrival_time = now_function_();
UdpPacket packet = std::move(packet_or_error.value());
packet_consumer_->OnReceivedPacket(
packet.source(), arrival_time,
std::move(static_cast<std::vector<uint8_t>&>(packet)));
}
} // namespace openscreen::cast

View file

@ -0,0 +1,164 @@
// 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_PUBLIC_ENVIRONMENT_H_
#define CAST_STREAMING_PUBLIC_ENVIRONMENT_H_
#include <stdint.h>
#include <functional>
#include <memory>
#include <vector>
#include "cast/streaming/impl/statistics_collector.h"
#include "platform/api/time.h"
#include "platform/api/udp_socket.h"
#include "platform/base/ip_address.h"
#include "platform/base/span.h"
#include "util/raw_ptr.h"
#include "util/raw_ref.h"
namespace openscreen::cast {
// Provides the common environment for operating system resources shared by
// multiple components.
class Environment : public UdpSocket::Client {
public:
class PacketConsumer {
public:
virtual void OnReceivedPacket(const IPEndpoint& source,
Clock::time_point arrival_time,
std::vector<uint8_t> packet) = 0;
protected:
virtual ~PacketConsumer();
};
// Consumers of the environment's UDP socket should be careful to check the
// socket's state before accessing its methods, especially
// GetBoundLocalEndpoint(). If the environment is `kStarting`, the
// local endpoint may not be set yet and will be zero initialized.
enum class SocketState {
// Socket is still initializing. Usually the UDP socket bind is
// the last piece.
kStarting,
// The socket is ready for use and has been bound.
kReady,
// The socket is either closed (normally or due to an error) or in an
// invalid state. Currently the environment does not create a new socket
// in this case, so to be used again the environment itself needs to be
// recreated.
kInvalid
};
// Classes concerned with the Environment's UDP socket state may inherit from
// `Subscriber` and then `Subscribe`.
class SocketSubscriber {
public:
// Event that occurs when the environment is ready for use.
virtual void OnSocketReady() = 0;
// Event that occurs when the environment has experienced a fatal error.
virtual void OnSocketInvalid(const Error& error) = 0;
protected:
virtual ~SocketSubscriber();
};
// Construct with the given clock source and TaskRunner. Creates and
// internally-owns a UdpSocket, and immediately binds it to the given
// `local_endpoint`. Default behavior if `local_endpoint` is omitted is to
// bind to all available interfaces using IPv4.
Environment(ClockNowFunctionPtr now_function,
TaskRunner& task_runner,
const IPEndpoint& local_endpoint = IPEndpoint::kAnyV4());
~Environment() override;
ClockNowFunctionPtr now_function() const { return now_function_; }
Clock::time_point now() const { return now_function_(); }
TaskRunner& task_runner() const { return *task_runner_; }
// Returns the local endpoint the socket is bound to, or the zero IPEndpoint
// if socket creation/binding failed.
//
// Note: This method is virtual to allow unit tests to fake that there really
// is a bound socket.
virtual IPEndpoint GetBoundLocalEndpoint() const;
// Get/Set the remote endpoint. This is separate from the constructor because
// the remote endpoint is, in some cases, discovered only after receiving a
// packet.
const IPEndpoint& remote_endpoint() const { return remote_endpoint_; }
void set_remote_endpoint(const IPEndpoint& endpoint) {
remote_endpoint_ = endpoint;
}
SocketState socket_state() const { return state_; }
void SetSocketStateForTesting(SocketState state);
// Subscribe to socket changes. Callers can unsubscribe by passing
// nullptr.
void SetSocketSubscriber(SocketSubscriber* subscriber);
// Subscribe to frame and packet events. Callers can unsubscribe by passing
// nullptr. Note that if the collector is destroyed before the environment,
// callers MUST unsubscribe to avoid an access exception.
void SetStatisticsCollector(StatisticsCollector* subscriber);
StatisticsCollector* statistics_collector() {
return statistics_collector_.get();
}
// Start/Resume delivery of incoming packets to the given `packet_consumer`.
// Delivery will continue until DropIncomingPackets() is called.
void ConsumeIncomingPackets(PacketConsumer* packet_consumer);
// Stop delivery of incoming packets, dropping any that do come in. All
// internal references to the PacketConsumer that was provided in the last
// call to ConsumeIncomingPackets() are cleared.
void DropIncomingPackets();
// Returns the maximum packet size for the network. This will always return a
// value of at least kRequiredNetworkPacketSize.
int GetMaxPacketSize() const;
// Sets the DSCP value for the underlying UDP socket.
void SetDscp(UdpSocket::DscpMode mode);
// Sends the given `packet` to the remote endpoint, best-effort.
// set_remote_endpoint() must be called beforehand with a valid IPEndpoint.
//
// Note: This method is virtual to allow unit tests to intercept packets
// before they actually head-out through the socket.
virtual void SendPacket(ByteView packet, PacketMetadata metadata);
private:
// UdpSocket::Client implementation.
void OnBound(UdpSocket* socket) final;
void OnError(UdpSocket* socket, const Error& error) final;
void OnSendError(UdpSocket* socket, const Error& error) final;
void OnRead(UdpSocket* socket, ErrorOr<UdpPacket> packet_or_error) final;
ClockNowFunctionPtr now_function_;
const raw_ref<TaskRunner> task_runner_;
// The UDP socket bound to the local endpoint that was passed into the
// constructor, or null if socket creation failed.
const std::unique_ptr<UdpSocket> socket_;
// These are externally set/cleared. Behaviors are described in getter/setter
// method comments above.
IPEndpoint local_endpoint_{};
IPEndpoint remote_endpoint_{};
raw_ptr<PacketConsumer> packet_consumer_ = nullptr;
SocketState state_ = SocketState::kStarting;
raw_ptr<SocketSubscriber> socket_subscriber_ = nullptr;
raw_ptr<StatisticsCollector> statistics_collector_ = nullptr;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_ENVIRONMENT_H_

View file

@ -0,0 +1,20 @@
// Copyright 2016 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/public/frame_id.h"
namespace openscreen::cast {
std::ostream& operator<<(std::ostream& out, const FrameId rhs) {
return out << rhs.ToString();
}
std::string FrameId::ToString() const {
if (is_null())
return "F<null>";
return "F" + std::to_string(value());
}
} // namespace openscreen::cast

View file

@ -0,0 +1,121 @@
// Copyright 2016 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_PUBLIC_FRAME_ID_H_
#define CAST_STREAMING_PUBLIC_FRAME_ID_H_
#include <stdint.h>
#include <limits>
#include <sstream>
#include <string>
#include "cast/streaming/impl/expanded_value_base.h"
namespace openscreen::cast {
// Forward declaration (see below).
class FrameId;
// Convenience operator overloads for logging.
std::ostream& operator<<(std::ostream& out, const FrameId rhs);
// Unique identifier for a frame in a RTP media stream. FrameIds are truncated
// to 8-bit values in RTP and RTCP headers, and then expanded back by the other
// endpoint when parsing the headers.
//
// Usage example:
//
// // Distance/offset math.
// FrameId first = FrameId::first();
// FrameId second = first + 1;
// FrameId third = second + 1;
// int64_t offset = third - first;
// FrameId fourth = second + offset;
//
// // Logging convenience.
// OSP_DLOG_INFO << "The current frame is " << fourth;
class FrameId : public ExpandedValueBase<int64_t, FrameId> {
public:
// The "null" FrameId constructor. Represents a FrameId field that has not
// been set and/or a "not applicable" indicator.
constexpr FrameId() : FrameId(std::numeric_limits<int64_t>::min()) {}
constexpr explicit FrameId(int64_t value) : ExpandedValueBase(value) {}
// Allow copy construction and assignment.
constexpr FrameId(const FrameId&) = default;
constexpr FrameId& operator=(const FrameId&) = default;
// Returns true if this is the special value representing null.
constexpr bool is_null() const { return *this == FrameId(); }
// Distance operator.
int64_t operator-(FrameId rhs) const {
OSP_CHECK(!is_null());
OSP_CHECK(!rhs.is_null());
return value_ - rhs.value_;
}
// Operators to compute advancement by incremental amounts.
constexpr FrameId operator+(int64_t rhs) const {
OSP_CHECK(!is_null());
return FrameId(value_ + rhs);
}
constexpr FrameId operator-(int64_t rhs) const {
OSP_CHECK(!is_null());
return FrameId(value_ - rhs);
}
constexpr FrameId& operator+=(int64_t rhs) {
OSP_CHECK(!is_null());
return (*this = (*this + rhs));
}
constexpr FrameId& operator-=(int64_t rhs) {
OSP_CHECK(!is_null());
return (*this = (*this - rhs));
}
constexpr FrameId& operator++() {
OSP_CHECK(!is_null());
++value_;
return *this;
}
constexpr FrameId& operator--() {
OSP_CHECK(!is_null());
--value_;
return *this;
}
constexpr FrameId operator++(int) {
OSP_CHECK(!is_null());
return FrameId(value_++);
}
constexpr FrameId operator--(int) {
OSP_CHECK(!is_null());
return FrameId(value_--);
}
// The identifier for the first frame in a stream.
static constexpr FrameId first() { return FrameId(0); }
// A virtual identifier, representing the frame before the first. There should
// never actually be a frame streamed with this identifier. Instead, this is
// used in various components to represent a "not yet seen/processed the first
// frame" state.
//
// The name "leader" comes from the terminology used in tape reels, which
// refers to the non-data-carrying segment of tape before the recording
// begins.
static constexpr FrameId leader() { return FrameId(-1); }
constexpr int64_t value() const { return value_; }
std::string ToString() const;
private:
friend class ExpandedValueBase<int64_t, FrameId>;
friend std::ostream& operator<<(std::ostream& out, const FrameId rhs);
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_FRAME_ID_H_

View file

@ -0,0 +1,487 @@
// 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/public/offer_messages.h"
#include <inttypes.h>
#include <algorithm>
#include <limits>
#include <ranges>
#include <string>
#include <string_view>
#include <utility>
#include "cast/streaming/public/constants.h"
#include "platform/base/error.h"
#include "util/big_endian.h"
#include "util/enum_name_table.h"
#include "util/json/json_helpers.h"
#include "util/json/json_serialization.h"
#include "util/osp_logging.h"
#include "util/string_util.h"
#include "util/stringprintf.h"
namespace openscreen::cast {
namespace {
constexpr char kSupportedStreams[] = "supportedStreams";
constexpr char kAudioSourceType[] = "audio_source";
constexpr char kVideoSourceType[] = "video_source";
constexpr char kStreamType[] = "type";
[[nodiscard]] constexpr bool CodecParameterIsValid(VideoCodec codec,
std::string_view parameter) {
if (parameter.empty()) {
return true;
}
switch (codec) {
using enum VideoCodec;
case kVp8:
return parameter.starts_with("vp08");
case kVp9:
return parameter.starts_with("vp09");
case kAv1:
return parameter.starts_with("av01");
case kHevc:
return parameter.starts_with("hev1");
case kH264:
return parameter.starts_with("avc1");
case kNotSpecified:
return false;
}
OSP_NOTREACHED();
}
bool CodecParameterIsValid(AudioCodec codec,
const std::string& codec_parameter) {
if (codec_parameter.empty()) {
return true;
}
switch (codec) {
case AudioCodec::kAac:
return codec_parameter.starts_with("mp4a.");
// Opus doesn't use codec parameters.
case AudioCodec::kOpus: // fallthrough
case AudioCodec::kNotSpecified:
return false;
}
OSP_NOTREACHED();
}
EnumNameTable<CastMode, 2> kCastModeNames{
{{"mirroring", CastMode::kMirroring}, {"remoting", CastMode::kRemoting}}};
bool TryParseRtpPayloadType(const Json::Value& value, RtpPayloadType* out) {
int t;
if (!json::TryParseInt(value, &t)) {
return false;
}
uint8_t t_small = t;
if (t_small != t || !IsRtpPayloadType(t_small)) {
return false;
}
*out = static_cast<RtpPayloadType>(t_small);
return true;
}
bool TryParseRtpTimebase(const Json::Value& value, int* out) {
std::string raw_timebase;
if (!json::TryParseString(value, &raw_timebase)) {
return false;
}
// The spec demands a leading 1, so this isn't really a fraction.
const auto fraction = SimpleFraction::FromString(raw_timebase);
if (fraction.is_error() || !fraction.value().is_positive() ||
fraction.value().numerator() != 1) {
return false;
}
*out = fraction.value().denominator();
return true;
}
// For a hex byte, the conversion is 4 bits to 1 character, e.g.
// 0b11110001 becomes F1, so 1 byte is two characters.
constexpr int kHexDigitsPerByte = 2;
constexpr int kAesBytesSize = 16;
constexpr int kAesStringLength = kAesBytesSize * kHexDigitsPerByte;
bool TryParseAesHexBytes(const Json::Value& value,
std::array<uint8_t, kAesBytesSize>* out) {
std::string hex_string;
if (!json::TryParseString(value, &hex_string)) {
return false;
}
constexpr int kHexDigitsPerScanField = 16;
constexpr int kNumScanFields = kAesStringLength / kHexDigitsPerScanField;
uint64_t quads[kNumScanFields];
int chars_scanned;
if (hex_string.size() == kAesStringLength &&
sscanf(hex_string.c_str(), "%16" SCNx64 "%16" SCNx64 "%n", &quads[0],
&quads[1], &chars_scanned) == kNumScanFields &&
chars_scanned == kAesStringLength &&
std::none_of(hex_string.begin(), hex_string.end(),
[](char c) { return std::isspace(c); })) {
WriteBigEndian(quads[0], out->data());
WriteBigEndian(quads[1], out->data() + 8);
return true;
}
return false;
}
std::string_view ToString(Stream::Type type) {
switch (type) {
case Stream::Type::kAudioSource:
return kAudioSourceType;
case Stream::Type::kVideoSource:
return kVideoSourceType;
default: {
OSP_NOTREACHED();
}
}
}
bool TryParseResolutions(const Json::Value& value,
std::vector<Resolution>* out) {
out->clear();
// Some legacy senders don't provide resolutions, so just return empty.
if (!value.isArray() || value.empty()) {
return false;
}
for (Json::ArrayIndex i = 0; i < value.size(); ++i) {
auto resolution = Resolution::TryParse(value[i]);
if (resolution.is_error()) {
out->clear();
return false;
}
out->push_back(std::move(resolution.value()));
}
return true;
}
} // namespace
ErrorOr<Stream> Stream::TryParse(const Json::Value& value, Stream::Type type) {
if (!value.isObject()) {
return Error(Error::Code::kJsonParseError, "Stream is not a JSON object");
}
Stream out;
out.type = type;
if (!json::TryParseInt(value["index"], &out.index) ||
!json::TryParseUint(value["ssrc"], &out.ssrc) ||
!TryParseRtpPayloadType(value["rtpPayloadType"], &out.rtp_payload_type) ||
!TryParseRtpTimebase(value["timeBase"], &out.rtp_timebase)) {
return Error(Error::Code::kJsonParseError,
"Offer stream has missing or invalid mandatory field");
}
if (!json::TryParseInt(value["channels"], &out.channels)) {
out.channels = out.type == Stream::Type::kAudioSource
? kDefaultNumAudioChannels
: kDefaultNumVideoChannels;
} else if (out.channels <= 0) {
return Error(Error::Code::kJsonParseError, "Invalid channel count");
}
if (!TryParseAesHexBytes(value["aesKey"], &out.aes_key) ||
!TryParseAesHexBytes(value["aesIvMask"], &out.aes_iv_mask)) {
return Error(Error::Code::kUnencryptedOffer,
"Offer stream must have both a valid aesKey and aesIvMask");
}
if (out.rtp_timebase <
std::min(kDefaultAudioMinSampleRate, kRtpVideoTimebase) ||
out.rtp_timebase > kRtpVideoTimebase) {
return Error(Error::Code::kJsonParseError, "rtp_timebase (sample rate)");
}
out.target_delay = kDefaultTargetPlayoutDelay;
int target_delay;
if (json::TryParseInt(value["targetDelay"], &target_delay)) {
auto d = std::chrono::milliseconds(target_delay);
if (kMinTargetPlayoutDelay <= d && d <= kMaxTargetPlayoutDelay) {
out.target_delay = d;
}
}
json::TryParseBool(value["receiverRtcpEventLog"],
&out.receiver_rtcp_event_log);
int dscp_value;
if (json::TryParseInt(value["receiverRtcpDscp"], &dscp_value)) {
// DSCP values are clamped to [0, 63].
if (dscp_value < 0 || dscp_value > 63) {
return Error(Error::Code::kJsonParseError,
"receiverRtcpDscp (invalid DSCP value)");
}
out.receiver_rtcp_dscp = dscp_value;
}
json::TryParseStringArray(value["rtpExtensions"], &out.rtp_extensions);
json::TryParseString(value["codecParameter"], &out.codec_parameter);
return out;
}
Json::Value Stream::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root["index"] = index;
root["type"] = std::string(ToString(type));
root["channels"] = channels;
root["rtpPayloadType"] = static_cast<int>(rtp_payload_type);
// rtpProfile is technically required by the spec, although it is always set
// to cast. We set it here to be compliant with all spec implementers.
root["rtpProfile"] = "cast";
static_assert(sizeof(ssrc) <= sizeof(Json::UInt),
"this code assumes Ssrc fits in a Json::UInt");
root["ssrc"] = static_cast<Json::UInt>(ssrc);
root["targetDelay"] = static_cast<int>(target_delay.count());
root["aesKey"] = HexEncode(aes_key.data(), aes_key.size());
root["aesIvMask"] = HexEncode(aes_iv_mask.data(), aes_iv_mask.size());
root["receiverRtcpEventLog"] = receiver_rtcp_event_log;
if (receiver_rtcp_dscp.has_value()) {
root["receiverRtcpDscp"] = receiver_rtcp_dscp.value();
}
root["timeBase"] = "1/" + std::to_string(rtp_timebase);
root["codecParameter"] = codec_parameter;
if (!rtp_extensions.empty()) {
root["rtpExtensions"] = json::PrimitiveVectorToJson(rtp_extensions);
}
return root;
}
bool Stream::IsValid() const {
return channels >= 1 && index >= 0 && target_delay.count() > 0 &&
target_delay.count() <= std::numeric_limits<int>::max() &&
rtp_timebase >= 1;
}
ErrorOr<AudioStream> AudioStream::TryParse(const Json::Value& value) {
if (!value.isObject()) {
return Error(Error::Code::kJsonParseError,
"Audio stream is not a JSON object");
}
auto stream_or_error = Stream::TryParse(value, Stream::Type::kAudioSource);
if (stream_or_error.is_error()) {
return stream_or_error.error();
}
AudioStream out;
out.stream = std::move(stream_or_error.value());
std::string codec_name;
if (!json::TryParseInt(value["bitRate"], &out.bit_rate) || out.bit_rate < 0 ||
!json::TryParseString(value[kCodecName], &codec_name)) {
return Error(Error::Code::kJsonParseError, "Invalid audio stream field");
}
ErrorOr<AudioCodec> codec = StringToAudioCodec(codec_name);
if (!codec) {
return Error(Error::Code::kUnknownCodec,
"Codec is not known, can't use stream");
}
out.codec = codec.value();
if (!CodecParameterIsValid(codec.value(), out.stream.codec_parameter)) {
return Error(Error::Code::kInvalidCodecParameter,
StringFormat("Invalid audio codec parameter ({} for codec {})",
out.stream.codec_parameter.c_str(),
CodecToString(codec.value())));
}
return out;
}
Json::Value AudioStream::ToJson() const {
OSP_CHECK(IsValid());
Json::Value out = stream.ToJson();
out[kCodecName] = CodecToString(codec);
out["bitRate"] = bit_rate;
return out;
}
bool AudioStream::IsValid() const {
return bit_rate >= 0 && stream.IsValid();
}
ErrorOr<VideoStream> VideoStream::TryParse(const Json::Value& value) {
if (!value.isObject()) {
return Error(Error::Code::kJsonParseError,
"Video stream is not a JSON object");
}
auto stream_or_error = Stream::TryParse(value, Stream::Type::kVideoSource);
if (stream_or_error.is_error()) {
return stream_or_error.error();
}
VideoStream out;
out.stream = std::move(stream_or_error.value());
std::string codec_name;
if (!json::TryParseString(value[kCodecName], &codec_name)) {
return Error(Error::Code::kJsonParseError, "Video stream missing codec");
}
ErrorOr<VideoCodec> codec = StringToVideoCodec(codec_name);
if (!codec) {
return Error(Error::Code::kUnknownCodec,
"Codec is not known, can't use stream");
}
out.codec = codec.value();
if (!CodecParameterIsValid(codec.value(), out.stream.codec_parameter)) {
return Error(Error::Code::kInvalidCodecParameter,
StringFormat("Invalid video codec parameter ({} for codec {})",
out.stream.codec_parameter.c_str(),
CodecToString(codec.value())));
}
out.max_frame_rate = SimpleFraction{kDefaultMaxFrameRate, 1};
std::string raw_max_frame_rate;
if (json::TryParseString(value["maxFrameRate"], &raw_max_frame_rate)) {
auto parsed = SimpleFraction::FromString(raw_max_frame_rate);
if (parsed.is_value() && parsed.value().is_positive()) {
out.max_frame_rate = parsed.value();
}
}
TryParseResolutions(value["resolutions"], &out.resolutions);
json::TryParseString(value["profile"], &out.profile);
json::TryParseString(value["protection"], &out.protection);
json::TryParseString(value["level"], &out.level);
json::TryParseString(value["errorRecoveryMode"], &out.error_recovery_mode);
if (!json::TryParseInt(value["maxBitRate"], &out.max_bit_rate)) {
out.max_bit_rate = 4 << 20;
}
return out;
}
Json::Value VideoStream::ToJson() const {
OSP_CHECK(IsValid());
Json::Value out = stream.ToJson();
out["codecName"] = CodecToString(codec);
out["maxFrameRate"] = max_frame_rate.ToString();
out["maxBitRate"] = max_bit_rate;
out["protection"] = protection;
out["profile"] = profile;
out["level"] = level;
out["errorRecoveryMode"] = error_recovery_mode;
Json::Value rs;
for (auto resolution : resolutions) {
rs.append(resolution.ToJson());
}
out["resolutions"] = std::move(rs);
return out;
}
bool VideoStream::IsValid() const {
return max_bit_rate > 0 && max_frame_rate.is_positive();
}
// static
ErrorOr<Offer> Offer::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError, "null offer");
}
const ErrorOr<CastMode> cast_mode =
GetEnum(kCastModeNames, root["castMode"].asString());
Json::Value supported_streams = root[kSupportedStreams];
if (!supported_streams.isArray()) {
return Error(Error::Code::kJsonParseError, "supported streams in offer");
}
std::vector<AudioStream> audio_streams;
std::vector<VideoStream> video_streams;
using Dscp = std::optional<int>;
std::optional<Dscp> receiver_rtcp_dscp;
for (Json::ArrayIndex i = 0; i < supported_streams.size(); ++i) {
const Json::Value& fields = supported_streams[i];
std::string type;
if (!json::TryParseString(fields[kStreamType], &type)) {
return Error(Error::Code::kJsonParseError, "Missing stream type");
}
Error error = Error::None();
if (type == kAudioSourceType) {
auto stream_or_error = AudioStream::TryParse(fields);
if (stream_or_error.is_value()) {
auto stream = std::move(stream_or_error.value());
if (!receiver_rtcp_dscp) {
receiver_rtcp_dscp.emplace(stream.stream.receiver_rtcp_dscp);
} else if (stream.stream.receiver_rtcp_dscp != *receiver_rtcp_dscp) {
return Error(Error::Code::kJsonParseError,
"Mixed DSCP values in offer");
}
audio_streams.push_back(std::move(stream));
} else {
error = stream_or_error.error();
}
} else if (type == kVideoSourceType) {
auto stream_or_error = VideoStream::TryParse(fields);
if (stream_or_error.is_value()) {
auto stream = std::move(stream_or_error.value());
if (!receiver_rtcp_dscp) {
receiver_rtcp_dscp.emplace(stream.stream.receiver_rtcp_dscp);
} else if (stream.stream.receiver_rtcp_dscp != *receiver_rtcp_dscp) {
return Error(Error::Code::kJsonParseError,
"Mixed DSCP values in offer");
}
video_streams.push_back(std::move(stream));
} else {
error = stream_or_error.error();
}
}
if (!error.ok()) {
if (error.code() == Error::Code::kUnknownCodec) {
OSP_VLOG << "Dropping audio stream due to unknown codec: " << error;
continue;
} else {
return error;
}
}
}
return Offer{cast_mode.value(CastMode::kMirroring), std::move(audio_streams),
std::move(video_streams)};
}
Json::Value Offer::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root["castMode"] = GetEnumName(kCastModeNames, cast_mode).value();
Json::Value streams;
for (auto& stream : audio_streams) {
streams.append(stream.ToJson());
}
for (auto& stream : video_streams) {
streams.append(stream.ToJson());
}
root[kSupportedStreams] = std::move(streams);
return root;
}
bool Offer::IsValid() const {
return std::ranges::all_of(
audio_streams, [](const AudioStream& a) { return a.IsValid(); }) &&
std::ranges::all_of(video_streams,
[](const VideoStream& v) { return v.IsValid(); });
}
} // namespace openscreen::cast

View file

@ -0,0 +1,115 @@
// 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_PUBLIC_OFFER_MESSAGES_H_
#define CAST_STREAMING_PUBLIC_OFFER_MESSAGES_H_
#include <chrono>
#include <string>
#include <vector>
#include "cast/streaming/impl/rtp_defines.h"
#include "cast/streaming/message_fields.h"
#include "cast/streaming/public/session_config.h"
#include "cast/streaming/resolution.h"
#include "json/value.h"
#include "platform/base/error.h"
#include "util/simple_fraction.h"
// This file contains the implementation of the Cast V2 Mirroring Control
// Protocol offer object definition.
namespace openscreen::cast {
// If the target delay provided by the sender is not bounded by
// [kMinTargetDelay, kMaxTargetDelay], it will be set to
// kDefaultTargetPlayoutDelay.
inline constexpr auto kMinTargetPlayoutDelay = std::chrono::milliseconds(0);
inline constexpr auto kMaxTargetPlayoutDelay = std::chrono::milliseconds(5000);
// If the sender provides an invalid maximum frame rate, it ill
// be set to kDefaultMaxFrameRate.
inline constexpr int kDefaultMaxFrameRate = 30;
inline constexpr int kDefaultNumVideoChannels = 1;
inline constexpr int kDefaultNumAudioChannels = 2;
// A stream, as detailed by the CastV2 protocol spec, is a segment of an
// offer message specifically representing a configuration object for
// a codec and its related fields, such as maximum bit rate, time base,
// and other fields.
// Composed classes include AudioStream and VideoStream, which contain
// fields specific to audio and video respectively.
struct Stream {
enum class Type : uint8_t { kAudioSource, kVideoSource };
static ErrorOr<Stream> TryParse(const Json::Value& root, Stream::Type type);
Json::Value ToJson() const;
bool IsValid() const;
int index = 0;
Type type = {};
// Default channel count is 1, e.g. for video.
int channels = 0;
RtpPayloadType rtp_payload_type = {};
Ssrc ssrc = {};
std::chrono::milliseconds target_delay = {};
// AES Key and IV mask format is very strict: a 32 digit hex string that
// must be converted to a 16 digit byte array.
std::array<uint8_t, 16> aes_key = {};
std::array<uint8_t, 16> aes_iv_mask = {};
// The event logs are generally recommended for use in gathering statistics
// for the sender session.
bool receiver_rtcp_event_log = true;
std::optional<int> receiver_rtcp_dscp;
int rtp_timebase = 0;
// The codec parameter field honors the format laid out in RFC 6381:
// https://datatracker.ietf.org/doc/html/rfc6381.
std::string codec_parameter;
std::vector<std::string> rtp_extensions;
};
struct AudioStream {
static ErrorOr<AudioStream> TryParse(const Json::Value& root);
Json::Value ToJson() const;
bool IsValid() const;
Stream stream;
AudioCodec codec = AudioCodec::kNotSpecified;
int bit_rate = 0;
};
struct VideoStream {
static ErrorOr<VideoStream> TryParse(const Json::Value& root);
Json::Value ToJson() const;
bool IsValid() const;
Stream stream;
VideoCodec codec = VideoCodec::kNotSpecified;
SimpleFraction max_frame_rate;
int max_bit_rate = 0;
std::string protection;
std::string profile;
std::string level;
std::vector<Resolution> resolutions;
std::string error_recovery_mode;
};
struct Offer {
static ErrorOr<Offer> TryParse(const Json::Value& root);
Json::Value ToJson() const;
bool IsValid() const;
CastMode cast_mode = CastMode::kMirroring;
std::vector<AudioStream> audio_streams;
std::vector<VideoStream> video_streams;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_OFFER_MESSAGES_H_

View file

@ -0,0 +1,300 @@
// 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/public/receiver_message.h"
#include <utility>
#include <variant>
#include "cast/streaming/message_fields.h"
#include "json/reader.h"
#include "json/writer.h"
#include "platform/base/error.h"
#include "util/base64.h"
#include "util/enum_name_table.h"
#include "util/json/json_helpers.h"
#include "util/json/json_serialization.h"
#include "util/osp_logging.h"
#include "util/string_util.h"
#include "util/stringprintf.h"
namespace openscreen::cast {
namespace {
EnumNameTable<ReceiverMessage::Type, 4> kMessageTypeNames{
{{kMessageTypeAnswer, ReceiverMessage::Type::kAnswer},
{"CAPABILITIES_RESPONSE", ReceiverMessage::Type::kCapabilitiesResponse},
{"RPC", ReceiverMessage::Type::kRpc},
{"INPUT", ReceiverMessage::Type::kInput}}};
EnumNameTable<MediaCapability, 10> kMediaCapabilityNames{
{{"audio", MediaCapability::kAudio},
{"aac", MediaCapability::kAac},
{"opus", MediaCapability::kOpus},
{"video", MediaCapability::kVideo},
{"4k", MediaCapability::k4k},
{"h264", MediaCapability::kH264},
{"vp8", MediaCapability::kVp8},
{"vp9", MediaCapability::kVp9},
{"hevc", MediaCapability::kHevc},
{"av1", MediaCapability::kAv1}}};
ReceiverMessage::Type GetMessageType(const Json::Value& root) {
std::string type;
if (!json::TryParseString(root[kMessageType], &type)) {
return ReceiverMessage::Type::kUnknown;
}
string_util::AsciiStrToUpper(type);
ErrorOr<ReceiverMessage::Type> parsed = GetEnum(kMessageTypeNames, type);
return parsed.value(ReceiverMessage::Type::kUnknown);
}
bool TryParseCapability(const Json::Value& value, MediaCapability* out) {
std::string c;
if (!json::TryParseString(value, &c)) {
return false;
}
const ErrorOr<MediaCapability> capability = GetEnum(kMediaCapabilityNames, c);
if (capability.is_error()) {
return false;
}
*out = capability.value();
return true;
}
} // namespace
ReceiverError::ReceiverError(int code, std::string_view description)
: code(code), description(description) {
if (code >= kOpenscreenErrorOffset) {
openscreen_code = static_cast<Error::Code>(code - kOpenscreenErrorOffset);
}
}
ReceiverError::ReceiverError(Error::Code code, std::string_view description)
: code(static_cast<int>(code) + kOpenscreenErrorOffset),
openscreen_code(code),
description(description) {}
ReceiverError::ReceiverError(const Error& error)
: code(static_cast<int>(error.code()) + kOpenscreenErrorOffset),
openscreen_code(error.code()),
description(error.message()) {}
ReceiverError::ReceiverError(const ReceiverError&) = default;
ReceiverError::ReceiverError(ReceiverError&&) noexcept = default;
ReceiverError& ReceiverError::operator=(const ReceiverError&) = default;
ReceiverError& ReceiverError::operator=(ReceiverError&&) = default;
ReceiverError::~ReceiverError() = default;
// static
ErrorOr<ReceiverError> ReceiverError::Parse(const Json::Value& value) {
if (!value.isObject()) {
return Error(Error::Code::kParameterInvalid,
"Empty JSON in receiver error parsing");
}
int code;
std::string description;
if (!json::TryParseInt(value[kErrorCode], &code) ||
!json::TryParseString(value[kErrorDescription], &description)) {
return Error::Code::kJsonParseError;
}
return ReceiverError(code, description);
}
Json::Value ReceiverError::ToJson() const {
Json::Value root;
root[kErrorCode] = openscreen_code ? static_cast<int>(*openscreen_code) +
kOpenscreenErrorOffset
: code;
root[kErrorDescription] = description;
return root;
}
Error ReceiverError::ToError() const {
if (openscreen_code) {
return Error(*openscreen_code, description);
}
std::string full_description = StringFormat("Error code: {}, description: {}",
code, description.c_str());
return Error(Error::Code::kUnknownError, std::move(full_description));
}
// static
ErrorOr<ReceiverCapability> ReceiverCapability::Parse(
const Json::Value& value) {
if (!value.isObject()) {
return Error(Error::Code::kParameterInvalid,
"Empty JSON in capabilities parsing");
}
int remoting_version;
if (!json::TryParseInt(value["remoting"], &remoting_version)) {
remoting_version = ReceiverCapability::kRemotingVersionUnknown;
}
std::vector<MediaCapability> capabilities;
if (!json::TryParseArray<MediaCapability>(
value["mediaCaps"], TryParseCapability, &capabilities)) {
return Error(Error::Code::kJsonParseError,
"Failed to parse media capabilities");
}
return ReceiverCapability{remoting_version, std::move(capabilities)};
}
Json::Value ReceiverCapability::ToJson() const {
Json::Value root;
root["remoting"] = remoting_version;
Json::Value capabilities(Json::ValueType::arrayValue);
for (const auto& capability : media_capabilities) {
capabilities.append(GetEnumName(kMediaCapabilityNames, capability).value());
}
root["mediaCaps"] = std::move(capabilities);
return root;
}
// static
ErrorOr<ReceiverMessage> ReceiverMessage::Parse(const Json::Value& value) {
ReceiverMessage message;
if (!value.isObject()) {
return Error(Error::Code::kJsonParseError, "Invalid message body");
}
std::string result;
if (!json::TryParseString(value[kResult], &result)) {
result = kResultError;
}
message.type = GetMessageType(value);
message.valid =
(result == kResultOk || message.type == ReceiverMessage::Type::kRpc ||
message.type == ReceiverMessage::Type::kInput);
if (message.type != ReceiverMessage::Type::kRpc &&
message.type != ReceiverMessage::Type::kInput) {
if (!json::TryParseInt(value[kSequenceNumber],
&(message.sequence_number))) {
message.sequence_number = -1;
}
// Sequence numbers must be non-negative.
if (message.sequence_number < 0) {
message.valid = false;
}
}
if (!message.valid) {
ErrorOr<ReceiverError> error =
ReceiverError::Parse(value[kErrorMessageBody]);
if (error.is_value()) {
message.body = std::move(error.value());
}
return message;
}
switch (message.type) {
case Type::kAnswer: {
auto answer_or_error =
openscreen::cast::Answer::TryParse(value[kAnswerMessageBody]);
if (answer_or_error.is_value()) {
message.body = std::move(answer_or_error.value());
message.valid = true;
}
} break;
case Type::kCapabilitiesResponse: {
ErrorOr<ReceiverCapability> capability =
ReceiverCapability::Parse(value[kCapabilitiesMessageBody]);
if (capability.is_value()) {
message.body = std::move(capability.value());
message.valid = true;
}
} break;
case Type::kRpc: {
std::string encoded_rpc;
std::vector<uint8_t> rpc;
if (json::TryParseString(value[kRpcMessageBody], &encoded_rpc) &&
base64::Decode(encoded_rpc, &rpc)) {
message.body = std::move(rpc);
message.valid = true;
}
} break;
case Type::kInput: {
std::string encoded_input;
std::vector<uint8_t> input;
if (json::TryParseString(value[kInputMessageBody], &encoded_input) &&
base64::Decode(encoded_input, &input)) {
message.body = std::move(input);
message.valid = true;
}
} break;
default:
break;
}
return message;
}
ErrorOr<Json::Value> ReceiverMessage::ToJson() const {
OSP_CHECK(type != ReceiverMessage::Type::kUnknown)
<< "Trying to send an unknown message is a developer error";
Json::Value root;
root[kMessageType] = GetEnumName(kMessageTypeNames, type).value();
if (sequence_number >= 0) {
root[kSequenceNumber] = sequence_number;
}
switch (type) {
case ReceiverMessage::Type::kAnswer:
if (valid) {
root[kResult] = kResultOk;
root[kAnswerMessageBody] = std::get<Answer>(body).ToJson();
} else {
root[kResult] = kResultError;
root[kErrorMessageBody] = std::get<ReceiverError>(body).ToJson();
}
break;
case ReceiverMessage::Type::kCapabilitiesResponse:
if (valid) {
root[kResult] = kResultOk;
root[kCapabilitiesMessageBody] =
std::get<ReceiverCapability>(body).ToJson();
} else {
root[kResult] = kResultError;
root[kErrorMessageBody] = std::get<ReceiverError>(body).ToJson();
}
break;
// NOTE: RPC messages do NOT have a result field.
case ReceiverMessage::Type::kRpc:
root[kRpcMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
case ReceiverMessage::Type::kInput:
root[kInputMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
default:
OSP_NOTREACHED();
}
return root;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,117 @@
// 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_PUBLIC_RECEIVER_MESSAGE_H_
#define CAST_STREAMING_PUBLIC_RECEIVER_MESSAGE_H_
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#include "cast/streaming/public/answer_messages.h"
#include "json/value.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
enum class MediaCapability {
kAudio,
kAac,
kOpus,
kVideo,
k4k,
kH264,
kVp8,
kVp9,
kHevc,
kAv1
};
struct ReceiverCapability {
static constexpr int kRemotingVersionUnknown = -1;
Json::Value ToJson() const;
static ErrorOr<ReceiverCapability> Parse(const Json::Value& value);
// The remoting version that the receiver uses.
int remoting_version = kRemotingVersionUnknown;
// Set of capabilities (e.g., ac3, 4k, hevc, vp9, dolby_vision, etc.).
std::vector<MediaCapability> media_capabilities;
};
// To avoid collisions with legacy error values, all Open Screen receiver errors
// are offset.
struct ReceiverError {
explicit ReceiverError(int code, std::string_view description = "");
explicit ReceiverError(Error::Code code, std::string_view description = "");
explicit ReceiverError(const Error& error);
ReceiverError(const ReceiverError&);
ReceiverError(ReceiverError&&) noexcept;
ReceiverError& operator=(const ReceiverError&);
ReceiverError& operator=(ReceiverError&&);
~ReceiverError();
Json::Value ToJson() const;
static ErrorOr<ReceiverError> Parse(const Json::Value& value);
Error ToError() const;
// All Open Screen errors are offset by a fixed value to avoid overlapping
// with legacy values.
static constexpr int kOpenscreenErrorOffset = 10000;
// Raw error code.
int32_t code = -1;
// Parsed openscreen::Error code. May be nullopt if not a match.
std::optional<Error::Code> openscreen_code;
// Error description.
std::string description;
};
struct ReceiverMessage {
public:
// Receiver response message type.
enum class Type {
// Unknown message type.
kUnknown,
// Response to OFFER message.
kAnswer,
// Response to GET_CAPABILITIES message.
kCapabilitiesResponse,
// Rpc binary messages. The payload is base64-encoded.
kRpc,
// Input-related binary messages. The payload is base64-encoded.
kInput,
};
static ErrorOr<ReceiverMessage> Parse(const Json::Value& value);
ErrorOr<Json::Value> ToJson() const;
Type type = Type::kUnknown;
int32_t sequence_number = -1;
bool valid = false;
std::variant<std::monostate,
Answer,
std::vector<uint8_t>, // Binary-encoded protobuf message.
ReceiverCapability,
ReceiverError>
body;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_RECEIVER_MESSAGE_H_

View file

@ -0,0 +1,12 @@
// 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/public/sender.h"
namespace openscreen::cast {
Sender::Observer::~Observer() = default;
Sender::~Sender() = default;
} // namespace openscreen::cast

View file

@ -0,0 +1,173 @@
// 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_PUBLIC_SENDER_H_
#define CAST_STREAMING_PUBLIC_SENDER_H_
#include <stdint.h>
#include <chrono>
#include "cast/streaming/public/encoded_frame.h"
#include "cast/streaming/public/frame_id.h"
#include "cast/streaming/public/session_config.h"
#include "cast/streaming/rtp_time.h"
#include "cast/streaming/ssrc.h"
#include "platform/api/time.h"
namespace openscreen::cast {
// The Cast Streaming Sender, a peer corresponding to some Cast Streaming
// Receiver at the other end of a network link.
//
// The Sender is the peer responsible for enqueuing EncodedFrames for streaming,
// guaranteeing their delivery to a Receiver, and handling feedback events from
// a Receiver. Some feedback events are used for managing the Sender's internal
// queue of in-flight frames, requesting network packet re-transmits, etc.;
// while others are exposed via the Sender's public interface. For example,
// sometimes the Receiver signals that it needs a a key frame to resolve a
// picture loss condition, and the modules upstream of the Sender (e.g., where
// encoding happens) should call NeedsKeyFrame() to check for, and handle that.
//
// There are usually one or two Senders in a streaming session, one for audio
// and one for video. Both senders work with the same SenderPacketRouter
// instance to schedule their transmission of packets, and provide the necessary
// metrics for estimating bandwidth utilization and availability.
//
// It is the responsibility of upstream code modules to handle congestion
// control. With respect to this Sender, that means the media encoding bit rate
// should be throttled based on network bandwidth availability. This Sender does
// not do any throttling, only flow-control. In other words, this Sender can
// only manage its in-flight queue of frames, and if that queue grows too large,
// it will eventually reject further enqueuing.
//
// General usage: A client should check the in-flight media duration frequently
// to decide when to pause encoding, to avoid wasting system resources on
// encoding frames that will likely be rejected by the Sender. The client should
// also frequently call NeedsKeyFrame() and, when this returns true, direct its
// encoder to produce a key frame soon. Finally, when using EnqueueFrame(), an
// EncodedFrame struct should be prepared with its frame_id field set to
// whatever GetNextFrameId() returns. Please see method comments for
// more-detailed usage info.
class Sender {
public:
// Interface for receiving notifications about events of possible interest.
class Observer {
public:
// Called when a frame was canceled, which may occur in the following cases:
// - The Receiver acknowledged successful receipt of the frame.
// - The Receiver decided to skip over the frame (e.g. it was too late).
// - The Sender decided to skip the frame (e.g. OnFrameCanceled() called).
//
// Note: Frame cancellations may occur out-of-order.
virtual void OnFrameCanceled(FrameId frame_id) = 0;
// Called when a Receiver begins reporting picture loss, and there is no key
// frame currently enqueued in the Sender. The application should enqueue a
// key frame as soon as possible.
//
// This acts as a "push" notification, which is useful for immediately
// waking up an application that may be waiting for the next capture tick.
// For "pull" state checking inside a continuous encoding loop, see
// NeedsKeyFrame().
virtual void OnPictureLost() = 0;
protected:
virtual ~Observer();
};
// Result codes for EnqueueFrame().
enum EnqueueFrameResult {
// The frame has been queued for sending.
OK,
// The frame's payload was too large.
PAYLOAD_TOO_LARGE,
// The span of FrameIds is too large.
REACHED_ID_SPAN_LIMIT,
// Too-large a media duration is in-flight.
MAX_DURATION_IN_FLIGHT,
};
virtual ~Sender();
// The session configuration for this sender. The configuration is generated
// from the offer/answer exchange, and includes critical information like the
// RTP timebase, SSRCs for sending and receiving, and the AES configuration.
virtual const SessionConfig& config() const = 0;
// Sets an observer for receiving notifications. Call with nullptr to stop
// observing.
virtual void SetObserver(Observer* observer) = 0;
// Returns the number of frames currently in-flight. This is only meant to be
// informative. Clients should use GetInFlightMediaDuration() to make
// throttling decisions.
virtual size_t GetInFlightFrameCount() const = 0;
// Returns the total media duration of the frames currently in-flight,
// assuming the next not-yet-enqueued frame will have the given RTP timestamp.
// For a better user experience, the result should be compared to
// GetMaxInFlightMediaDuration(), and media encoding should be throttled down
// before additional EnqueueFrame() calls would cause this to reach the
// current maximum limit.
virtual Clock::duration GetInFlightMediaDuration(
RtpTimeTicks next_frame_rtp_timestamp) const = 0;
// Return the maximum acceptable in-flight media duration, given the current
// target playout delay setting and end-to-end network/system conditions.
virtual Clock::duration GetMaxInFlightMediaDuration() const = 0;
// Returns true if the Receiver requires a key frame. Note that this will
// return true until a key frame is accepted by EnqueueFrame(). Thus, when
// encoding is pipelined, care should be taken to instruct the encoder to
// produce just ONE forced key frame.
//
// This acts as a stateful "pull" check, which is useful for an encoder loop
// to poll right before processing the next image. For "push" notifications
// to wake up an idle application, see Observer::OnPictureLost().
virtual bool NeedsKeyFrame() const = 0;
// Returns the next FrameId, the one after the frame enqueued by the last call
// to EnqueueFrame(). Note that the next call to EnqueueFrame() assumes this
// frame ID be used.
virtual FrameId GetNextFrameId() const = 0;
// Get the current round trip time, defined as the total time between when the
// sender report is sent and the receiver report is received. This value is
// updated with each receiver report using a weighted moving average of 1/8
// for the new value and 7/8 for the previous value. Will be set to
// Clock::duration::zero() if no reports have been received yet.
// TODO(crbug.com/498036656): move to a more modern approach for estimating
// bandwidth.
virtual Clock::duration GetCurrentRoundTripTime() const = 0;
// Enqueues the given `frame` for sending as soon as possible. Returns OK if
// the frame is accepted, and some time later Observer::OnFrameCanceled() will
// be called once it is no longer in-flight.
//
// All fields of the `frame` must be set to valid values: the `frame_id` must
// be the same as GetNextFrameId(); both the `rtp_timestamp` and
// `reference_time` fields must be monotonically increasing relative to the
// prior frame; and the frame's `data` pointer must be set.
[[nodiscard]] virtual EnqueueFrameResult EnqueueFrame(
const EncodedFrame& frame) = 0;
// Causes all pending operations to discard data when they are processed
// later. This will notify observers by invoking OnFrameCanceled() for each
// canceled frame.
virtual void CancelInFlightData() = 0;
// May be called by the consumer to report that a frame has been dropped. This
// is used to report drop statistics to the sender's statistics collector.
virtual void ReportFrameDropEvent(FrameId frame_id,
RtpTimeTicks rtp_timestamp,
Clock::time_point drop_time) = 0;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_SENDER_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/public/session_config.h"
#include <algorithm>
#include <utility>
namespace openscreen::cast {
namespace {
bool IsNonZero(uint8_t byte) {
return byte > 0;
}
} // namespace
SessionConfig::SessionConfig(Ssrc sender_ssrc,
Ssrc receiver_ssrc,
int rtp_timebase,
int channels,
std::chrono::milliseconds target_playout_delay,
std::array<uint8_t, 16> aes_secret_key,
std::array<uint8_t, 16> aes_iv_mask,
bool is_pli_enabled,
StreamType stream_type,
bool are_receiver_event_logs_enabled)
: sender_ssrc(sender_ssrc),
receiver_ssrc(receiver_ssrc),
rtp_timebase(rtp_timebase),
channels(channels),
target_playout_delay(target_playout_delay),
aes_secret_key(std::move(aes_secret_key)),
aes_iv_mask(std::move(aes_iv_mask)),
is_pli_enabled(is_pli_enabled),
stream_type(stream_type),
are_receiver_event_logs_enabled(are_receiver_event_logs_enabled) {}
SessionConfig::SessionConfig(const SessionConfig& other) = default;
SessionConfig::SessionConfig(SessionConfig&& other) noexcept = default;
SessionConfig& SessionConfig::operator=(const SessionConfig& other) = default;
SessionConfig& SessionConfig::operator=(SessionConfig&& other) noexcept =
default;
SessionConfig::~SessionConfig() = default;
bool SessionConfig::IsValid() const {
return sender_ssrc > 0 && receiver_ssrc > 0 && rtp_timebase > 0 &&
channels > 0 &&
std::any_of(aes_secret_key.begin(), aes_secret_key.end(), IsNonZero) &&
std::any_of(aes_iv_mask.begin(), aes_iv_mask.end(), IsNonZero);
}
} // namespace openscreen::cast

View file

@ -0,0 +1,71 @@
// 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_PUBLIC_SESSION_CONFIG_H_
#define CAST_STREAMING_PUBLIC_SESSION_CONFIG_H_
#include <array>
#include <chrono>
#include <cstdint>
#include "cast/streaming/public/constants.h"
#include "cast/streaming/ssrc.h"
namespace openscreen::cast {
// Common streaming configuration, established from the OFFER/ANSWER exchange,
// that the Sender and Receiver are both assuming.
struct SessionConfig final {
SessionConfig(Ssrc sender_ssrc,
Ssrc receiver_ssrc,
int rtp_timebase,
int channels,
std::chrono::milliseconds target_playout_delay,
std::array<uint8_t, 16> aes_secret_key,
std::array<uint8_t, 16> aes_iv_mask,
bool is_pli_enabled = false,
StreamType stream_type = StreamType::kUnknown,
bool are_receiver_event_logs_enabled = true);
SessionConfig(const SessionConfig& other);
SessionConfig(SessionConfig&& other) noexcept;
SessionConfig& operator=(const SessionConfig& other);
SessionConfig& operator=(SessionConfig&& other) noexcept;
~SessionConfig();
bool IsValid() const;
// The sender and receiver's SSRC identifiers. Note: SSRC identifiers
// are defined as unsigned 32 bit integers here:
// https://tools.ietf.org/html/rfc5576#page-5
Ssrc sender_ssrc = 0;
Ssrc receiver_ssrc = 0;
// RTP timebase: The number of RTP units advanced per second. For audio,
// this is the sampling rate. For video, this is 90 kHz by convention.
int rtp_timebase = 90000;
// Number of channels. Must be 1 for video, for audio typically 2.
int channels = 1;
// Initial target playout delay.
std::chrono::milliseconds target_playout_delay;
// The AES-128 crypto key and initialization vector.
std::array<uint8_t, 16> aes_secret_key{};
std::array<uint8_t, 16> aes_iv_mask{};
// Whether picture loss indication (PLI) should be used for this session.
bool is_pli_enabled = false;
// The type (e.g. audio or video) of the stream.
StreamType stream_type = StreamType::kUnknown;
// Whether RTCP event logs from the Receiver are enabled. These are used for
// generating statistics. It is recommended that this generally be true.
bool are_receiver_event_logs_enabled = true;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_SESSION_CONFIG_H_

View file

@ -0,0 +1,388 @@
// 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/public/session_messenger.h"
#include <algorithm>
#include <chrono>
#include <string>
#include "cast/common/public/message_port.h"
#include "cast/streaming/message_fields.h"
#include "platform/base/trivial_clock_traits.h"
#include "util/json/json_helpers.h"
#include "util/json/json_serialization.h"
#include "util/osp_logging.h"
#include "util/string_util.h"
namespace openscreen::cast {
namespace {
// Default timeout to receive a reply message in response to a request message
// sent by us.
constexpr std::chrono::milliseconds kReplyTimeout(4000);
// Special character indicating message was sent to all receivers or senders.
constexpr char kAnyDestination[] = "*";
void ReplyIfTimedOut(
int sequence_number,
std::vector<std::pair<int, SenderSessionMessenger::ReplyCallback>>*
replies) {
for (auto it = replies->begin(); it != replies->end(); ++it) {
if (it->first == sequence_number) {
OSP_VLOG << "Reply was an error with due to timeout for sequence number: "
<< sequence_number;
// We erase before handling the callback, since it may invalidate the
// replies vector.
SenderSessionMessenger::ReplyCallback callback = std::move(it->second);
replies->erase(it);
callback(Error(Error::Code::kMessageTimeout,
string_util::StrCat({"message timed out; max delay of ",
ToString(kReplyTimeout)})));
return;
}
}
}
} // namespace
SessionMessenger::SessionMessenger(MessagePort& message_port,
std::string source_id,
ErrorCallback cb)
: message_port_(message_port),
source_id_(source_id),
error_callback_(std::move(cb)) {
OSP_CHECK(!source_id_.empty());
message_port_->SetClient(*this);
}
SessionMessenger::~SessionMessenger() {
message_port_->ResetClient();
}
Error SessionMessenger::SendMessage(const std::string& destination_id,
const std::string& namespace_,
const Json::Value& message_root) {
OSP_CHECK(namespace_ == kCastRemotingNamespace ||
namespace_ == kCastWebrtcNamespace);
auto body_or_error = json::Stringify(message_root);
if (body_or_error.is_error()) {
return std::move(body_or_error.error());
}
OSP_VLOG << "Sending message: DESTINATION[" << destination_id
<< "], NAMESPACE[" << namespace_ << "], BODY:\n"
<< body_or_error.value();
message_port_->PostMessage(destination_id, namespace_, body_or_error.value());
return Error::None();
}
void SessionMessenger::ReportError(const Error& error) {
error_callback_(error);
}
SenderSessionMessenger::SenderSessionMessenger(MessagePort& message_port,
std::string source_id,
std::string receiver_id,
ErrorCallback cb,
TaskRunner& task_runner)
: SessionMessenger(message_port, std::move(source_id), std::move(cb)),
task_runner_(task_runner),
receiver_id_(std::move(receiver_id)) {}
void SenderSessionMessenger::SetHandler(ReceiverMessage::Type type,
ReplyCallback cb) {
// Currently the only handlers allowed are for RPC and INPUT messages.
if (type == ReceiverMessage::Type::kRpc) {
rpc_callback_ = std::move(cb);
} else if (type == ReceiverMessage::Type::kInput) {
input_callback_ = std::move(cb);
} else {
OSP_NOTREACHED();
}
}
void SenderSessionMessenger::ResetHandler(ReceiverMessage::Type type) {
if (type == ReceiverMessage::Type::kRpc) {
rpc_callback_ = {};
} else if (type == ReceiverMessage::Type::kInput) {
input_callback_ = {};
} else {
OSP_NOTREACHED();
}
}
Error SenderSessionMessenger::SendOutboundMessage(SenderMessage message) {
const auto namespace_ = (message.type == SenderMessage::Type::kRpc ||
message.type == SenderMessage::Type::kInput)
? kCastRemotingNamespace
: kCastWebrtcNamespace;
ErrorOr<Json::Value> jsonified = message.ToJson();
OSP_CHECK(jsonified.is_value()) << "Tried to send an invalid message";
return SessionMessenger::SendMessage(receiver_id_, namespace_,
jsonified.value());
}
Error SenderSessionMessenger::SendRpcMessage(ByteView message) {
return SendOutboundMessage(SenderMessage{
openscreen::cast::SenderMessage::Type::kRpc,
-1 /* sequence_number, unused by RPC messages */, true /* valid */,
std::vector<uint8_t>(message.begin(), message.end())});
}
Error SenderSessionMessenger::SendInputMessage(ByteView message) {
return SendOutboundMessage(SenderMessage{
openscreen::cast::SenderMessage::Type::kInput,
-1 /* sequence_number, unused by INPUT messages */, true /* valid */,
std::vector<uint8_t>(message.begin(), message.end())});
}
Error SenderSessionMessenger::SendRequest(SenderMessage message,
ReceiverMessage::Type reply_type,
ReplyCallback cb) {
// RPC and INPUT messages are not meant to be request/reply.
OSP_CHECK(reply_type != ReceiverMessage::Type::kRpc);
OSP_CHECK(reply_type != ReceiverMessage::Type::kInput);
if (!cb) {
return Error(Error::Code::kParameterInvalid,
"Must provide a reply callback");
}
const Error error = SendOutboundMessage(message);
if (!error.ok()) {
return error;
}
OSP_DCHECK(awaiting_replies_.find(message.sequence_number) ==
awaiting_replies_.end());
awaiting_replies_.emplace_back(message.sequence_number, std::move(cb));
task_runner_->PostTaskWithDelay(
[self = weak_factory_.GetWeakPtr(), seq_num = message.sequence_number] {
if (self) {
ReplyIfTimedOut(seq_num, &self->awaiting_replies_);
}
},
kReplyTimeout);
return Error::None();
}
void SenderSessionMessenger::OnMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message) {
if (source_id != receiver_id_ && source_id != kAnyDestination) {
OSP_DLOG_WARN << "Received message from unknown/incorrect Cast Receiver "
<< source_id << ". Currently connected to " << receiver_id_;
return;
}
if (message_namespace != kCastWebrtcNamespace &&
message_namespace != kCastRemotingNamespace) {
OSP_DLOG_WARN << "Received message from unknown namespace: "
<< message_namespace << ". Message was " << message;
return;
}
ErrorOr<Json::Value> message_body = json::Parse(message);
if (!message_body || !message_body.value().isObject()) {
ReportError(message_body.error());
OSP_DLOG_WARN << "Received an invalid message: " << message;
return;
}
// If the message is valid JSON and we don't understand it, there are two
// options: (1) it's an unknown type, or (2) the receiver filled out the
// message incorrectly. In the first case we can drop it, it's likely just
// unsupported. In the second case we might need it, so worth warning the
// client.
ErrorOr<ReceiverMessage> receiver_message =
ReceiverMessage::Parse(message_body.value());
if (receiver_message.is_error()) {
ReportError(receiver_message.error());
OSP_DLOG_WARN << "Received an invalid receiver message: "
<< receiver_message.error();
return;
}
if (receiver_message.value().type == ReceiverMessage::Type::kRpc) {
if (rpc_callback_) {
rpc_callback_(receiver_message.value());
} else {
OSP_DLOG_INFO << "Received RPC message but no callback, dropping";
}
} else if (receiver_message.value().type == ReceiverMessage::Type::kInput) {
if (input_callback_) {
input_callback_(receiver_message.value());
} else {
OSP_DLOG_INFO << "Received INPUT message but no callback, dropping";
}
} else {
const int sequence_number = receiver_message.value().sequence_number;
auto it = awaiting_replies_.find(sequence_number);
if (it == awaiting_replies_.end()) {
OSP_DLOG_WARN << "Received a reply I wasn't waiting for: "
<< sequence_number;
return;
}
ReplyCallback callback = std::move(it->second);
awaiting_replies_.erase(it);
callback(std::move(receiver_message.value()));
}
}
void SenderSessionMessenger::OnError(const Error& error) {
OSP_DLOG_WARN << "Received an error in the session messenger: " << error;
ReportError(error);
}
ReceiverSessionMessenger::ReceiverSessionMessenger(MessagePort& message_port,
std::string source_id,
ErrorCallback cb)
: SessionMessenger(message_port, std::move(source_id), std::move(cb)) {}
void ReceiverSessionMessenger::SetHandler(SenderMessage::Type type,
RequestCallback cb) {
OSP_DCHECK(callbacks_.find(type) == callbacks_.end());
callbacks_.emplace_back(type, std::move(cb));
}
void ReceiverSessionMessenger::ResetHandler(SenderMessage::Type type) {
callbacks_.erase_key(type);
}
Error ReceiverSessionMessenger::SendRpcMessage(const std::string& source_id,
ByteView message) {
return SendMessage(
source_id,
ReceiverMessage{ReceiverMessage::Type::kRpc, -1 /* sequence_number */,
true /* valid */,
std::vector<uint8_t>(message.begin(), message.end())});
}
Error ReceiverSessionMessenger::SendInputMessage(const std::string& source_id,
ByteView message) {
return SendMessage(
source_id,
ReceiverMessage{ReceiverMessage::Type::kInput, -1 /* sequence_number */,
true /* valid */,
std::vector<uint8_t>(message.begin(), message.end())});
}
Error ReceiverSessionMessenger::SendMessage(const std::string& source_id,
ReceiverMessage message) {
if (source_id.empty()) {
return Error(Error::Code::kInitializationFailure,
"Cannot send a message without a current source ID.");
}
const auto namespace_ = (message.type == ReceiverMessage::Type::kRpc ||
message.type == ReceiverMessage::Type::kInput)
? kCastRemotingNamespace
: kCastWebrtcNamespace;
ErrorOr<Json::Value> message_json = message.ToJson();
OSP_CHECK(message_json.is_value()) << "Tried to send an invalid message";
return SessionMessenger::SendMessage(source_id, namespace_,
message_json.value());
}
void ReceiverSessionMessenger::SetCustomMessageHandler(
std::string_view message_namespace,
CustomMessageCallback cb) {
auto it = std::find_if(custom_message_handlers_.begin(),
custom_message_handlers_.end(),
[&message_namespace](const auto& pair) {
return pair.first == message_namespace;
});
if (!cb) {
if (it != custom_message_handlers_.end()) {
custom_message_handlers_.erase(it);
}
return;
}
if (it != custom_message_handlers_.end()) {
OSP_LOG_ERROR << "Handler already exists for namespace: "
<< message_namespace;
return;
} else {
custom_message_handlers_.emplace_back(std::string(message_namespace),
std::move(cb));
}
}
Error ReceiverSessionMessenger::SendMessage(std::string_view destination_id,
std::string_view message_namespace,
std::string_view message) {
message_port().PostMessage(std::string(destination_id),
std::string(message_namespace),
std::string(message));
return Error::None();
}
void ReceiverSessionMessenger::OnMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message) {
if (message_namespace != kCastWebrtcNamespace &&
message_namespace != kCastRemotingNamespace) {
auto it = std::find_if(custom_message_handlers_.begin(),
custom_message_handlers_.end(),
[&message_namespace](const auto& pair) {
return pair.first == message_namespace;
});
if (it != custom_message_handlers_.end()) {
it->second(source_id, message_namespace, message);
return;
}
OSP_DLOG_WARN << "Received message from unknown namespace: "
<< message_namespace;
return;
}
// If the message is bad JSON, the sender is in a funky state so we
// report an error.
ErrorOr<Json::Value> message_body = json::Parse(message);
if (message_body.is_error() || !message_body.value().isObject()) {
ReportError(message_body.error());
return;
}
// If the message is valid JSON and we don't understand it, there are two
// options: (1) it's an unknown type, or (2) the sender filled out the message
// incorrectly. In the first case we can drop it, it's likely just
// unsupported. In the second case we might need it, so worth warning the
// client.
ErrorOr<SenderMessage> sender_message =
SenderMessage::Parse(message_body.value());
if (sender_message.is_error()) {
ReportError(sender_message.error());
OSP_DLOG_WARN << "Received an invalid sender message: "
<< sender_message.error();
return;
}
if (sender_message.value().type == SenderMessage::Type::kOffer ||
sender_message.value().type == SenderMessage::Type::kGetCapabilities) {
OSP_VLOG << "Received Message:\n" << message;
}
auto it = callbacks_.find(sender_message.value().type);
if (it == callbacks_.end()) {
OSP_DLOG_INFO << "Received message without a callback, dropping";
return;
}
it->second(source_id, sender_message.value());
}
void ReceiverSessionMessenger::OnError(const Error& error) {
OSP_DLOG_WARN << "Received an error in the session messenger: " << error;
ReportError(error);
}
} // 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_PUBLIC_SESSION_MESSENGER_H_
#define CAST_STREAMING_PUBLIC_SESSION_MESSENGER_H_
#include <functional>
#include <string>
#include <utility>
#include <vector>
#include "cast/common/public/message_port.h"
#include "cast/streaming/public/answer_messages.h"
#include "cast/streaming/public/offer_messages.h"
#include "cast/streaming/public/receiver_message.h"
#include "cast/streaming/sender_message.h"
#include "json/value.h"
#include "platform/api/task_runner.h"
#include "platform/base/span.h"
#include "util/flat_map.h"
#include "util/raw_ref.h"
#include "util/weak_ptr.h"
namespace openscreen::cast {
// A message port interface designed specifically for use by the Receiver
// and Sender session classes.
class SessionMessenger : public MessagePort::Client {
public:
using ErrorCallback = std::function<void(Error)>;
SessionMessenger(MessagePort& message_port,
std::string source_id,
ErrorCallback cb);
~SessionMessenger() override;
MessagePort& message_port() { return *message_port_; }
protected:
// Barebones message sending method shared by both children.
[[nodiscard]] Error SendMessage(const std::string& destination_id,
const std::string& namespace_,
const Json::Value& message_root);
// Used to report errors in subclasses.
void ReportError(const Error& error);
const std::string& source_id() override { return source_id_; }
private:
const raw_ref<MessagePort> message_port_;
const std::string source_id_;
ErrorCallback error_callback_;
};
// Message port interface designed to handle sending messages to and
// from a receiver. When possible, errors receiving messages are reported
// to the ReplyCallback passed to SendRequest(), otherwise errors are
// reported to the ErrorCallback passed in the constructor.
class SenderSessionMessenger final : public SessionMessenger {
public:
using ReplyCallback = std::function<void(ErrorOr<ReceiverMessage>)>;
SenderSessionMessenger(MessagePort& message_port,
std::string source_id,
std::string receiver_id,
ErrorCallback cb,
TaskRunner& task_runner);
// Set receiver message handler. Note that this should only be
// applied for messages that don't have sequence numbers, like RPC
// and status messages.
void SetHandler(ReceiverMessage::Type type, ReplyCallback cb);
void ResetHandler(ReceiverMessage::Type type);
// Send a message that doesn't require a reply.
[[nodiscard]] Error SendOutboundMessage(SenderMessage message);
// Convenience method for sending a valid RPC message.
[[nodiscard]] Error SendRpcMessage(ByteView message);
// Convenience method for sending a valid INPUT message.
[[nodiscard]] Error SendInputMessage(ByteView message);
// Send a request (with optional reply callback).
[[nodiscard]] Error SendRequest(SenderMessage message,
ReceiverMessage::Type reply_type,
ReplyCallback cb);
// MessagePort::Client overrides
void OnMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message) override;
void OnError(const Error& error) override;
private:
const raw_ref<TaskRunner> task_runner_;
// This messenger should only be connected to one receiver, so `receiver_id_`
// should not change.
const std::string receiver_id_;
// We keep a list here of replies we are expecting--if the reply is
// received for this sequence number, we call its respective callback,
// otherwise it is called after an internally specified timeout.
FlatMap<int, ReplyCallback> awaiting_replies_;
// Currently we can only set a handler for RPC messages, so no need for
// a flatmap here.
ReplyCallback rpc_callback_;
ReplyCallback input_callback_;
WeakPtrFactory<SenderSessionMessenger> weak_factory_{this};
};
// Message port interface designed for messaging to and from a sender.
class ReceiverSessionMessenger final : public SessionMessenger {
public:
using RequestCallback =
std::function<void(const std::string&, SenderMessage)>;
ReceiverSessionMessenger(MessagePort& message_port,
std::string source_id,
ErrorCallback cb);
// Set sender message handler.
void SetHandler(SenderMessage::Type type, RequestCallback cb);
void ResetHandler(SenderMessage::Type type);
// Convenience method for sending a valid RPC message.
[[nodiscard]] Error SendRpcMessage(const std::string& source_id,
ByteView message);
// Convenience method for sending a valid INPUT message.
[[nodiscard]] Error SendInputMessage(const std::string& source_id,
ByteView message);
// Send a JSON message.
[[nodiscard]] Error SendMessage(const std::string& source_id,
ReceiverMessage message);
// Send a raw string message to a custom namespace.
[[nodiscard]] Error SendMessage(std::string_view destination_id,
std::string_view message_namespace,
std::string_view message);
using CustomMessageCallback =
std::function<void(const std::string& /* source_id */,
const std::string& /* message_namespace */,
const std::string& /* message */)>;
void SetCustomMessageHandler(std::string_view message_namespace,
CustomMessageCallback cb);
// MessagePort::Client overrides
void OnMessage(const std::string& source_id,
const std::string& message_namespace,
const std::string& message) override;
void OnError(const Error& error) override;
private:
FlatMap<SenderMessage::Type, RequestCallback> callbacks_;
std::vector<std::pair<std::string, CustomMessageCallback>>
custom_message_handlers_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_SESSION_MESSENGER_H_

View file

@ -0,0 +1,183 @@
// 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/public/statistics.h"
#include <algorithm>
#include <iomanip>
#include <iostream>
#include "util/enum_name_table.h"
#include "util/json/json_helpers.h"
#include "util/json/json_serialization.h"
#include "util/stringprintf.h"
namespace openscreen::cast {
namespace {
template <typename Type>
Json::Value ToJson(const Type& t) {
return t.ToJson();
}
template <>
Json::Value ToJson(const double& t) {
return t;
}
template <typename T, typename Type>
Json::Value ArrayToJson(
const std::array<T, static_cast<size_t>(Type::kNumTypes)>& list,
const EnumNameTable<Type, static_cast<size_t>(Type::kNumTypes)>& names) {
Json::Value out;
for (size_t i = 0; i < list.size(); ++i) {
ErrorOr<const char*> name = GetEnumName(names, static_cast<Type>(i));
OSP_CHECK(name);
out[name.value()] = ToJson(list[i]);
}
return out;
}
} // namespace
// External linkage for unit test
extern const EnumNameTable<StatisticType,
static_cast<size_t>(StatisticType::kNumTypes)>
kStatisticTypeNames = {
{{"EnqueueFps", StatisticType::kEnqueueFps},
{"AvgCaptureLatencyMs", StatisticType::kAvgCaptureLatencyMs},
{"AvgEncodeTimeMs", StatisticType::kAvgEncodeTimeMs},
{"AvgQueueingLatencyMs", StatisticType::kAvgQueueingLatencyMs},
{"AvgNetworkLatencyMs", StatisticType::kAvgNetworkLatencyMs},
{"AvgPacketLatencyMs", StatisticType::kAvgPacketLatencyMs},
{"AvgFrameLatencyMs", StatisticType::kAvgFrameLatencyMs},
{"AvgEndToEndLatencyMs", StatisticType::kAvgEndToEndLatencyMs},
{"EncodeRateKbps", StatisticType::kEncodeRateKbps},
{"PacketTransmissionRateKbps",
StatisticType::kPacketTransmissionRateKbps},
{"TimeSinceLastReceiverResponseMs",
StatisticType::kTimeSinceLastReceiverResponseMs},
{"NumFramesCaptured", StatisticType::kNumFramesCaptured},
{"NumFramesDroppedByEncoder",
StatisticType::kNumFramesDroppedByEncoder},
{"NumLateFrames", StatisticType::kNumLateFrames},
{"NumPacketsSent", StatisticType::kNumPacketsSent},
{"NumPacketsReceived", StatisticType::kNumPacketsReceived},
{"FirstEventTimeMs", StatisticType::kFirstEventTimeMs},
{"LastEventTimeMs", StatisticType::kLastEventTimeMs}}};
// External linkage for unit test
extern const EnumNameTable<HistogramType,
static_cast<size_t>(HistogramType::kNumTypes)>
kHistogramTypeNames = {
{{"CaptureLatencyMs", HistogramType::kCaptureLatencyMs},
{"EncodeTimeMs", HistogramType::kEncodeTimeMs},
{"QueueingLatencyMs", HistogramType::kQueueingLatencyMs},
{"NetworkLatencyMs", HistogramType::kNetworkLatencyMs},
{"PacketLatencyMs", HistogramType::kPacketLatencyMs},
{"EndToEndLatencyMs", HistogramType::kEndToEndLatencyMs},
{"FrameLatenessMs", HistogramType::kFrameLatenessMs}}};
SimpleHistogram::SimpleHistogram() = default;
SimpleHistogram::SimpleHistogram(int64_t min, int64_t max, int64_t width)
: min(min), max(max), width(width), buckets((max - min) / width + 2) {
OSP_CHECK_GT(buckets.size(), 2u);
OSP_CHECK_EQ(0, (max - min) % width);
}
SimpleHistogram::SimpleHistogram(const SimpleHistogram&) = default;
SimpleHistogram::SimpleHistogram(SimpleHistogram&&) noexcept = default;
SimpleHistogram& SimpleHistogram::operator=(const SimpleHistogram&) = default;
SimpleHistogram& SimpleHistogram::operator=(SimpleHistogram&&) = default;
SimpleHistogram::~SimpleHistogram() = default;
bool SimpleHistogram::operator==(const SimpleHistogram& other) const {
return min == other.min && max == other.max && width == other.width &&
buckets == other.buckets;
}
void SimpleHistogram::Add(int64_t sample) {
if (sample < min) {
++buckets.front();
} else if (sample >= max) {
++buckets.back();
} else {
size_t index = 1 + (sample - min) / width;
OSP_CHECK_LT(index, buckets.size());
++buckets[index];
}
}
void SimpleHistogram::Reset() {
buckets.assign(buckets.size(), 0);
}
Json::Value SimpleHistogram::ToJson() const {
// Nest the bucket values in an array instead of a dictionary, so we sort
// numerically instead of alphabetically.
Json::Value out(Json::ValueType::arrayValue);
for (size_t i = 0; i < buckets.size(); ++i) {
if (buckets[i] != 0) {
Json::Value entry;
entry[GetBucketName(i)] = buckets[i];
out.append(entry);
}
}
return out;
}
std::string SimpleHistogram::ToString() const {
return json::Stringify(ToJson()).value();
}
SimpleHistogram::SimpleHistogram(int64_t min,
int64_t max,
int64_t width,
std::vector<int> buckets)
: SimpleHistogram(min, max, width) {
this->buckets = std::move(buckets);
}
std::string SimpleHistogram::GetBucketName(size_t index) const {
if (index == 0) {
return "<" + std::to_string(min);
}
if (index == buckets.size() - 1) {
return ">=" + std::to_string(max);
}
// See the constructor comment for an example of how these bucket bounds
// are calculated.
const int bucket_min = min + width * (index - 1);
const int bucket_max = min + index * width - 1;
return StringFormat("{}-{}", bucket_min, bucket_max);
}
Json::Value SenderStats::ToJson() const {
Json::Value out;
out["audio_statistics"] = ArrayToJson(audio_statistics, kStatisticTypeNames);
out["audio_histograms"] = ArrayToJson(audio_histograms, kHistogramTypeNames);
out["video_statistics"] = ArrayToJson(video_statistics, kStatisticTypeNames);
out["video_histograms"] = ArrayToJson(video_histograms, kHistogramTypeNames);
return out;
}
std::string SenderStats::ToString() const {
return json::Stringify(ToJson()).value();
}
std::ostream& operator<<(std::ostream& out, const SenderStats& stats) {
return out << stats.ToString();
}
std::ostream& operator<<(std::ostream& out, const SimpleHistogram& histogram) {
return out << histogram.ToString();
}
SenderStatsClient::~SenderStatsClient() {}
} // namespace openscreen::cast

View file

@ -0,0 +1,195 @@
// Copyright 2024 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_PUBLIC_STATISTICS_H_
#define CAST_STREAMING_PUBLIC_STATISTICS_H_
#include <stddef.h>
#include <stdint.h>
#include <string>
#include <utility>
#include <vector>
#include "cast/streaming/public/frame_id.h"
#include "cast/streaming/rtp_time.h"
#include "json/value.h"
#include "platform/api/time.h"
namespace openscreen::cast {
// This file must be updated whenever sender_stats.proto is updated.
enum class StatisticType {
// Frame enqueuing rate.
kEnqueueFps = 0,
// Average capture latency in milliseconds.
kAvgCaptureLatencyMs,
// Average encode duration in milliseconds.
kAvgEncodeTimeMs,
// Duration from when a frame is encoded to when the packet is first
// sent.
kAvgQueueingLatencyMs,
// Duration from when a packet is transmitted to when it is received.
// This measures latency from sender to receiver.
kAvgNetworkLatencyMs,
// Duration from when a frame is encoded to when the packet is first
// received.
kAvgPacketLatencyMs,
// Average latency between frame encoded and the moment when the frame
// is fully received.
kAvgFrameLatencyMs,
// Duration from when a frame is captured to when it should be played out.
kAvgEndToEndLatencyMs,
// Encode bitrate in kbps.
kEncodeRateKbps,
// Packet transmission bitrate in kbps.
kPacketTransmissionRateKbps,
// Duration in milliseconds since the estimated last time the receiver sent
// a response.
kTimeSinceLastReceiverResponseMs,
// Number of frames captured.
kNumFramesCaptured,
// Number of frames dropped by encoder.
kNumFramesDroppedByEncoder,
// Number of late frames.
kNumLateFrames,
// Number of packets that were sent.
kNumPacketsSent,
// Number of packets that were received by receiver.
kNumPacketsReceived,
// Unix time in milliseconds of first event since reset.
kFirstEventTimeMs,
// Unix time in milliseconds of last event since reset.
kLastEventTimeMs,
// The number of statistic types.
kNumTypes = kLastEventTimeMs + 1
};
enum class HistogramType {
// Histogram representing the capture latency (in milliseconds).
kCaptureLatencyMs,
// Histogram representing the encode time (in milliseconds).
kEncodeTimeMs,
// Histogram representing the queueing latency (in milliseconds).
kQueueingLatencyMs,
// Histogram representing the network latency (in milliseconds).
kNetworkLatencyMs,
// Histogram representing the packet latency (in milliseconds).
kPacketLatencyMs,
// Histogram representing the end to end latency (in milliseconds).
kEndToEndLatencyMs,
// Histogram representing how late frames are (in milliseconds).
kFrameLatenessMs,
// The number of histogram types.
kNumTypes = kFrameLatenessMs + 1
};
struct SimpleHistogram {
// This will create N+2 buckets where N = (max - min) / width:
// Underflow bucket: < min
// Bucket 0: [min, min + width - 1]
// Bucket 1: [min + width, min + 2 * width - 1]
// ...
// Bucket N-1: [max - width, max - 1]
// Overflow bucket: >= max
// `min` must be less than `max`.
// `width` must divide `max - min` evenly.
SimpleHistogram(int64_t min, int64_t max, int64_t width);
SimpleHistogram();
SimpleHistogram(const SimpleHistogram&);
SimpleHistogram(SimpleHistogram&&) noexcept;
SimpleHistogram& operator=(const SimpleHistogram&);
SimpleHistogram& operator=(SimpleHistogram&&);
~SimpleHistogram();
bool operator==(const SimpleHistogram&) const;
void Add(int64_t sample);
void Reset();
Json::Value ToJson() const;
std::string ToString() const;
int64_t min = 1;
int64_t max = 1;
int64_t width = 1;
std::vector<int> buckets;
private:
SimpleHistogram(int64_t min,
int64_t max,
int64_t width,
std::vector<int> buckets);
std::string GetBucketName(size_t index) const;
};
std::ostream& operator<<(std::ostream& out, const SimpleHistogram& histogram);
struct SenderStats {
using StatisticsList =
std::array<double, static_cast<size_t>(StatisticType::kNumTypes)>;
using HistogramsList =
std::array<SimpleHistogram,
static_cast<size_t>(HistogramType::kNumTypes)>;
// The current audio statistics.
StatisticsList audio_statistics = {};
// The current audio histograms.
HistogramsList audio_histograms = {};
// The current video statistics.
StatisticsList video_statistics = {};
// The current video histograms.
HistogramsList video_histograms = {};
Json::Value ToJson() const;
std::string ToString() const;
};
std::ostream& operator<<(std::ostream& out, const SenderStats& stats);
// The consumer may provide a statistics client if they are interested in
// getting statistics about the ongoing session.
class SenderStatsClient {
public:
// Gets called regularly with updated statistics while they are being
// generated.
virtual void OnStatisticsUpdated(const SenderStats& updated_stats) = 0;
protected:
virtual ~SenderStatsClient();
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_STATISTICS_H_

View file

@ -0,0 +1,141 @@
// 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/resolution.h"
#include <utility>
#include "cast/streaming/message_fields.h"
#include "platform/base/error.h"
#include "util/json/json_helpers.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
namespace {
/// Dimension properties.
// Width in pixels.
constexpr char kWidth[] = "width";
// Height in pixels.
constexpr char kHeight[] = "height";
// Frame rate as a rational decimal number or fraction.
// E.g. 30 and "3000/1001" are both valid representations.
constexpr char kFrameRate[] = "frameRate";
// Choice of epsilon for double comparison allows for proper comparison
// for both aspect ratios and frame rates. For frame rates, it is based on the
// broadcast rate of 29.97fps, which is actually 29.976. For aspect ratios, it
// allows for a one-pixel difference at a 4K resolution, we want it to be
// relatively high to avoid false negative comparison results.
bool FrameRateEquals(double a, double b) {
const double kEpsilonForFrameRateComparisons = .0001;
return std::abs(a - b) < kEpsilonForFrameRateComparisons;
}
} // namespace
ErrorOr<Resolution> Resolution::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError,
"Resolution is not a JSON object");
}
Resolution out;
if (!json::TryParseInt(root[kWidth], &out.width) ||
!json::TryParseInt(root[kHeight], &out.height)) {
return Error(Error::Code::kJsonParseError, "Invalid resolution");
}
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid resolution values");
}
return out;
}
bool Resolution::IsValid() const {
return width > 0 && height > 0;
}
Json::Value Resolution::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root[kWidth] = width;
root[kHeight] = height;
return root;
}
bool Resolution::operator==(const Resolution& other) const {
return std::tie(width, height) == std::tie(other.width, other.height);
}
bool Resolution::operator!=(const Resolution& other) const {
return !(*this == other);
}
bool Resolution::IsSupersetOf(const Resolution& other) const {
return width >= other.width && height >= other.height;
}
ErrorOr<Dimensions> Dimensions::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError,
"Dimensions is not a JSON object");
}
Dimensions out;
if (!json::TryParseInt(root[kWidth], &out.width) ||
!json::TryParseInt(root[kHeight], &out.height)) {
return Error(Error::Code::kJsonParseError, "Invalid dimensions");
}
if (!root[kFrameRate].isNull()) {
if (!json::TryParseSimpleFraction(root[kFrameRate], &out.frame_rate)) {
return Error(Error::Code::kJsonParseError, "Invalid frame rate");
}
}
if (!out.IsValid()) {
return Error(Error::Code::kJsonParseError, "Invalid dimensions values");
}
return out;
}
bool Dimensions::IsValid() const {
return width > 0 && height > 0 && frame_rate.is_positive();
}
Json::Value Dimensions::ToJson() const {
OSP_CHECK(IsValid());
Json::Value root;
root[kWidth] = width;
root[kHeight] = height;
root[kFrameRate] = frame_rate.ToString();
return root;
}
bool Dimensions::operator==(const Dimensions& other) const {
return (std::tie(width, height) == std::tie(other.width, other.height) &&
FrameRateEquals(static_cast<double>(frame_rate),
static_cast<double>(other.frame_rate)));
}
bool Dimensions::operator!=(const Dimensions& other) const {
return !(*this == other);
}
bool Dimensions::IsSupersetOf(const Dimensions& other) const {
if (static_cast<double>(frame_rate) !=
static_cast<double>(other.frame_rate)) {
return static_cast<double>(frame_rate) >=
static_cast<double>(other.frame_rate);
}
return ToResolution().IsSupersetOf(other.ToResolution());
}
} // namespace openscreen::cast

View file

@ -0,0 +1,67 @@
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Resolutions and dimensions (resolutions with a frame rate) are used
// extensively throughout cast streaming. Since their serialization to and
// from JSON is stable and standard, we have a single place definition for
// these for use both in our public APIs and private messages.
#ifndef CAST_STREAMING_RESOLUTION_H_
#define CAST_STREAMING_RESOLUTION_H_
#include "json/value.h"
#include "util/simple_fraction.h"
namespace openscreen::cast {
// A resolution in pixels.
struct Resolution {
static ErrorOr<Resolution> TryParse(const Json::Value& value);
bool IsValid() const;
Json::Value ToJson() const;
// Returns true if both `width` and `height` of this instance are greater than
// or equal to that of `other`.
bool IsSupersetOf(const Resolution& other) const;
bool operator==(const Resolution& other) const;
bool operator!=(const Resolution& other) const;
// Width and height in pixels.
int width = 0;
int height = 0;
};
// A resolution in pixels and a frame rate.
struct Dimensions {
static ErrorOr<Dimensions> TryParse(const Json::Value& value);
bool IsValid() const;
Json::Value ToJson() const;
// Returns true if all properties of this instance are greater than or equal
// to those of `other`.
bool IsSupersetOf(const Dimensions& other) const;
bool operator==(const Dimensions& other) const;
bool operator!=(const Dimensions& other) const;
// Get just the width and height fields (for comparisons).
constexpr Resolution ToResolution() const { return {width, height}; }
// The effective bit rate is the width * height * frame rate.
constexpr int effective_bit_rate() const {
return width * height * static_cast<double>(frame_rate);
}
// Width and height in pixels.
int width = 0;
int height = 0;
// `frame_rate` is the maximum maintainable frame rate.
SimpleFraction frame_rate{0, 1};
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_RESOLUTION_H_

View file

@ -0,0 +1,23 @@
// 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.
#include "cast/streaming/rtp_time.h"
#include <sstream>
namespace openscreen::cast {
std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs) {
if (rhs.value_ >= 0)
out << "RTP+";
else
out << "RTP";
return out << rhs.value_;
}
std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs) {
return out << "RTP@" << rhs.value_;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,257 @@
// 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_RTP_TIME_H_
#define CAST_STREAMING_RTP_TIME_H_
#include <stdint.h>
#include <chrono>
#include <cmath>
#include <limits>
#include <sstream>
#include <type_traits>
#include "cast/streaming/impl/expanded_value_base.h"
#include "platform/api/time.h"
#include "util/saturate_cast.h"
namespace openscreen::cast {
// Forward declarations (see below).
class RtpTimeDelta;
class RtpTimeTicks;
// Convenience operator overloads for logging.
std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs);
std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs);
// The difference between two RtpTimeTicks values. This data type is modeled
// off of Chromium's base::TimeDelta, and used for performing compiler-checked
// arithmetic with RtpTimeTicks.
//
// This data type wraps a value, providing only the meaningful set of math
// operations that may be performed on the value. RtpTimeDeltas may be
// added/subtracted with other RtpTimeDeltas to produce a RtpTimeDelta holding
// the sum/difference. RtpTimeDeltas may also be multiplied or divided by
// integer amounts. Finally, RtpTimeDeltas may be divided by other
// RtpTimeDeltas to compute a number of periods (trunc'ed to an integer), or
// modulo each other to determine a time period remainder.
//
// The base class provides bit truncation/extension features for
// wire-formatting, and also the comparison operators.
//
// Usage example:
//
// // Time math.
// RtpTimeDelta zero;
// RtpTimeDelta one_second_later =
// zero + RtpTimeDelta::FromTicks(kAudioSamplingRate);
// RtpTimeDelta ten_seconds_later = one_second_later * 10;
// int64_t ten_periods = ten_seconds_later / one_second_later;
//
// // Logging convenience.
// OSP_DLOG_INFO << "The RTP time offset is " << ten_seconds_later;
//
// // Convert (approximately!) between RTP timebase and microsecond timebase:
// RtpTimeDelta nine_seconds_in_rtp = ten_seconds_later - one_second_later;
// using std::chrono::microseconds;
// microseconds nine_seconds_duration =
// nine_seconds_in_rtp.ToDuration<microseconds>(kAudioSamplingRate);
// RtpTimeDelta two_seconds_in_rtp =
// RtpTimeDelta::FromDuration(std::chrono::seconds(2),
// kAudioSamplingRate);
class RtpTimeDelta : public ExpandedValueBase<int64_t, RtpTimeDelta> {
public:
constexpr RtpTimeDelta() : ExpandedValueBase(0) {}
// Arithmetic operators (with other deltas).
constexpr RtpTimeDelta operator+(RtpTimeDelta rhs) const {
return RtpTimeDelta(value_ + rhs.value_);
}
constexpr RtpTimeDelta operator-(RtpTimeDelta rhs) const {
return RtpTimeDelta(value_ - rhs.value_);
}
constexpr RtpTimeDelta& operator+=(RtpTimeDelta rhs) {
return (*this = (*this + rhs));
}
constexpr RtpTimeDelta& operator-=(RtpTimeDelta rhs) {
return (*this = (*this - rhs));
}
constexpr RtpTimeDelta operator-() const { return RtpTimeDelta(-value_); }
// Multiplicative operators (with other deltas).
constexpr int64_t operator/(RtpTimeDelta rhs) const {
return value_ / rhs.value_;
}
constexpr RtpTimeDelta operator%(RtpTimeDelta rhs) const {
return RtpTimeDelta(value_ % rhs.value_);
}
constexpr RtpTimeDelta& operator%=(RtpTimeDelta rhs) {
return (*this = (*this % rhs));
}
// Multiplicative operators (with integer types).
template <typename IntType>
constexpr RtpTimeDelta operator*(IntType rhs) const {
static_assert(std::numeric_limits<IntType>::is_integer,
"|rhs| must be a POD integer type");
return RtpTimeDelta(value_ * rhs);
}
template <typename IntType>
constexpr RtpTimeDelta operator/(IntType rhs) const {
static_assert(std::numeric_limits<IntType>::is_integer,
"|rhs| must be a POD integer type");
return RtpTimeDelta(value_ / rhs);
}
template <typename IntType>
constexpr RtpTimeDelta& operator*=(IntType rhs) {
return (*this = (*this * rhs));
}
template <typename IntType>
constexpr RtpTimeDelta& operator/=(IntType rhs) {
return (*this = (*this / rhs));
}
// Maps this RtpTimeDelta to an approximate std::chrono::duration using the
// given RTP timebase. Assumes a zero-valued Duration corresponds to a
// zero-valued RtpTimeDelta.
template <typename Duration>
Duration ToDuration(int rtp_timebase) const {
OSP_CHECK_GT(rtp_timebase, 0);
constexpr Duration kOneSecond =
std::chrono::duration_cast<Duration>(std::chrono::seconds(1));
return Duration(ToNearestRepresentativeValue<typename Duration::rep>(
static_cast<double>(value_) / rtp_timebase * kOneSecond.count()));
}
// Maps the `duration` to an approximate RtpTimeDelta using the given RTP
// timebase. Assumes a zero-valued Duration corresponds to a zero-valued
// RtpTimeDelta.
template <typename Duration>
static constexpr RtpTimeDelta FromDuration(Duration duration,
int rtp_timebase) {
constexpr Duration kOneSecond =
std::chrono::duration_cast<Duration>(std::chrono::seconds(1));
static_assert(kOneSecond > Duration::zero(),
"Duration is too coarse-grained to represent one second.");
return RtpTimeDelta(ToNearestRepresentativeValue<int64_t>(
static_cast<double>(duration.count()) / kOneSecond.count() *
rtp_timebase));
}
// Construct a RtpTimeDelta from an exact number of ticks.
static constexpr RtpTimeDelta FromTicks(int64_t ticks) {
return RtpTimeDelta(ticks);
}
private:
friend class ExpandedValueBase<int64_t, RtpTimeDelta>;
friend class RtpTimeTicks;
friend std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs);
constexpr explicit RtpTimeDelta(int64_t ticks) : ExpandedValueBase(ticks) {}
constexpr int64_t value() const { return value_; }
template <typename Rep>
static std::enable_if_t<std::is_floating_point<Rep>::value, Rep>
ToNearestRepresentativeValue(double ticks) {
return Rep(ticks);
}
template <typename Rep>
static std::enable_if_t<std::is_integral<Rep>::value, Rep>
ToNearestRepresentativeValue(double ticks) {
return rounded_saturate_cast<Rep>(ticks);
}
};
// A media timestamp whose timebase matches the periodicity of the content
// (e.g., for audio, the timebase would be the sampling frequency). This data
// type is modeled off of Chromium's base::TimeTicks.
//
// This data type wraps a value, providing only the meaningful set of math
// operations that may be performed on the value. The difference between two
// RtpTimeTicks is a RtpTimeDelta. Likewise, adding or subtracting a
// RtpTimeTicks with a RtpTimeDelta produces an off-set RtpTimeTicks.
//
// The base class provides bit truncation/extension features for
// wire-formatting, and also the comparison operators.
//
// Usage example:
//
// // Time math.
// RtpTimeTicks origin;
// RtpTimeTicks at_one_second =
// origin + RtpTimeDelta::FromTicks(kAudioSamplingRate);
// RtpTimeTicks at_two_seconds =
// at_one_second + RtpTimeDelta::FromTicks(kAudioSamplingRate);
// RtpTimeDelta elasped_in_between = at_two_seconds - at_one_second;
// RtpTimeDelta thrice_as_much_elasped = elasped_in_between * 3;
// RtpTimeTicks at_four_seconds = at_one_second + thrice_as_much_elasped;
//
// // Logging convenience.
// OSP_DLOG_INFO << "The RTP timestamp is " << at_four_seconds;
//
// // Convert (approximately!) between RTP timebase and stream time offsets in
// // microsecond timebase:
// using std::chrono::microseconds;
// microseconds four_seconds_since_stream_start =
// at_four_seconds.ToTimeSinceOrigin<microseconds>(kAudioSamplingRate);
// RtpTimeTicks at_three_seconds = RtpTimeDelta::FromTimeSinceOrigin(
// std::chrono::seconds(3), kAudioSamplingRate);
class RtpTimeTicks : public ExpandedValueBase<int64_t, RtpTimeTicks> {
public:
constexpr explicit RtpTimeTicks(int64_t value) : ExpandedValueBase(value) {}
constexpr RtpTimeTicks() : ExpandedValueBase(0) {}
constexpr int64_t value() const { return value_; }
// Compute the difference between two RtpTimeTickses.
constexpr RtpTimeDelta operator-(RtpTimeTicks rhs) const {
return RtpTimeDelta(value_ - rhs.value_);
}
// Return a new RtpTimeTicks before or after this one.
constexpr RtpTimeTicks operator+(RtpTimeDelta rhs) const {
return RtpTimeTicks(value_ + rhs.value());
}
constexpr RtpTimeTicks operator-(RtpTimeDelta rhs) const {
return RtpTimeTicks(value_ - rhs.value());
}
constexpr RtpTimeTicks& operator+=(RtpTimeDelta rhs) {
return (*this = (*this + rhs));
}
constexpr RtpTimeTicks& operator-=(RtpTimeDelta rhs) {
return (*this = (*this - rhs));
}
// Maps this RtpTimeTicks to an approximate std::chrono::duration representing
// the amount of time since the origin point (e.g., the start of a stream)
// using the given `rtp_timebase`. Assumes a zero-valued Duration corresponds
// to a zero-valued RtpTimeTicks.
template <typename Duration>
Duration ToTimeSinceOrigin(int rtp_timebase) const {
return (*this - RtpTimeTicks()).ToDuration<Duration>(rtp_timebase);
}
// Maps the `time_since_origin` to an approximate RtpTimeTicks using the given
// RTP timebase. Assumes a zero-valued Duration corresponds to a zero-valued
// RtpTimeTicks.
template <typename Duration>
static constexpr RtpTimeTicks FromTimeSinceOrigin(Duration time_since_origin,
int rtp_timebase) {
return RtpTimeTicks() +
RtpTimeDelta::FromDuration(time_since_origin, rtp_timebase);
}
private:
friend class ExpandedValueBase<int64_t, RtpTimeTicks>;
friend std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs);
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_RTP_TIME_H_

View file

@ -0,0 +1,128 @@
// 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/sender_message.h"
#include <utility>
#include <variant>
#include "cast/streaming/message_fields.h"
#include "util/base64.h"
#include "util/enum_name_table.h"
#include "util/json/json_helpers.h"
#include "util/json/json_serialization.h"
#include "util/string_util.h"
namespace openscreen::cast {
namespace {
EnumNameTable<SenderMessage::Type, 4> kMessageTypeNames{
{{kMessageTypeOffer, SenderMessage::Type::kOffer},
{"GET_CAPABILITIES", SenderMessage::Type::kGetCapabilities},
{"RPC", SenderMessage::Type::kRpc},
{"INPUT", SenderMessage::Type::kInput}}};
SenderMessage::Type GetMessageType(const Json::Value& root) {
std::string type;
if (!json::TryParseString(root[kMessageType], &type)) {
return SenderMessage::Type::kUnknown;
}
string_util::AsciiStrToUpper(type);
ErrorOr<SenderMessage::Type> parsed = GetEnum(kMessageTypeNames, type);
return parsed.value(SenderMessage::Type::kUnknown);
}
} // namespace
// static
ErrorOr<SenderMessage> SenderMessage::Parse(const Json::Value& value) {
if (!value.isObject()) {
return Error(Error::Code::kParameterInvalid,
"SenderMessage body is not a JSON object");
}
SenderMessage message;
if (!json::TryParseInt(value[kSequenceNumber], &(message.sequence_number))) {
message.sequence_number = -1;
}
message.type = GetMessageType(value);
switch (message.type) {
case Type::kOffer: {
auto offer_or_error = Offer::TryParse(value[kOfferMessageBody]);
if (offer_or_error.is_value()) {
message.body = std::move(offer_or_error.value());
message.valid = true;
}
} break;
case Type::kRpc: {
std::string rpc_body;
std::vector<uint8_t> rpc;
if (json::TryParseString(value[kRpcMessageBody], &rpc_body) &&
base64::Decode(rpc_body, &rpc)) {
message.body = rpc;
message.valid = true;
}
} break;
case Type::kInput: {
std::string input_body;
std::vector<uint8_t> input;
if (json::TryParseString(value[kInputMessageBody], &input_body) &&
base64::Decode(input_body, &input)) {
message.body = input;
message.valid = true;
}
} break;
case Type::kGetCapabilities:
message.valid = true;
break;
default:
break;
}
return message;
}
ErrorOr<Json::Value> SenderMessage::ToJson() const {
OSP_CHECK(type != SenderMessage::Type::kUnknown)
<< "Trying to send an unknown message is a developer error";
Json::Value root;
ErrorOr<const char*> message_type = GetEnumName(kMessageTypeNames, type);
root[kMessageType] = message_type.value();
if (sequence_number >= 0) {
root[kSequenceNumber] = sequence_number;
}
switch (type) {
case SenderMessage::Type::kOffer:
root[kOfferMessageBody] = std::get<Offer>(body).ToJson();
break;
case SenderMessage::Type::kRpc:
root[kRpcMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
case SenderMessage::Type::kInput:
root[kInputMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
case SenderMessage::Type::kGetCapabilities:
break;
default:
OSP_NOTREACHED();
}
return root;
}
} // namespace openscreen::cast

View file

@ -0,0 +1,55 @@
// 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_SENDER_MESSAGE_H_
#define CAST_STREAMING_SENDER_MESSAGE_H_
#include <memory>
#include <string>
#include <variant>
#include <vector>
#include "cast/streaming/public/offer_messages.h"
#include "json/value.h"
#include "platform/base/error.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
struct SenderMessage {
public:
// Receiver response message type.
enum class Type {
// Unknown message type.
kUnknown,
// OFFER request message.
kOffer,
// GET_CAPABILITIES request message.
kGetCapabilities,
// Rpc binary messages. The payload is base64-encoded.
kRpc,
// Input-related binary messages. The payload is base64-encoded.
kInput,
};
static ErrorOr<SenderMessage> Parse(const Json::Value& value);
ErrorOr<Json::Value> ToJson() const;
Type type = Type::kUnknown;
int32_t sequence_number = -1;
bool valid = false;
std::variant<std::monostate,
std::vector<uint8_t>, // Binary-encoded protobuf message.
Offer,
std::string>
body;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_SENDER_MESSAGE_H_

View file

@ -0,0 +1,273 @@
// 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/sender_packet_router.h"
#include <algorithm>
#include <utility>
#include "cast/streaming/impl/packet_util.h"
#include "cast/streaming/public/constants.h"
#include "platform/base/span.h"
#include "util/chrono_helpers.h"
#include "util/osp_logging.h"
#include "util/saturate_cast.h"
#include "util/stringprintf.h"
namespace openscreen::cast {
using clock_operators::operator<<;
SenderPacketRouter::SenderPacketRouter(Environment& environment,
int max_burst_bitrate)
: SenderPacketRouter(
environment,
ComputeMaxPacketsPerBurst(max_burst_bitrate,
environment.GetMaxPacketSize(),
kDefaultBurstInterval),
kDefaultBurstInterval) {}
SenderPacketRouter::SenderPacketRouter(Environment& environment,
int max_packets_per_burst,
milliseconds burst_interval)
: BandwidthEstimator(max_packets_per_burst,
burst_interval,
environment.now()),
environment_(environment),
packet_buffer_size_(environment.GetMaxPacketSize()),
packet_buffer_(new uint8_t[packet_buffer_size_]),
max_packets_per_burst_(max_packets_per_burst),
burst_interval_(burst_interval),
max_burst_bitrate_(ComputeMaxBurstBitrate(packet_buffer_size_,
max_packets_per_burst_,
burst_interval_)),
alarm_(environment_->now_function(), environment_->task_runner()) {
OSP_CHECK_GT(packet_buffer_size_, kRequiredNetworkPacketSize);
}
SenderPacketRouter::~SenderPacketRouter() {
OSP_CHECK(senders_.empty());
}
void SenderPacketRouter::OnSenderCreated(Ssrc receiver_ssrc, Sender* sender) {
OSP_CHECK(FindEntry(receiver_ssrc) == senders_.end());
senders_.push_back(SenderEntry{receiver_ssrc, sender, kNever, kNever});
if (senders_.size() == 1) {
environment_->ConsumeIncomingPackets(this);
} else {
// Sort the list of Senders so that they are iterated in priority order.
std::sort(senders_.begin(), senders_.end());
}
}
void SenderPacketRouter::OnSenderDestroyed(Ssrc receiver_ssrc) {
const auto it = FindEntry(receiver_ssrc);
OSP_CHECK(it != senders_.end());
senders_.erase(it);
// If there are no longer any Senders, suspend receiving RTCP packets.
if (senders_.empty()) {
environment_->DropIncomingPackets();
}
}
void SenderPacketRouter::RequestRtcpSend(Ssrc receiver_ssrc) {
const auto it = FindEntry(receiver_ssrc);
OSP_CHECK(it != senders_.end());
it->next_rtcp_send_time = Alarm::kImmediately;
ScheduleNextBurst();
}
void SenderPacketRouter::RequestRtpSend(Ssrc receiver_ssrc) {
const auto it = FindEntry(receiver_ssrc);
OSP_CHECK(it != senders_.end());
it->next_rtp_send_time = Alarm::kImmediately;
ScheduleNextBurst();
}
void SenderPacketRouter::OnReceivedPacket(const IPEndpoint& source,
Clock::time_point arrival_time,
std::vector<uint8_t> packet) {
// If the packet did not come from the expected endpoint, ignore it.
OSP_CHECK_NE(source.port, uint16_t{0});
if (source != environment_->remote_endpoint()) {
return;
}
// Determine which Sender to dispatch the packet to. Senders may only receive
// RTCP packets from Receivers. Log a warning containing a pretty-printed dump
// if the packet is not an RTCP packet.
const std::pair<ApparentPacketType, Ssrc> seems_like =
InspectPacketForRouting(packet);
if (seems_like.first != ApparentPacketType::RTCP) {
constexpr int kMaxPartiaHexDumpSize = 96;
const std::size_t encode_size =
std::min(packet.size(), static_cast<size_t>(kMaxPartiaHexDumpSize));
OSP_LOG_WARN << "UNKNOWN packet of " << packet.size()
<< " bytes. Partial hex dump: "
<< HexEncode(packet.data(), encode_size);
return;
}
const auto it = FindEntry(seems_like.second);
if (it != senders_.end()) {
it->sender->OnReceivedRtcpPacket(arrival_time, std::move(packet));
}
}
SenderPacketRouter::SenderEntries::iterator SenderPacketRouter::FindEntry(
Ssrc receiver_ssrc) {
return std::find_if(senders_.begin(), senders_.end(),
[receiver_ssrc](const SenderEntry& entry) {
return entry.receiver_ssrc == receiver_ssrc;
});
}
void SenderPacketRouter::ScheduleNextBurst() {
// Determine the next burst time by scanning for the earliest of the
// next-scheduled send times for each Sender.
const Clock::time_point earliest_allowed_burst_time =
last_burst_time_ + burst_interval_;
Clock::time_point next_burst_time = kNever;
for (const SenderEntry& entry : senders_) {
const auto next_send_time =
std::min(entry.next_rtcp_send_time, entry.next_rtp_send_time);
if (next_send_time >= next_burst_time) {
continue;
}
if (next_send_time <= earliest_allowed_burst_time) {
next_burst_time = earliest_allowed_burst_time;
// No need to continue, since `next_burst_time` cannot become any earlier.
break;
}
next_burst_time = next_send_time;
}
// Schedule the alarm for the next burst time unless none of the Senders has
// anything to send.
if (next_burst_time == kNever) {
alarm_.Cancel();
} else {
alarm_.Schedule([this] { SendBurstOfPackets(); }, next_burst_time);
}
}
void SenderPacketRouter::SendBurstOfPackets() {
// Treat RTCP packets as "critical priority," and so there is no upper limit
// on the number to send. Practically, this will always be limited by the
// number of Senders; so, this won't be a huge number of packets.
const Clock::time_point burst_time = environment_->now();
const int num_rtcp_packets_sent = SendJustTheRtcpPackets(burst_time);
// Now send all the RTP packets, up to the maximum number allowed in a burst.
// Higher priority Senders' RTP packets are sent first.
const int num_rtp_packets_sent = SendJustTheRtpPackets(
burst_time, max_packets_per_burst_ - num_rtcp_packets_sent);
last_burst_time_ = burst_time;
BandwidthEstimator::OnBurstComplete(
num_rtcp_packets_sent + num_rtp_packets_sent, burst_time);
ScheduleNextBurst();
}
int SenderPacketRouter::SendJustTheRtcpPackets(Clock::time_point send_time) {
int num_sent = 0;
for (SenderEntry& entry : senders_) {
if (entry.next_rtcp_send_time > send_time) {
continue;
}
// Note: Only one RTCP packet is sent from the same Sender in the same
// burst. This is because RTCP packets are supposed to always contain the
// most up-to-date Sender state. Having multiple RTCP packets in the same
// burst would mean that all but the last one are old/irrelevant snapshots
// of Sender state, and this would just thrash/confuse the Receiver.
const ByteBuffer packet = entry.sender->GetRtcpPacketForImmediateSend(
send_time, ByteBuffer(packet_buffer_.get(), packet_buffer_size_));
if (!packet.empty()) {
environment_->SendPacket(
ByteView(packet.data(), packet.size()),
PacketMetadata{.stream_type = entry.sender->GetStreamType(),
.rtp_timestamp = entry.sender->GetLastRtpTimestamp()});
entry.next_rtcp_send_time = send_time + kRtcpReportInterval;
++num_sent;
}
}
return num_sent;
}
int SenderPacketRouter::SendJustTheRtpPackets(Clock::time_point send_time,
int num_packets_to_send) {
int num_sent = 0;
for (SenderEntry& entry : senders_) {
if (num_sent >= num_packets_to_send) {
break;
}
if (entry.next_rtp_send_time > send_time) {
continue;
}
for (; num_sent < num_packets_to_send; ++num_sent) {
const ByteBuffer packet = entry.sender->GetRtpPacketForImmediateSend(
send_time, ByteBuffer(packet_buffer_.get(), packet_buffer_size_));
if (packet.empty()) {
break;
}
environment_->SendPacket(
ByteView(packet.data(), packet.size()),
PacketMetadata{.stream_type = entry.sender->GetStreamType(),
.rtp_timestamp = entry.sender->GetLastRtpTimestamp()});
}
entry.next_rtp_send_time = entry.sender->GetRtpResumeTime();
}
return num_sent;
}
namespace {
constexpr int kBitsPerByte = 8;
constexpr auto kOneSecondInMilliseconds = to_milliseconds(seconds(1));
} // namespace
// static
int SenderPacketRouter::ComputeMaxPacketsPerBurst(int max_burst_bitrate,
int packet_size,
milliseconds burst_interval) {
OSP_CHECK_GT(max_burst_bitrate, 0);
OSP_CHECK_GT(packet_size, 0);
OSP_CHECK_GT(burst_interval, milliseconds(0));
OSP_CHECK_LE(burst_interval, kOneSecondInMilliseconds);
const int max_packets_per_second =
max_burst_bitrate / kBitsPerByte / packet_size;
const int bursts_per_second = kOneSecondInMilliseconds / burst_interval;
return std::max(max_packets_per_second / bursts_per_second, 1);
}
// static
int SenderPacketRouter::ComputeMaxBurstBitrate(int packet_size,
int max_packets_per_burst,
milliseconds burst_interval) {
OSP_CHECK_GT(packet_size, 0);
OSP_CHECK_GT(max_packets_per_burst, 0);
OSP_CHECK_GT(burst_interval, milliseconds(0));
OSP_CHECK_LE(burst_interval, kOneSecondInMilliseconds);
const int64_t max_bits_per_burst =
int64_t{packet_size} * kBitsPerByte * max_packets_per_burst;
const int bursts_per_second = kOneSecondInMilliseconds / burst_interval;
return saturate_cast<int>(max_bits_per_burst * bursts_per_second);
}
SenderPacketRouter::Sender::~Sender() = default;
// static
constexpr int SenderPacketRouter::kDefaultMaxBurstBitrate;
// static
constexpr milliseconds SenderPacketRouter::kDefaultBurstInterval;
// static
constexpr Clock::time_point SenderPacketRouter::kNever;
} // namespace openscreen::cast

View file

@ -0,0 +1,203 @@
// 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_SENDER_PACKET_ROUTER_H_
#define CAST_STREAMING_SENDER_PACKET_ROUTER_H_
#include <stdint.h>
#include <chrono>
#include <memory>
#include <vector>
#include "cast/streaming/impl/bandwidth_estimator.h"
#include "cast/streaming/public/constants.h"
#include "cast/streaming/public/environment.h"
#include "cast/streaming/ssrc.h"
#include "platform/api/time.h"
#include "platform/base/span.h"
#include "util/alarm.h"
#include "util/raw_ptr.h"
#include "util/raw_ref.h"
namespace openscreen::cast {
// Manages network packet transmission for one or more Senders, directing each
// inbound packet to a specific Sender instance, pacing the transmission of
// outbound packets, and employing network bandwidth/availability monitoring and
// congestion control.
//
// Instead of just sending packets whenever they want, Senders must request
// transmission from the SenderPacketRouter. The router then calls-back to each
// Sender, in the near future, when it has allocated an available time slice for
// transmission. The Sender is allowed to decide, at that exact moment, which
// packet most needs to be sent.
//
// Pacing strategy: Packets are sent in bursts. This allows the platform
// (operating system) to collect many small packets into a short-term buffer,
// which allows for optimizations at the link layer. For example, multiple
// packets can be sent together as one larger transmission unit, and this can be
// critical for good performance over shared-medium networks (such as 802.11
// WiFi). https://en.wikipedia.org/wiki/Frame-bursting
class SenderPacketRouter : public BandwidthEstimator,
public Environment::PacketConsumer {
public:
class Sender {
public:
// Called to provide the Sender with what looks like a RTCP packet meant for
// it specifically (among other Senders) to process. `arrival_time`
// indicates when the packet arrived (i.e., when it was received from the
// platform).
virtual void OnReceivedRtcpPacket(Clock::time_point arrival_time,
ByteView packet) = 0;
// Populates the given `buffer` with a RTCP/RTP packet that will be sent
// immediately. Returns the portion of `buffer` contaning the packet, or an
// empty Span if nothing is ready to send.
virtual ByteBuffer GetRtcpPacketForImmediateSend(
Clock::time_point send_time,
ByteBuffer buffer) = 0;
virtual ByteBuffer GetRtpPacketForImmediateSend(Clock::time_point send_time,
ByteBuffer buffer) = 0;
// Returns the point-in-time at which RTP sending should resume, or kNever
// if it should be suspended until an explicit call to RequestRtpSend(). The
// implementation may return a value on or before "now" to indicate an
// immediate resume is desired.
virtual Clock::time_point GetRtpResumeTime() = 0;
// Returns the last logged RTP timestamp, for use in expanding truncated
// packet RTP timestamps for metrics purposes.
virtual RtpTimeTicks GetLastRtpTimestamp() const = 0;
// Returns the type of stream that this sender is providing.
virtual StreamType GetStreamType() const = 0;
protected:
virtual ~Sender();
};
// Constructs an instance with default burst parameters appropriate for the
// given `max_burst_bitrate`.
explicit SenderPacketRouter(Environment& environment,
int max_burst_bitrate = kDefaultMaxBurstBitrate);
// Constructs an instance with specific burst parameters. The maximum bitrate
// will be computed based on these (and Environment::GetMaxPacketSize()).
SenderPacketRouter(Environment& environment,
int max_packets_per_burst,
std::chrono::milliseconds burst_interval);
~SenderPacketRouter();
int max_packet_size() const { return packet_buffer_size_; }
int max_burst_bitrate() const { return max_burst_bitrate_; }
// Called from a Sender constructor/destructor to register/deregister a Sender
// instance that processes RTP/RTCP packets from a Receiver having the given
// SSRC.
void OnSenderCreated(Ssrc receiver_ssrc, Sender* client);
void OnSenderDestroyed(Ssrc receiver_ssrc);
// Requests an immediate send of a RTCP packet, and then RTCP sending will
// repeat at regular intervals (see kRtcpSendInterval) until the Sender is
// de-registered.
void RequestRtcpSend(Ssrc receiver_ssrc);
// Requests an immediate send of a RTP packet. RTP sending will continue until
// the Sender stops providing packet data.
//
// See also: Sender::GetRtpResumeTime().
void RequestRtpSend(Ssrc receiver_ssrc);
// A reasonable default maximum bitrate for bursting. Congestion control
// should always be employed to limit the Senders' sustained/average outbound
// data volume for "fair" use of the network.
static constexpr int kDefaultMaxBurstBitrate = 24 << 20; // 24 megabits/sec
// The minimum amount of time between burst-sends. The methodology by which
// this value was determined is lost knowledge, but is likely the result of
// experimentation with various network and operating system configurations.
// This value came from the original Chrome Cast Streaming implementation.
static constexpr std::chrono::milliseconds kDefaultBurstInterval{10};
// A special time_point value representing "never."
static constexpr Clock::time_point kNever = Clock::time_point::max();
private:
struct SenderEntry {
Ssrc receiver_ssrc;
raw_ptr<Sender> sender;
Clock::time_point next_rtcp_send_time;
Clock::time_point next_rtp_send_time;
// Entries are ordered by the transmission priority (high→low), as implied
// by their SSRC. See ssrc.h for details.
bool operator<(const SenderEntry& other) const {
return ComparePriority(receiver_ssrc, other.receiver_ssrc) < 0;
}
};
using SenderEntries = std::vector<SenderEntry>;
// Environment::PacketConsumer implementation.
void OnReceivedPacket(const IPEndpoint& source,
Clock::time_point arrival_time,
std::vector<uint8_t> packet) final;
// Helper to return an iterator pointing to the entry corresponding to the
// given `receiver_ssrc`, or "end" if not found.
SenderEntries::iterator FindEntry(Ssrc receiver_ssrc);
// Examine the next send time for all Senders, and decide whether to schedule
// a burst-send.
void ScheduleNextBurst();
// Performs a burst-send of packets. This is called whenever the Alarm fires.
void SendBurstOfPackets();
// Send an RTCP packet from each Sender that has one ready, and return the
// number of packets sent.
int SendJustTheRtcpPackets(Clock::time_point send_time);
// Send zero or more RTP packets from each Sender, up to a maximum of
// `num_packets_to_send`, and return the number of packets sent.
int SendJustTheRtpPackets(Clock::time_point send_time,
int num_packets_to_send);
// Returns the maximum number of packets to send in one burst, based on the
// given parameters.
static int ComputeMaxPacketsPerBurst(
int max_burst_bitrate,
int packet_size,
std::chrono::milliseconds burst_interval);
// Returns the maximum bitrate inferred by the given parameters.
static int ComputeMaxBurstBitrate(int packet_size,
int max_packets_per_burst,
std::chrono::milliseconds burst_interval);
const raw_ref<Environment> environment_;
const int packet_buffer_size_;
const std::unique_ptr<uint8_t[]> packet_buffer_;
const int max_packets_per_burst_;
const std::chrono::milliseconds burst_interval_;
const int max_burst_bitrate_;
// Schedules the task that calls back into this SenderPacketRouter at a later
// time to send the next burst of packets.
Alarm alarm_;
// The current list of Senders and their timing information. This is
// maintained in order of the priority implied by the Sender SSRC's.
SenderEntries senders_;
// The last time a burst of packets was sent. This is used to determine the
// next burst time.
Clock::time_point last_burst_time_ = Clock::time_point::min();
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_SENDER_PACKET_ROUTER_H_

View file

@ -0,0 +1,42 @@
// 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/ssrc.h"
#include <random>
#include "platform/api/time.h"
namespace openscreen::cast {
namespace {
// These ranges are arbitrary, but have been used for several years (in prior
// implementations of Cast Streaming).
constexpr int kHigherPriorityMin = 1;
constexpr int kHigherPriorityMax = 50000;
constexpr int kNormalPriorityMin = 50001;
constexpr int kNormalPriorityMax = 100000;
} // namespace
Ssrc GenerateSsrc(bool higher_priority) {
// 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()));
std::uniform_int_distribution<int> distribution(
higher_priority ? kHigherPriorityMin : kNormalPriorityMin,
higher_priority ? kHigherPriorityMax : kNormalPriorityMax);
return static_cast<Ssrc>(distribution(generator));
}
int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b) {
return static_cast<int>(ssrc_a) - static_cast<int>(ssrc_b);
}
} // namespace openscreen::cast

View file

@ -0,0 +1,37 @@
// 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_SSRC_H_
#define CAST_STREAMING_SSRC_H_
#include <stdint.h>
namespace openscreen::cast {
// A Synchronization Source is a 32-bit opaque identifier used in RTP packets
// for identifying the source (or recipient) of a logical sequence of encoded
// audio/video frames. In other words, an audio stream will have one sender SSRC
// and a video stream will have a different sender SSRC.
using Ssrc = uint32_t;
// The "not set" or "null" value for the Ssrc type.
inline constexpr Ssrc kNullSsrc = 0;
// Computes a new SSRC that will be used to uniquely identify an RTP stream. The
// `higher_priority` argument, if true, will generate an SSRC that causes the
// system to use a higher priority when scheduling data transmission. Generally,
// this is set to true for audio streams and false for video streams.
Ssrc GenerateSsrc(bool higher_priority);
// Returns a value indicating how to prioritize data transmission for a stream
// with `ssrc_a` versus a stream with `ssrc_b`:
//
// ret < 0: Stream `ssrc_a` has higher priority.
// ret == 0: Equal priority.
// ret > 0: Stream `ssrc_b` has higher priority.
int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b);
} // namespace openscreen::cast
#endif // CAST_STREAMING_SSRC_H_

View file

@ -0,0 +1,26 @@
#include "patches/compat_shims.h"
void AES_ctr128_encrypt(const unsigned char* in,
unsigned char* out,
size_t length,
const AES_KEY* key,
unsigned char ivec[AES_BLOCK_SIZE],
unsigned char ecount_buf[AES_BLOCK_SIZE],
unsigned int* num) {
unsigned int n = *num;
size_t l = 0;
while (l < length) {
if (n == 0) {
AES_encrypt(ivec, ecount_buf, key);
for (int i = AES_BLOCK_SIZE - 1; i >= 0; --i) {
if (++ivec[i]) {
break;
}
}
}
out[l] = static_cast<unsigned char>(in[l] ^ ecount_buf[n]);
++l;
n = (n + 1) % AES_BLOCK_SIZE;
}
*num = n;
}

View file

@ -0,0 +1,35 @@
// Force-included (via -include, see ../build.rs) into every translation
// unit in this vendored openscreen subset. Papers over a few places where
// upstream code either relies on a BoringSSL-only API that system OpenSSL
// doesn't expose, or was written assuming an include that some other header
// in a full Chromium checkout happens to pull in transitively. Not part of
// upstream openscreen -- see ../PATCHES.md.
#ifndef PATCHES_COMPAT_SHIMS_H_
#define PATCHES_COMPAT_SHIMS_H_
// cast/streaming/impl/frame_crypto.cc uses strlen-family functions without
// including <cstring> itself.
#include <cstring>
#include <openssl/aes.h>
// CRYPTO_library_init() was an OpenSSL 1.0.x-era macro/no-op that BoringSSL
// still defines for source compatibility; system OpenSSL 3.x has no such
// symbol (initialization there is automatic). frame_crypto.cc's call to it
// is a no-op on any OpenSSL version this new, so it's shimmed out entirely.
#define CRYPTO_library_init() ((void)0)
// AES_ctr128_encrypt() is a BoringSSL convenience wrapper around AES-CTR
// that system OpenSSL's public headers don't expose. Implemented in
// compat_shims.cc using the standard CTR-mode algorithm (NIST SP 800-38A,
// big-endian 128-bit counter block) over AES_encrypt(), which OpenSSL does
// still expose.
extern "C" void AES_ctr128_encrypt(const unsigned char* in,
unsigned char* out,
size_t length,
const AES_KEY* key,
unsigned char ivec[AES_BLOCK_SIZE],
unsigned char ecount_buf[AES_BLOCK_SIZE],
unsigned int* num);
#endif // PATCHES_COMPAT_SHIMS_H_

View file

@ -0,0 +1,54 @@
// 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 PLATFORM_API_CONNECTION_H_
#define PLATFORM_API_CONNECTION_H_
#include <cstdint>
#include <vector>
#include "platform/base/error.h"
#include "platform/base/ip_address.h"
#include "platform/base/span.h"
namespace openscreen {
// Represents a connection between two endpoints. This class provides an
// interface for sending and receiving byte data over a connection.
class Connection {
public:
// Client callbacks are run via the TaskRunner used by TlsConnectionFactory.
class Client {
public:
// Called when `connection` experiences an error, such as a read error.
virtual void OnError(Connection* connection, const Error& error) = 0;
// Called when a `block` arrives on `connection`.
virtual void OnRead(Connection* connection, std::vector<uint8_t> block) = 0;
protected:
virtual ~Client() = default;
};
virtual ~Connection() = default;
// Sets the Client associated with this instance. This should be called as
// soon as the factory provides a new Connection instance via
// TlsConnectionFactory::OnAccepted(), OnConnected() or CreateSocket().
// Pass nullptr to unset the Client.
virtual void SetClient(Client* client) = 0;
// Sends a message. Returns true iff the message will be sent.
[[nodiscard]] virtual bool Send(ByteView data) = 0;
// Get the connected remote address.
virtual IPEndpoint GetRemoteEndpoint() const = 0;
protected:
Connection() = default;
};
} // namespace openscreen
#endif // PLATFORM_API_CONNECTION_H_

View file

@ -0,0 +1,26 @@
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PLATFORM_API_EXPORT_H_
#define PLATFORM_API_EXPORT_H_
#if defined(WIN32)
#if defined(OPENSCREEN_SHARED_IMPLEMENTATION)
#define OPENSCREEN_EXPORT __declspec(dllexport)
#else
#define OPENSCREEN_EXPORT __declspec(dllimport)
#endif // defined(OPENSCREEN_SHARED_IMPLEMENTATION)
#else
#if defined(OPENSCREEN_SHARED_IMPLEMENTATION)
#define OPENSCREEN_EXPORT __attribute__((visibility("default")))
#else
#define OPENSCREEN_EXPORT
#endif // defined(OPENSCREEN_SHARED_IMPLEMENTATION)
#endif // defined(WIN32)
#endif // PLATFORM_API_EXPORT_H_

View file

@ -0,0 +1,62 @@
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PLATFORM_API_LOGGING_H_
#define PLATFORM_API_LOGGING_H_
#include <sstream>
namespace openscreen {
enum class LogLevel {
// Very detailed information, often used for evaluating performance or
// debugging production issues in-the-wild.
kVerbose = 0,
// Used occasionally to note events of interest, but not for indicating any
// problems. This is also used for general console messaging in Open Screen's
// standalone executables.
kInfo = 1,
// Indicates a problem that may or may not lead to an operational failure.
kWarning = 2,
// Indicates an operational failure that may or may not cause a component to
// stop working.
kError = 3,
// Indicates a logic flaw, corruption, impossible/unanticipated situation, or
// operational failure so serious that Open Screen will soon call Break() to
// abort the current process. Examples: security/privacy risks, memory
// management issues, API contract violations.
kFatal = 4,
};
// Returns true if `level` is at or above the level where the embedder will
// record/emit log entries from the code in `file`.
bool IsLoggingOn(LogLevel level, const std::string_view file);
// Record a log entry, consisting of its logging level, location and message.
// The embedder may filter-out entries according to its own policy, but this
// function will not be called if IsLoggingOn(level, file) returns false.
// Whenever `level` is kFatal, Open Screen will call Break() immediately after
// this returns.
//
// `message` is passed as a string stream to avoid unnecessary string copies.
// Embedders can call its rdbuf() or str() methods to access the log message.
void LogWithLevel(LogLevel level,
const char* file,
int line,
std::stringstream message);
// Breaks into the debugger, if one is present. Otherwise, aborts the current
// process (i.e., this function should not return). In production builds, an
// embedder could invoke its infrastructure for performing "dumps," consisting
// of thread stack traces and other relevant process state information, before
// aborting the process.
[[noreturn]] void Break();
} // namespace openscreen
#endif // PLATFORM_API_LOGGING_H_

View file

@ -0,0 +1,24 @@
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PLATFORM_API_NETWORK_INTERFACE_H_
#define PLATFORM_API_NETWORK_INTERFACE_H_
#include <vector>
#include "platform/base/interface_info.h"
namespace openscreen {
// Returns an InterfaceInfo for each currently active network interface on the
// system. No two entries in this vector can have the same NetworkInterfaceIndex
// value.
//
// This can return an empty vector if there are no active network interfaces or
// an error occurred querying the system for them.
std::vector<InterfaceInfo> GetNetworkInterfaces();
} // namespace openscreen
#endif // PLATFORM_API_NETWORK_INTERFACE_H_

View file

@ -0,0 +1,64 @@
// 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 PLATFORM_API_TASK_RUNNER_H_
#define PLATFORM_API_TASK_RUNNER_H_
#include <future>
#include <utility>
#include "platform/api/time.h"
namespace openscreen {
// A thread-safe API surface that allows for posting tasks. The underlying
// implementation may be single or multi-threaded, and all complication should
// be handled by the implementation class. The implementation must guarantee:
// (1) Tasks shall not overlap in time/CPU.
// (2) Tasks shall run sequentially, e.g. posting task A then B implies
// that A shall run before B.
// (3) If task A is posted before task B, then any mutation in A happens-before
// B runs (even if A and B run on different threads).
class TaskRunner {
public:
using Task = std::packaged_task<void()>;
virtual ~TaskRunner() = default;
// Takes any callable target (function, lambda-expression, std::bind result,
// etc.) that should be run at the first convenient time.
template <typename Functor>
inline void PostTask(Functor f) {
PostPackagedTask(Task(std::move(f)));
}
// Takes any callable target (function, lambda-expression, std::bind result,
// etc.) that should be run no sooner than `delay` time from now. Note that
// the Task might run after an additional delay, especially under heavier
// system load. There is no deadline concept.
template <typename Functor>
inline void PostTaskWithDelay(Functor f, Clock::duration delay) {
PostPackagedTaskWithDelay(Task(std::move(f)), delay);
}
// Implementations should provide the behavior explained in the comments above
// for PostTask[WithDelay](). Client code may also call these directly when
// passing an existing Task object.
virtual void PostPackagedTask(Task task) = 0;
virtual void PostPackagedTaskWithDelay(Task task, Clock::duration delay) = 0;
// Return true if the calling thread is the thread that task runner is using
// to run tasks, false otherwise.
virtual bool IsRunningOnTaskRunner() = 0;
// Posts a task to delete `object`.
template <class T>
void DeleteSoon(const T* object) {
PostTask([object] { delete static_cast<const T*>(object); });
}
};
} // namespace openscreen
#endif // PLATFORM_API_TASK_RUNNER_H_

View file

@ -0,0 +1,23 @@
// 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 "platform/api/task_runner_deleter.h"
namespace openscreen {
TaskRunnerDeleter::TaskRunnerDeleter() = default;
TaskRunnerDeleter::TaskRunnerDeleter(TaskRunner& task_runner)
: task_runner_(&task_runner) {}
TaskRunnerDeleter::~TaskRunnerDeleter() = default;
TaskRunnerDeleter::TaskRunnerDeleter(const TaskRunnerDeleter&) = default;
TaskRunnerDeleter& TaskRunnerDeleter::operator=(const TaskRunnerDeleter&) =
default;
TaskRunnerDeleter::TaskRunnerDeleter(TaskRunnerDeleter&&) noexcept = default;
TaskRunnerDeleter& TaskRunnerDeleter::operator=(TaskRunnerDeleter&&) noexcept =
default;
} // namespace openscreen

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 PLATFORM_API_TASK_RUNNER_DELETER_H_
#define PLATFORM_API_TASK_RUNNER_DELETER_H_
#include <memory>
#include <utility>
#include "platform/api/task_runner.h"
namespace openscreen {
// Helper that deletes an object on the provided TaskRunner.
//
// Usage with std::unique_ptr:
//
// std::unique_ptr<Foo, TaskRunnerDeleter> some_foo;
// ...
// some_foo = TaskRunnerDeleter::MakeUnique(
// task_runner, foo_arg1, foo_arg2, ...);
struct TaskRunnerDeleter {
TaskRunnerDeleter();
explicit TaskRunnerDeleter(TaskRunner& task_runner);
~TaskRunnerDeleter();
TaskRunnerDeleter(const TaskRunnerDeleter&);
TaskRunnerDeleter& operator=(const TaskRunnerDeleter&);
TaskRunnerDeleter(TaskRunnerDeleter&&) noexcept;
TaskRunnerDeleter& operator=(TaskRunnerDeleter&&) noexcept;
// For compatibility with std:: deleters.
template <typename T>
void operator()(const T* ptr) {
if (task_runner_ && ptr)
task_runner_->DeleteSoon(ptr);
}
template <typename Type, typename Deleter = TaskRunnerDeleter>
static std::unique_ptr<Type, Deleter> WrapUnique(TaskRunner& task_runner,
Type* t) {
return std::unique_ptr<Type, Deleter>(t, TaskRunnerDeleter(task_runner));
}
template <typename Type,
typename Deleter = TaskRunnerDeleter,
typename... Args>
static std::unique_ptr<Type, Deleter> MakeUnique(TaskRunner& task_runner,
Args&&... args) {
return std::unique_ptr<Type, Deleter>(
new Type(std::forward<Args>(args)...),
TaskRunnerDeleter(task_runner)); // NOLINT
}
#if defined(__clang__)
[[clang::annotate("raw_ptr_exclusion")]]
#endif
TaskRunner* task_runner_ = nullptr;
};
} // namespace openscreen
#endif // PLATFORM_API_TASK_RUNNER_DELETER_H_

View file

@ -0,0 +1,37 @@
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PLATFORM_API_TIME_H_
#define PLATFORM_API_TIME_H_
#include <chrono>
#include "platform/base/trivial_clock_traits.h"
namespace openscreen {
// The "reasonably high-resolution" source of monotonic time from the embedder,
// exhibiting the traits described in TrivialClockTraits. This class is not
// instantiated. It only contains a static now() function.
//
// For example, the default platform implementation bases this on
// std::chrono::steady_clock or std::chrono::high_resolution_clock, but an
// embedder may choose to use a different source of time (e.g., the embedder's
// time library, a simulated time source, or a mock).
class Clock : public TrivialClockTraits {
public:
// Returns the current time.
static time_point now() noexcept;
};
// Returns the number of seconds since UNIX epoch (1 Jan 1970, midnight)
// according to the wall clock, which is subject to adjustments (e.g., via NTP).
// Note that this is NOT necessarily the same time source as Clock::now() above,
// and is NOT guaranteed to be monotonically non-decreasing; it is "calendar
// time."
std::chrono::seconds GetWallTimeSinceUnixEpoch() noexcept;
} // namespace openscreen
#endif // PLATFORM_API_TIME_H_

View file

@ -0,0 +1,12 @@
// 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 "platform/api/tls_connection.h"
namespace openscreen {
TlsConnection::TlsConnection() = default;
} // namespace openscreen

View file

@ -0,0 +1,29 @@
// 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 PLATFORM_API_TLS_CONNECTION_H_
#define PLATFORM_API_TLS_CONNECTION_H_
#include <cstdint>
#include <vector>
#include "platform/api/connection.h"
#include "platform/base/error.h"
#include "platform/base/ip_address.h"
#include "platform/base/span.h"
namespace openscreen {
class TlsConnection : public Connection {
public:
// Get the connected remote address.
virtual IPEndpoint GetRemoteEndpoint() const = 0;
protected:
TlsConnection();
};
} // namespace openscreen
#endif // PLATFORM_API_TLS_CONNECTION_H_

View file

@ -0,0 +1,14 @@
// 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 "platform/api/tls_connection_factory.h"
namespace openscreen {
TlsConnectionFactory::TlsConnectionFactory() = default;
TlsConnectionFactory::~TlsConnectionFactory() = default;
TlsConnectionFactory::Client::~Client() = default;
} // namespace openscreen

View file

@ -0,0 +1,82 @@
// 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 PLATFORM_API_TLS_CONNECTION_FACTORY_H_
#define PLATFORM_API_TLS_CONNECTION_FACTORY_H_
#include <stdint.h>
#include <memory>
#include <vector>
#include "platform/base/ip_address.h"
namespace openscreen {
class TaskRunner;
class TlsConnection;
struct TlsConnectOptions;
struct TlsCredentials;
struct TlsListenOptions;
// We expect a single factory to be able to handle an arbitrary number of
// calls using the same client and task runner.
class TlsConnectionFactory {
public:
// Client callbacks are ran on the provided TaskRunner.
class Client {
public:
// Provides a new `connection` that resulted from listening on the local
// socket. `der_x509_peer_cert` is the DER-encoded X509 certificate from the
// peer if present, or empty if the peer didn't provide one.
virtual void OnAccepted(TlsConnectionFactory* factory,
std::vector<uint8_t> der_x509_peer_cert,
std::unique_ptr<TlsConnection> connection) = 0;
// Provides a new `connection` that resulted from connecting to a remote
// endpoint. `der_x509_peer_cert` is the DER-encoded X509 certificate from
// the peer.
virtual void OnConnected(TlsConnectionFactory* factory,
std::vector<uint8_t> der_x509_peer_cert,
std::unique_ptr<TlsConnection> connection) = 0;
virtual void OnConnectionFailed(TlsConnectionFactory* factory,
const IPEndpoint& remote_address) = 0;
// Called when a non-recoverable error occurs.
virtual void OnError(TlsConnectionFactory* factory, const Error& error) = 0;
protected:
virtual ~Client();
};
// The connection factory requires a client for yielding creation results
// asynchronously, as well as a task runner it can use to for running
// callbacks both on the factory and on created TlsConnection instances.
static std::unique_ptr<TlsConnectionFactory> CreateFactory(
Client& client,
TaskRunner& task_runner);
virtual ~TlsConnectionFactory();
// Fires an OnConnected or OnConnectionFailed event.
virtual void Connect(const IPEndpoint& remote_address,
const TlsConnectOptions& options) = 0;
// Set the TlsCredentials used for listening for new connections. Currently,
// having different certificates on different address is not supported. This
// must be called before the first call to Listen.
virtual void SetListenCredentials(const TlsCredentials& credentials) = 0;
// Fires an OnAccepted or OnConnectionFailed event.
virtual void Listen(const IPEndpoint& local_address,
const TlsListenOptions& options) = 0;
protected:
TlsConnectionFactory();
};
} // namespace openscreen
#endif // PLATFORM_API_TLS_CONNECTION_FACTORY_H_

View file

@ -0,0 +1,61 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "platform/api/trace_event.h"
#include <sstream>
namespace openscreen {
TraceEvent::TraceEvent(TraceCategory category,
Clock::time_point start_time,
const char* name,
const char* file_name,
uint32_t line_number)
: category(category),
start_time(start_time),
name(name),
file_name(file_name),
line_number(line_number) {}
TraceEvent::TraceEvent() = default;
TraceEvent::TraceEvent(TraceEvent&&) noexcept = default;
TraceEvent::TraceEvent(const TraceEvent&) = default;
TraceEvent& TraceEvent::operator=(TraceEvent&&) = default;
TraceEvent& TraceEvent::operator=(const TraceEvent&) = default;
TraceEvent::~TraceEvent() = default;
std::string TraceEvent::ToString() const {
std::ostringstream oss;
oss << ids << " " << openscreen::ToString(category) << "::" << name << " <"
<< file_name << ":" << line_number << ">";
// We only support two arguments in total.
if (!arguments.empty()) {
oss << " { " << arguments[0].first << ": " << arguments[0].second;
if (arguments.size() > 1) {
oss << ", " << arguments[1].first << ": " << arguments[1].second;
}
oss << " }";
}
return oss.str();
}
void TraceEvent::TruncateStrings() {
for (auto& argument : arguments) {
if (argument.second.size() > kMaxStringLength) {
argument.second.resize(kMaxStringLength);
// Populate last three digits with ellipses to indicate that
// we truncated this string.
argument.second.replace(kMaxStringLength - 3, 3, "...");
}
}
}
std::ostream& operator<<(std::ostream& out, const TraceEvent& event) {
return out << event.ToString();
}
} // namespace openscreen

View file

@ -0,0 +1,75 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PLATFORM_API_TRACE_EVENT_H_
#define PLATFORM_API_TRACE_EVENT_H_
#include <string>
#include <utility>
#include <vector>
#include "platform/api/time.h"
#include "platform/base/error.h"
#include "platform/base/trace_logging_activation.h"
#include "platform/base/trace_logging_types.h"
namespace openscreen {
// A collection of common properties of trace events.
struct TraceEvent {
// Constructor with only the required fields.
TraceEvent(TraceCategory category,
Clock::time_point start_time,
const char* name,
const char* file_name,
uint32_t line_number);
TraceEvent();
TraceEvent(TraceEvent&&) noexcept;
TraceEvent(const TraceEvent&);
TraceEvent& operator=(TraceEvent&&);
TraceEvent& operator=(const TraceEvent&);
~TraceEvent();
std::string ToString() const;
// May be called to truncate all std::strings on this object.
static const size_t kMaxStringLength = 1024;
void TruncateStrings();
// The category of this event.
TraceCategory category;
// Timestamp for when the event was created.
Clock::time_point start_time;
// Name of this operation.
const char* name = nullptr;
// Name of the file the log was generated in.
const char* file_name = nullptr;
// Line number the log was generated on.
uint32_t line_number = 0;
// The trace ids of this event and its ancestors.
TraceIdHierarchy ids;
// Flow IDs associated with this event.
std::vector<uint64_t> flow_ids;
// Optional result of the trace event.
Error::Code result = Error::Code::kNone;
// Optional list of arguments. May contain 0, 1, or 2 arguments.
// Excess arguments will remain unused.
using Argument = std::pair<const char*, std::string>;
std::vector<Argument> arguments;
};
std::ostream& operator<<(std::ostream& out, const TraceEvent& event);
} // namespace openscreen
#endif // PLATFORM_API_TRACE_EVENT_H_

View file

@ -0,0 +1,13 @@
// 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 "platform/api/trace_logging_platform.h"
namespace openscreen {
TraceLoggingPlatform::~TraceLoggingPlatform() = default;
void TraceLoggingPlatform::LogFlow(TraceEvent event, FlowType type) {}
} // namespace openscreen

View file

@ -0,0 +1,52 @@
// 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 PLATFORM_API_TRACE_LOGGING_PLATFORM_H_
#define PLATFORM_API_TRACE_LOGGING_PLATFORM_H_
#include <string>
#include <utility>
#include <vector>
#include "platform/api/time.h"
#include "platform/api/trace_event.h"
#include "platform/base/error.h"
#include "platform/base/trace_logging_activation.h"
#include "platform/base/trace_logging_types.h"
namespace openscreen {
// Optional platform API to support logging trace events from Open Screen. To
// use this, implement the TraceLoggingPlatform interface and call
// StartTracing() and StopTracing() to turn tracing on/off (see
// platform/base/trace_logging_activation.h).
//
// All methods must be thread-safe and re-entrant.
class TraceLoggingPlatform {
public:
virtual ~TraceLoggingPlatform();
// Determines whether trace logging is enabled for the given category. Note
// that if any categories are supported, this function should return "true"
// when called with TraceCategory::kAny.
virtual bool IsTraceLoggingEnabled(TraceCategory category) = 0;
// Log a synchronous trace.
virtual void LogTrace(TraceEvent event, Clock::time_point end_time) = 0;
// Log an asynchronous trace start.
virtual void LogAsyncStart(TraceEvent event) = 0;
// Log an asynchronous trace end.
virtual void LogAsyncEnd(TraceEvent event) = 0;
// Log a flow event.
// TODO(crbug.com/479316209): fast-follow: make non-optional once implemented
// in Chromium.
virtual void LogFlow(TraceEvent event, FlowType type);
};
} // namespace openscreen
#endif // PLATFORM_API_TRACE_LOGGING_PLATFORM_H_

View file

@ -0,0 +1,14 @@
// 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 "platform/api/udp_socket.h"
namespace openscreen {
UdpSocket::UdpSocket() = default;
UdpSocket::~UdpSocket() = default;
UdpSocket::Client::~Client() = default;
} // namespace openscreen

View file

@ -0,0 +1,140 @@
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef PLATFORM_API_UDP_SOCKET_H_
#define PLATFORM_API_UDP_SOCKET_H_
#include <stddef.h> // size_t
#include <stdint.h> // uint8_t
#include <memory>
#include "platform/api/network_interface.h"
#include "platform/base/error.h"
#include "platform/base/ip_address.h"
#include "platform/base/span.h"
#include "platform/base/udp_packet.h"
namespace openscreen {
class TaskRunner;
// An open UDP socket for sending/receiving datagrams to/from either specific
// endpoints or over IP multicast.
//
// Usage: The socket is created and opened by calling the Create() method. This
// returns a unique pointer that auto-closes/destroys the socket when it goes
// out-of-scope.
class UdpSocket {
public:
// Client for the UdpSocket class.
class Client {
public:
// Method called when the UDP socket is bound. Default implementation
// does nothing, as clients may not care about the socket bind state.
virtual void OnBound(UdpSocket* socket) {}
// Method called on socket configuration operations when an error occurs.
// These specific APIs are:
// UdpSocket::Bind()
// UdpSocket::SetMulticastOutboundInterface(...)
// UdpSocket::JoinMulticastGroup(...)
// UdpSocket::SetDscp(...)
virtual void OnError(UdpSocket* socket, const Error& error) = 0;
// Method called when an error occurs during a SendMessage call.
virtual void OnSendError(UdpSocket* socket, const Error& error) = 0;
// Method called when a packet is read.
virtual void OnRead(UdpSocket* socket, ErrorOr<UdpPacket> packet) = 0;
protected:
virtual ~Client();
};
// Common, modern code points for use with DSCP. This list is non-inclusive,
// callers are encouraged to check validity of an integer code point by
// ensuring it is in the bounds of [kBestEffort, kMaxValue] inclusive.
// https://www.rfc-editor.org/rfc/rfc2474.html
enum class DscpMode : uint8_t {
// Best-effort, no differentiated treatment.
kBestEffort = 0,
// Assured Forwarding code points.
// https://datatracker.ietf.org/doc/html/rfc2597#section-6
kAF11 = 10,
kAF12 = 12,
kAF13 = 14,
kAF21 = 18,
kAF22 = 20,
kAF23 = 22,
kAF31 = 26,
kAF32 = 28,
kAF33 = 30,
kAF41 = 34,
kAF42 = 36,
kAF43 = 38,
// Expedited Forwarding (EF) code point.
// https://www.rfc-editor.org/rfc/rfc3246.html
kEF = 46,
// As a 6-bit value, DSCP ranges from [0, 63] inclusive.
kMaxValue = 63,
};
using Version = IPAddress::Version;
// Creates a new, scoped UdpSocket within the IPv4 or IPv6 family.
// `local_endpoint` may be zero (see comments for Bind()). This method must be
// defined in the platform-level implementation. All `client` methods called
// will be queued on the provided `task_runner`. For this reason, the provided
// TaskRunner and Client must exist for the duration of the created socket's
// lifetime.
static ErrorOr<std::unique_ptr<UdpSocket>> Create(
TaskRunner& task_runner,
Client* client,
const IPEndpoint& local_endpoint);
virtual ~UdpSocket();
// Returns true if `socket` belongs to the IPv4/IPv6 address family.
virtual bool IsIPv4() const = 0;
virtual bool IsIPv6() const = 0;
// Returns the current local endpoint's address and port. Initially, this will
// be the same as the value that was passed into Create(). However, it can
// later change after certain operations, such as Bind(), are executed.
virtual IPEndpoint GetLocalEndpoint() const = 0;
// Binds to the address specified in the constructor. If the local endpoint's
// address is zero, the operating system will bind to all interfaces. If the
// local endpoint's port is zero, the operating system will automatically find
// a free local port and bind to it. Future calls to GetLocalEndpoint() will
// reflect the resolved port.
virtual void Bind() = 0;
// Sets the device to use for outgoing multicast packets on the socket.
virtual void SetMulticastOutboundInterface(NetworkInterfaceIndex ifindex) = 0;
// Joins to the multicast group at the given address, using the specified
// interface.
virtual void JoinMulticastGroup(const IPAddress& address,
NetworkInterfaceIndex ifindex) = 0;
// Sends a message. If the message is not sent, Client::OnSendError() will be
// called to indicate this. Error::Code::kAgain indicates the operation would
// block, which can be expected during normal operation.
virtual void SendMessage(ByteView data, const IPEndpoint& dest) = 0;
// Sets the DSCP value to use for all messages sent from this socket.
virtual void SetDscp(DscpMode mode) = 0;
protected:
UdpSocket();
};
} // namespace openscreen
#endif // PLATFORM_API_UDP_SOCKET_H_

Some files were not shown because too many files have changed in this diff Show more