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,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_