aboutsummaryrefslogtreecommitdiff
path: root/cast/streaming
diff options
context:
space:
mode:
Diffstat (limited to 'cast/streaming')
-rw-r--r--cast/streaming/answer_messages.cc473
-rw-r--r--cast/streaming/answer_messages.h78
-rw-r--r--cast/streaming/answer_messages_unittest.cc509
-rw-r--r--cast/streaming/message_util.h66
-rw-r--r--cast/streaming/offer_messages.cc69
-rw-r--r--cast/streaming/receiver_session.cc70
-rw-r--r--cast/streaming/receiver_session.h8
-rw-r--r--cast/streaming/receiver_session_unittest.cc18
-rw-r--r--cast/streaming/session_config.cc13
-rw-r--r--cast/streaming/session_config.h10
10 files changed, 1006 insertions, 308 deletions
diff --git a/cast/streaming/answer_messages.cc b/cast/streaming/answer_messages.cc
index e62f5a1f..a6303f1c 100644
--- a/cast/streaming/answer_messages.cc
+++ b/cast/streaming/answer_messages.cc
@@ -7,8 +7,9 @@
#include <utility>
#include "absl/strings/str_cat.h"
-#include "cast/streaming/message_util.h"
+#include "absl/strings/str_split.h"
#include "platform/base/error.h"
+#include "util/json/json_helpers.h"
#include "util/osp_logging.h"
namespace openscreen {
@@ -16,26 +17,143 @@ namespace cast {
namespace {
-static constexpr char kMessageKeyType[] = "type";
-static constexpr char kMessageTypeAnswer[] = "ANSWER";
-
-// List of ANSWER message fields.
-static constexpr char kAnswerMessageBody[] = "answer";
-static constexpr char kResult[] = "result";
-static constexpr char kResultOk[] = "ok";
-static constexpr char kResultError[] = "error";
-static constexpr char kErrorMessageBody[] = "error";
-static constexpr char kErrorCode[] = "code";
-static constexpr char kErrorDescription[] = "description";
+/// NOTE: Constants here are all taken from the Cast V2: Mirroring Control
+/// Protocol specification: http://goto.google.com/mirroring-control-protocol
+// TODO(jophba): document the protocol in a public repository.
+
+/// Constraint properties.
+// Audio constraints. See properties below.
+static constexpr char kAudio[] = "audio";
+// Video constraints. See properties below.
+static 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.
+static constexpr char kMinBitRate[] = "minBitRate";
+// 32kbps is sender default for audio minimum bit rate.
+static constexpr int kDefaultAudioMinBitRate = 32 * 1000;
+// 300kbps is sender default for video minimum bit rate.
+static constexpr int kDefaultVideoMinBitRate = 300 * 1000;
+
+// Maximum encoded bits per second. This is the lower of (1) the max capability
+// of the decoder, or (2) the max data transfer rate.
+static constexpr char kMaxBitRate[] = "maxBitRate";
+// Maximum supported end-to-end latency, in milliseconds. Proportional to the
+// size of the data buffers in the receiver.
+static 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.
+static 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.
+static constexpr char kMinDimensions[] = "minDimensions";
+// Maximum dimensions, not necessarily ideal dimensions.
+static constexpr char kMaxDimensions[] = "maxDimensions";
+
+/// Audio constraint properties.
+// Maximum supported sampling frequency (not necessarily ideal).
+static constexpr char kMaxSampleRate[] = "maxSampleRate";
+// Maximum number of audio channels (1 is mono, 2 is stereo, etc.).
+static constexpr char kMaxChannels[] = "maxChannels";
+
+/// Dimension properties.
+// Width in pixels.
+static constexpr char kWidth[] = "width";
+// Height in pixels.
+static constexpr char kHeight[] = "height";
+// Frame rate as a rational decimal number or fraction.
+// E.g. 30 and "3000/1001" are both valid representations.
+static constexpr char kFrameRate[] = "frameRate";
+
+/// 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).
+static 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.
+static constexpr char kAspectRatio[] = "aspectRatio";
+// The delimeter used for the aspect ratio format ("A:B").
+static constexpr char kAspectRatioDelimiter[] = ":";
+// Sets the aspect ratio constraints. Value must be either "sender" or
+// "receiver", see kScalingSender and kScalingReceiver below.
+static constexpr char kScaling[] = "scaling";
+// sclaing = "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.
+static 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.
+static 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.
+static constexpr char kUdpPort[] = "udpPort";
+static constexpr int kUdpPortMin = 1;
+static constexpr int kUdpPortMax = 65535;
+// Numbers specifying the indexes chosen from the offer message.
+static constexpr char kSendIndexes[] = "sendIndexes";
+// uint32_t values specifying the RTP SSRC values used to send the RTCP feedback
+// of the stream indicated in kSendIndexes.
+static 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.
+static constexpr char kConstraints[] = "constraints";
+// Provides details about the display on the receiver.
+static constexpr char kDisplay[] = "display";
+// absl::optional array of numbers specifying the indexes of streams that will
+// send event logs through RTCP.
+static 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.
+static constexpr char kReceiverRtcpDscp[] = "receiverRtcpDscp";
+// True if receiver can report wifi status.
+static constexpr char kReceiverGetStatus[] = "receiverGetStatus";
+// If this optional field is present the receiver supports the specific
+// RTP extensions (such as adaptive playout delay).
+static constexpr char kRtpExtensions[] = "rtpExtensions";
Json::Value AspectRatioConstraintToJson(AspectRatioConstraint aspect_ratio) {
switch (aspect_ratio) {
case AspectRatioConstraint::kVariable:
- return Json::Value("receiver");
+ return Json::Value(kScalingReceiver);
case AspectRatioConstraint::kFixed:
default:
- return Json::Value("sender");
+ return Json::Value(kScalingSender);
+ }
+}
+
+bool AspectRatioConstraintParseAndValidate(const Json::Value& value,
+ AspectRatioConstraint* out) {
+ // the aspect ratio constraint is an optional field.
+ if (!value) {
+ return true;
+ }
+
+ std::string aspect_ratio;
+ if (!json::ParseAndValidateString(value, &aspect_ratio)) {
+ return false;
+ }
+ if (aspect_ratio == kScalingReceiver) {
+ *out = AspectRatioConstraint::kVariable;
+ return true;
+ } else if (aspect_ratio == kScalingSender) {
+ *out = AspectRatioConstraint::kFixed;
+ return true;
}
+ return false;
}
template <typename T>
@@ -50,158 +168,283 @@ Json::Value PrimitiveVectorToJson(const std::vector<T>& vec) {
return array;
}
+template <typename T>
+bool ParseOptional(const Json::Value& value, absl::optional<T>* out) {
+ // It's fine if the value is empty.
+ if (!value) {
+ return true;
+ }
+ T tentative_out;
+ if (!T::ParseAndValidate(value, &tentative_out)) {
+ return false;
+ }
+ *out = tentative_out;
+ return true;
+}
+
} // namespace
-ErrorOr<Json::Value> AudioConstraints::ToJson() const {
- if (max_sample_rate <= 0 || max_channels <= 0 || min_bit_rate <= 0 ||
- max_bit_rate < min_bit_rate) {
- return CreateParameterError("AudioConstraints");
+// static
+bool AspectRatio::ParseAndValidate(const Json::Value& value, AspectRatio* out) {
+ std::string parsed_value;
+ if (!json::ParseAndValidateString(value, &parsed_value)) {
+ return false;
}
- Json::Value root;
- root["maxSampleRate"] = max_sample_rate;
- root["maxChannels"] = max_channels;
- root["minBitRate"] = min_bit_rate;
- root["maxBitRate"] = max_bit_rate;
- root["maxDelay"] = Json::Value::Int64(max_delay.count());
- return root;
+ std::vector<absl::string_view> fields =
+ absl::StrSplit(parsed_value, kAspectRatioDelimiter);
+ if (fields.size() != 2) {
+ return false;
+ }
+
+ if (!absl::SimpleAtoi(fields[0], &out->width) ||
+ !absl::SimpleAtoi(fields[1], &out->height)) {
+ return false;
+ }
+ return out->IsValid();
}
-ErrorOr<Json::Value> Dimensions::ToJson() const {
- if (width <= 0 || height <= 0 || !frame_rate.is_defined() ||
- !frame_rate.is_positive()) {
- return CreateParameterError("Dimensions");
+bool AspectRatio::IsValid() const {
+ return width > 0 && height > 0;
+}
+
+// static
+bool AudioConstraints::ParseAndValidate(const Json::Value& root,
+ AudioConstraints* out) {
+ if (!json::ParseAndValidateInt(root[kMaxSampleRate],
+ &(out->max_sample_rate)) ||
+ !json::ParseAndValidateInt(root[kMaxChannels], &(out->max_channels)) ||
+ !json::ParseAndValidateInt(root[kMaxBitRate], &(out->max_bit_rate)) ||
+ !json::ParseAndValidateMilliseconds(root[kMaxDelay], &(out->max_delay))) {
+ return false;
+ }
+ if (!json::ParseAndValidateInt(root[kMinBitRate], &(out->min_bit_rate))) {
+ out->min_bit_rate = kDefaultAudioMinBitRate;
}
+ return out->IsValid();
+}
+Json::Value AudioConstraints::ToJson() const {
+ OSP_DCHECK(IsValid());
Json::Value root;
- root["width"] = width;
- root["height"] = height;
- root["frameRate"] = frame_rate.ToString();
+ root[kMaxSampleRate] = max_sample_rate;
+ root[kMaxChannels] = max_channels;
+ root[kMinBitRate] = min_bit_rate;
+ root[kMaxBitRate] = max_bit_rate;
+ root[kMaxDelay] = Json::Value::Int64(max_delay.count());
return root;
}
-ErrorOr<Json::Value> VideoConstraints::ToJson() const {
- if (max_pixels_per_second <= 0 || min_bit_rate <= 0 ||
- max_bit_rate < min_bit_rate || max_delay.count() <= 0) {
- return CreateParameterError("VideoConstraints");
- }
+bool AudioConstraints::IsValid() const {
+ return max_sample_rate > 0 && max_channels > 0 && min_bit_rate > 0 &&
+ max_bit_rate >= min_bit_rate;
+}
- auto error_or_min_dim = min_dimensions.ToJson();
- if (error_or_min_dim.is_error()) {
- return error_or_min_dim.error();
+bool Dimensions::ParseAndValidate(const Json::Value& root, Dimensions* out) {
+ if (!json::ParseAndValidateInt(root[kWidth], &(out->width)) ||
+ !json::ParseAndValidateInt(root[kHeight], &(out->height)) ||
+ !json::ParseAndValidateSimpleFraction(root[kFrameRate],
+ &(out->frame_rate))) {
+ return false;
}
+ return out->IsValid();
+}
- auto error_or_max_dim = max_dimensions.ToJson();
- if (error_or_max_dim.is_error()) {
- return error_or_max_dim.error();
- }
+bool Dimensions::IsValid() const {
+ return width > 0 && height > 0 && frame_rate.is_positive();
+}
+Json::Value Dimensions::ToJson() const {
+ OSP_DCHECK(IsValid());
Json::Value root;
- root["maxPixelsPerSecond"] = max_pixels_per_second;
- root["minDimensions"] = error_or_min_dim.value();
- root["maxDimensions"] = error_or_max_dim.value();
- root["minBitRate"] = min_bit_rate;
- root["maxBitRate"] = max_bit_rate;
- root["maxDelay"] = Json::Value::Int64(max_delay.count());
+ root[kWidth] = width;
+ root[kHeight] = height;
+ root[kFrameRate] = frame_rate.ToString();
return root;
}
-ErrorOr<Json::Value> Constraints::ToJson() const {
- auto audio_or_error = audio.ToJson();
- if (audio_or_error.is_error()) {
- return audio_or_error.error();
+// static
+bool VideoConstraints::ParseAndValidate(const Json::Value& root,
+ VideoConstraints* out) {
+ if (!json::ParseAndValidateDouble(root[kMaxPixelsPerSecond],
+ &(out->max_pixels_per_second)) ||
+ !Dimensions::ParseAndValidate(root[kMaxDimensions],
+ &(out->max_dimensions)) ||
+ !json::ParseAndValidateInt(root[kMaxBitRate], &(out->max_bit_rate)) ||
+ !json::ParseAndValidateMilliseconds(root[kMaxDelay], &(out->max_delay)) ||
+ !ParseOptional<Dimensions>(root[kMinDimensions],
+ &(out->min_dimensions))) {
+ return false;
}
-
- auto video_or_error = video.ToJson();
- if (video_or_error.is_error()) {
- return video_or_error.error();
+ if (!json::ParseAndValidateInt(root[kMinBitRate], &(out->min_bit_rate))) {
+ out->min_bit_rate = kDefaultVideoMinBitRate;
}
+ return out->IsValid();
+}
+bool VideoConstraints::IsValid() const {
+ return max_pixels_per_second > 0 && min_bit_rate > 0 &&
+ max_bit_rate > min_bit_rate && max_delay.count() > 0 &&
+ max_dimensions.IsValid() &&
+ (!min_dimensions.has_value() || min_dimensions->IsValid()) &&
+ max_dimensions.frame_rate.numerator > 0;
+}
+
+Json::Value VideoConstraints::ToJson() const {
+ OSP_DCHECK(IsValid());
Json::Value root;
- root["audio"] = audio_or_error.value();
- root["video"] = video_or_error.value();
+ root[kMaxPixelsPerSecond] = max_pixels_per_second;
+ if (min_dimensions.has_value()) {
+ root[kMinDimensions] = min_dimensions->ToJson();
+ }
+ root[kMaxDimensions] = max_dimensions.ToJson();
+ root[kMinBitRate] = min_bit_rate;
+ root[kMaxBitRate] = max_bit_rate;
+ root[kMaxDelay] = Json::Value::Int64(max_delay.count());
return root;
}
-ErrorOr<Json::Value> DisplayDescription::ToJson() const {
- if (aspect_ratio.width < 1 || aspect_ratio.height < 1) {
- return CreateParameterError("DisplayDescription");
+// static
+bool Constraints::ParseAndValidate(const Json::Value& root, Constraints* out) {
+ if (!AudioConstraints::ParseAndValidate(root[kAudio], &(out->audio)) ||
+ !VideoConstraints::ParseAndValidate(root[kVideo], &(out->video))) {
+ return false;
}
+ return out->IsValid();
+}
- auto dimensions_or_error = dimensions.ToJson();
- if (dimensions_or_error.is_error()) {
- return dimensions_or_error.error();
- }
+bool Constraints::IsValid() const {
+ return audio.IsValid() && video.IsValid();
+}
+Json::Value Constraints::ToJson() const {
+ OSP_DCHECK(IsValid());
Json::Value root;
- root["dimensions"] = dimensions_or_error.value();
- root["aspectRatio"] =
- absl::StrCat(aspect_ratio.width, ":", aspect_ratio.height);
- root["scaling"] = AspectRatioConstraintToJson(aspect_ratio_constraint);
+ root[kAudio] = audio.ToJson();
+ root[kVideo] = video.ToJson();
return root;
}
-ErrorOr<Json::Value> Answer::ToJson() const {
- if (udp_port <= 0 || udp_port > 65535) {
- return CreateParameterError("Answer - UDP Port number");
+// static
+bool DisplayDescription::ParseAndValidate(const Json::Value& root,
+ DisplayDescription* out) {
+ if (!ParseOptional<Dimensions>(root[kDimensions], &(out->dimensions)) ||
+ !ParseOptional<AspectRatio>(root[kAspectRatio], &(out->aspect_ratio))) {
+ return false;
}
- Json::Value root;
- if (constraints) {
- auto constraints_or_error = constraints.value().ToJson();
- if (constraints_or_error.is_error()) {
- return constraints_or_error.error();
- }
- root["constraints"] = constraints_or_error.value();
+ AspectRatioConstraint constraint;
+ if (AspectRatioConstraintParseAndValidate(root[kScaling], &constraint)) {
+ out->aspect_ratio_constraint =
+ absl::optional<AspectRatioConstraint>(std::move(constraint));
+ } else {
+ out->aspect_ratio_constraint = absl::nullopt;
}
- if (display) {
- auto display_or_error = display.value().ToJson();
- if (display_or_error.is_error()) {
- return display_or_error.error();
- }
- root["display"] = display_or_error.value();
+ return out->IsValid();
+}
+
+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 are provided.
+ if (aspect_ratio_constraint.has_value() &&
+ (aspect_ratio_constraint.value() == AspectRatioConstraint::kFixed) &&
+ !dimensions.has_value()) {
+ return false;
+ }
+ return aspect_ratio.has_value() || dimensions.has_value() ||
+ aspect_ratio_constraint.has_value();
+}
- root["castMode"] = cast_mode.ToString();
- root["udpPort"] = udp_port;
- root["receiverGetStatus"] = supports_wifi_status_reporting;
- root["sendIndexes"] = PrimitiveVectorToJson(send_indexes);
- root["ssrcs"] = PrimitiveVectorToJson(ssrcs);
- if (!receiver_rtcp_event_log.empty()) {
- root["receiverRtcpEventLog"] =
- PrimitiveVectorToJson(receiver_rtcp_event_log);
+Json::Value DisplayDescription::ToJson() const {
+ OSP_DCHECK(IsValid());
+ Json::Value root;
+ if (aspect_ratio.has_value()) {
+ root[kAspectRatio] = absl::StrCat(
+ aspect_ratio->width, kAspectRatioDelimiter, aspect_ratio->height);
}
- if (!receiver_rtcp_dscp.empty()) {
- root["receiverRtcpDscp"] = PrimitiveVectorToJson(receiver_rtcp_dscp);
+ if (dimensions.has_value()) {
+ root[kDimensions] = dimensions->ToJson();
}
- if (!rtp_extensions.empty()) {
- root["rtpExtensions"] = PrimitiveVectorToJson(rtp_extensions);
+ if (aspect_ratio_constraint.has_value()) {
+ root[kScaling] =
+ AspectRatioConstraintToJson(aspect_ratio_constraint.value());
}
return root;
}
-Json::Value Answer::ToAnswerMessage() const {
- auto json_or_error = ToJson();
- if (json_or_error.is_error()) {
- return CreateInvalidAnswer(json_or_error.error());
+bool Answer::ParseAndValidate(const Json::Value& root, Answer* out) {
+ if (!json::ParseAndValidateInt(root[kUdpPort], &(out->udp_port)) ||
+ !json::ParseAndValidateIntArray(root[kSendIndexes],
+ &(out->send_indexes)) ||
+ !json::ParseAndValidateUintArray(root[kSsrcs], &(out->ssrcs)) ||
+ !ParseOptional<Constraints>(root[kConstraints], &(out->constraints)) ||
+ !ParseOptional<DisplayDescription>(root[kDisplay], &(out->display))) {
+ return false;
+ }
+ if (!json::ParseBool(root[kReceiverGetStatus],
+ &(out->supports_wifi_status_reporting))) {
+ out->supports_wifi_status_reporting = false;
}
- Json::Value message_root;
- message_root[kMessageKeyType] = kMessageTypeAnswer;
- message_root[kAnswerMessageBody] = std::move(json_or_error.value());
- message_root[kResult] = kResultOk;
- return message_root;
+ // These function set to empty array if not present, so we can ignore
+ // the return value for optional values.
+ json::ParseAndValidateIntArray(root[kReceiverRtcpEventLog],
+ &(out->receiver_rtcp_event_log));
+ json::ParseAndValidateIntArray(root[kReceiverRtcpDscp],
+ &(out->receiver_rtcp_dscp));
+ json::ParseAndValidateStringArray(root[kRtpExtensions],
+ &(out->rtp_extensions));
+
+ return out->IsValid();
}
-Json::Value CreateInvalidAnswer(Error error) {
- Json::Value message_root;
- message_root[kMessageKeyType] = kMessageTypeAnswer;
- message_root[kResult] = kResultError;
- message_root[kErrorMessageBody][kErrorCode] = static_cast<int>(error.code());
- message_root[kErrorMessageBody][kErrorDescription] = error.message();
+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;
+}
- return message_root;
+Json::Value Answer::ToJson() const {
+ OSP_DCHECK(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[kReceiverGetStatus] = supports_wifi_status_reporting;
+ root[kSendIndexes] = PrimitiveVectorToJson(send_indexes);
+ root[kSsrcs] = PrimitiveVectorToJson(ssrcs);
+ root[kReceiverRtcpEventLog] = PrimitiveVectorToJson(receiver_rtcp_event_log);
+ root[kReceiverRtcpDscp] = PrimitiveVectorToJson(receiver_rtcp_dscp);
+ root[kRtpExtensions] = PrimitiveVectorToJson(rtp_extensions);
+ return root;
}
} // namespace cast
diff --git a/cast/streaming/answer_messages.h b/cast/streaming/answer_messages.h
index efd72d6a..4298913b 100644
--- a/cast/streaming/answer_messages.h
+++ b/cast/streaming/answer_messages.h
@@ -14,7 +14,7 @@
#include <utility>
#include <vector>
-#include "cast/streaming/offer_messages.h"
+#include "absl/types/optional.h"
#include "cast/streaming/ssrc.h"
#include "json/value.h"
#include "platform/base/error.h"
@@ -23,42 +23,61 @@
namespace openscreen {
namespace 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) ParseAndValidate. 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_DCHECKs if not IsValid().
+// (3) IsValid. Used by both ParseAndValidate and ToJson to ensure that the
+// object is in a good state.
struct AudioConstraints {
+ static bool ParseAndValidate(const Json::Value& value, AudioConstraints* out);
+ Json::Value ToJson() const;
+ bool IsValid() const;
+
int max_sample_rate = 0;
int max_channels = 0;
// Technically optional, sender will assume 32kbps if omitted.
int min_bit_rate = 0;
int max_bit_rate = 0;
std::chrono::milliseconds max_delay = {};
-
- ErrorOr<Json::Value> ToJson() const;
};
struct Dimensions {
+ static bool ParseAndValidate(const Json::Value& value, Dimensions* out);
+ Json::Value ToJson() const;
+ bool IsValid() const;
+
int width = 0;
int height = 0;
SimpleFraction frame_rate;
-
- ErrorOr<Json::Value> ToJson() const;
};
struct VideoConstraints {
+ static bool ParseAndValidate(const Json::Value& value, VideoConstraints* out);
+ Json::Value ToJson() const;
+ bool IsValid() const;
+
double max_pixels_per_second = {};
- Dimensions min_dimensions = {};
+ absl::optional<Dimensions> min_dimensions = {};
Dimensions max_dimensions = {};
// Technically optional, sender will assume 300kbps if omitted.
int min_bit_rate = 0;
int max_bit_rate = 0;
std::chrono::milliseconds max_delay = {};
-
- ErrorOr<Json::Value> ToJson() const;
};
struct Constraints {
+ static bool ParseAndValidate(const Json::Value& value, Constraints* out);
+ Json::Value ToJson() const;
+ bool IsValid() const;
+
AudioConstraints audio;
VideoConstraints video;
-
- ErrorOr<Json::Value> ToJson() const;
};
// Decides whether the Sender scales and letterboxes content to 16:9, or if
@@ -67,22 +86,35 @@ struct Constraints {
enum class AspectRatioConstraint : uint8_t { kVariable = 0, kFixed };
struct AspectRatio {
+ static bool ParseAndValidate(const Json::Value& value, AspectRatio* out);
+ 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 bool ParseAndValidate(const Json::Value& value,
+ DisplayDescription* out);
+ Json::Value ToJson() const;
+ bool IsValid() const;
+
// May exceed, be the same, or less than those mentioned in the
// video constraints.
- Dimensions dimensions;
- AspectRatio aspect_ratio = {};
- AspectRatioConstraint aspect_ratio_constraint = {};
-
- ErrorOr<Json::Value> ToJson() const;
+ absl::optional<Dimensions> dimensions;
+ absl::optional<AspectRatio> aspect_ratio = {};
+ absl::optional<AspectRatioConstraint> aspect_ratio_constraint = {};
};
struct Answer {
- CastMode cast_mode = {};
+ static bool ParseAndValidate(const Json::Value& value, Answer* out);
+ Json::Value ToJson() const;
+ bool IsValid() const;
+
int udp_port = 0;
std::vector<int> send_indexes;
std::vector<Ssrc> ssrcs;
@@ -97,22 +129,8 @@ struct Answer {
// RTP extensions should be empty, but not null.
std::vector<std::string> rtp_extensions = {};
-
- // ToJson performs a standard serialization, returning an error if this
- // instance failed to serialize properly.
- ErrorOr<Json::Value> ToJson() const;
-
- // In constrast to ToJson, ToAnswerMessage performs a successful serialization
- // even if the answer object is malformed, by complying to the spec's
- // error answer message format in this case.
- Json::Value ToAnswerMessage() const;
};
-// Helper method that creates an invalid Answer response. Exposed publicly
-// here as it is called in ToAnswerMessage(), but can also be called by
-// the receiver session.
-Json::Value CreateInvalidAnswer(Error error);
-
} // namespace cast
} // namespace openscreen
diff --git a/cast/streaming/answer_messages_unittest.cc b/cast/streaming/answer_messages_unittest.cc
index 35ad846a..1df6101d 100644
--- a/cast/streaming/answer_messages_unittest.cc
+++ b/cast/streaming/answer_messages_unittest.cc
@@ -17,12 +17,61 @@ namespace cast {
namespace {
+using ::testing::ElementsAre;
+
+// NOTE: the castMode property has been removed from the specification. We leave
+// it here in the valid offer to ensure that its inclusion does not break
+// parsing.
+constexpr char kValidAnswerJson[] = R"({
+ "castMode": "mirroring",
+ "udpPort": 1234,
+ "sendIndexes": [1, 3],
+ "ssrcs": [1233324, 2234222],
+ "constraints": {
+ "audio": {
+ "maxSampleRate": 96000,
+ "maxChannels": 5,
+ "minBitRate": 32000,
+ "maxBitRate": 320000,
+ "maxDelay": 5000
+ },
+ "video": {
+ "maxPixelsPerSecond": 62208000,
+ "minDimensions": {
+ "width": 320,
+ "height": 180,
+ "frameRate": 0
+ },
+ "maxDimensions": {
+ "width": 1920,
+ "height": 1080,
+ "frameRate": "60"
+ },
+ "minBitRate": 300000,
+ "maxBitRate": 10000000,
+ "maxDelay": 5000
+ }
+ },
+ "display": {
+ "dimensions": {
+ "width": 1920,
+ "height": 1080,
+ "frameRate": "60000/1001"
+ },
+ "aspectRatio": "64:27",
+ "scaling": "sender"
+ },
+ "receiverRtcpEventLog": [0, 1],
+ "receiverRtcpDscp": [234, 567],
+ "receiverGetStatus": true,
+ "rtpExtensions": ["adaptive_playout_delay"]
+})";
+
const Answer kValidAnswer{
- CastMode{CastMode::Type::kMirroring},
1234, // udp_port
std::vector<int>{1, 2, 3}, // send_indexes
std::vector<Ssrc>{123, 456}, // ssrcs
- Constraints{
+ absl::optional<Constraints>(Constraints{
AudioConstraints{
96000, // max_sample_rate
7, // max_channels
@@ -32,11 +81,11 @@ const Answer kValidAnswer{
}, // audio
VideoConstraints{
40000.0, // max_pixels_per_second
- Dimensions{
+ absl::optional<Dimensions>(Dimensions{
320, // width
480, // height
SimpleFraction{15000, 101} // frame_rate
- }, // min_dimensions
+ }), // min_dimensions
Dimensions{
1920, // width
1080, // height
@@ -46,30 +95,105 @@ const Answer kValidAnswer{
144000000, // max_bit_rate
milliseconds(3000) // max_delay
} // video
- }, // constraints
- DisplayDescription{
- Dimensions{
+ }), // constraints
+ absl::optional<DisplayDescription>(DisplayDescription{
+ absl::optional<Dimensions>(Dimensions{
640, // width
480, // height
SimpleFraction{30, 1} // frame_rate
- },
- AspectRatio{16, 9}, // aspect_ratio
- AspectRatioConstraint::kFixed, // scaling
- },
+ }),
+ absl::optional<AspectRatio>(AspectRatio{16, 9}), // aspect_ratio
+ absl::optional<AspectRatioConstraint>(
+ AspectRatioConstraint::kFixed), // scaling
+ }),
std::vector<int>{7, 8, 9}, // receiver_rtcp_event_log
std::vector<int>{11, 12, 13}, // receiver_rtcp_dscp
true, // receiver_get_status
std::vector<std::string>{"foo", "bar"} // rtp_extensions
};
+constexpr int kValidMaxPixelsPerSecond = 1920 * 1080 * 30;
+constexpr Dimensions kValidDimensions{1920, 1080, SimpleFraction{60, 1}};
+static const VideoConstraints kValidVideoConstraints{
+ kValidMaxPixelsPerSecond, absl::optional<Dimensions>(kValidDimensions),
+ kValidDimensions, 300 * 1000,
+ 300 * 1000 * 1000, milliseconds(3000)};
+
+constexpr AudioConstraints kValidAudioConstraints{123, 456, 300, 9920,
+ milliseconds(123)};
+
+void ExpectEqualsValidAnswerJson(const Answer& answer) {
+ EXPECT_EQ(1234, answer.udp_port);
+
+ EXPECT_THAT(answer.send_indexes, ElementsAre(1, 3));
+ EXPECT_THAT(answer.ssrcs, ElementsAre(1233324u, 2234222u));
+ ASSERT_TRUE(answer.constraints.has_value());
+ const AudioConstraints& audio = answer.constraints->audio;
+ EXPECT_EQ(96000, audio.max_sample_rate);
+ EXPECT_EQ(5, audio.max_channels);
+ EXPECT_EQ(32000, audio.min_bit_rate);
+ EXPECT_EQ(320000, audio.max_bit_rate);
+ EXPECT_EQ(milliseconds{5000}, audio.max_delay);
+
+ const VideoConstraints& video = answer.constraints->video;
+ EXPECT_EQ(62208000, video.max_pixels_per_second);
+ ASSERT_TRUE(video.min_dimensions.has_value());
+ EXPECT_EQ(320, video.min_dimensions->width);
+ EXPECT_EQ(180, video.min_dimensions->height);
+ EXPECT_EQ((SimpleFraction{0, 1}), video.min_dimensions->frame_rate);
+ EXPECT_EQ(1920, video.max_dimensions.width);
+ EXPECT_EQ(1080, video.max_dimensions.height);
+ EXPECT_EQ((SimpleFraction{60, 1}), video.max_dimensions.frame_rate);
+ EXPECT_EQ(300000, video.min_bit_rate);
+ EXPECT_EQ(10000000, video.max_bit_rate);
+ EXPECT_EQ(milliseconds{5000}, video.max_delay);
+
+ ASSERT_TRUE(answer.display.has_value());
+ const DisplayDescription& display = answer.display.value();
+ ASSERT_TRUE(display.dimensions.has_value());
+ EXPECT_EQ(1920, display.dimensions->width);
+ EXPECT_EQ(1080, display.dimensions->height);
+ EXPECT_EQ((SimpleFraction{60000, 1001}), display.dimensions->frame_rate);
+ EXPECT_EQ((AspectRatio{64, 27}), display.aspect_ratio.value());
+ EXPECT_EQ(AspectRatioConstraint::kFixed,
+ display.aspect_ratio_constraint.value());
+
+ EXPECT_THAT(answer.receiver_rtcp_event_log, ElementsAre(0, 1));
+ EXPECT_THAT(answer.receiver_rtcp_dscp, ElementsAre(234, 567));
+ EXPECT_TRUE(answer.supports_wifi_status_reporting);
+ EXPECT_THAT(answer.rtp_extensions, ElementsAre("adaptive_playout_delay"));
+}
+
+void ExpectFailureOnParse(absl::string_view raw_json) {
+ ErrorOr<Json::Value> root = json::Parse(raw_json);
+ // Must be a valid JSON object, but not a valid answer.
+ ASSERT_TRUE(root.is_value());
+
+ Answer answer;
+ EXPECT_FALSE(Answer::ParseAndValidate(std::move(root.value()), &answer));
+ EXPECT_FALSE(answer.IsValid());
+}
+
+// Functions that use ASSERT_* must return void, so we use an out parameter
+// here instead of returning.
+void ExpectSuccessOnParse(absl::string_view raw_json, Answer* out = nullptr) {
+ ErrorOr<Json::Value> root = json::Parse(raw_json);
+ // Must be a valid JSON object, but not a valid answer.
+ ASSERT_TRUE(root.is_value());
+
+ Answer answer;
+ ASSERT_TRUE(Answer::ParseAndValidate(std::move(root.value()), &answer));
+ EXPECT_TRUE(answer.IsValid());
+ if (out) {
+ *out = std::move(answer);
+ }
+}
+
} // anonymous namespace
TEST(AnswerMessagesTest, ProperlyPopulatedAnswerSerializesProperly) {
- auto value_or_error = kValidAnswer.ToJson();
- EXPECT_TRUE(value_or_error.is_value());
-
- Json::Value root = std::move(value_or_error.value());
- EXPECT_EQ(root["castMode"], "mirroring");
+ ASSERT_TRUE(kValidAnswer.IsValid());
+ Json::Value root = kValidAnswer.ToJson();
EXPECT_EQ(root["udpPort"], 1234);
Json::Value sendIndexes = std::move(root["sendIndexes"]);
@@ -142,41 +266,362 @@ TEST(AnswerMessagesTest, ProperlyPopulatedAnswerSerializesProperly) {
EXPECT_EQ(rtp_extensions[1], "bar");
}
-TEST(AnswerMessagesTest, InvalidDimensionsCauseError) {
+TEST(AnswerMessagesTest, InvalidDimensionsCauseInvalid) {
Answer invalid_dimensions = kValidAnswer;
- invalid_dimensions.display.value().dimensions.width = -1;
- auto value_or_error = invalid_dimensions.ToJson();
- EXPECT_TRUE(value_or_error.is_error());
+ invalid_dimensions.display->dimensions->width = -1;
+ EXPECT_FALSE(invalid_dimensions.IsValid());
}
TEST(AnswerMessagesTest, InvalidAudioConstraintsCauseError) {
Answer invalid_audio = kValidAnswer;
- invalid_audio.constraints.value().audio.max_bit_rate =
- invalid_audio.constraints.value().audio.min_bit_rate - 1;
- auto value_or_error = invalid_audio.ToJson();
- EXPECT_TRUE(value_or_error.is_error());
+ invalid_audio.constraints->audio.max_bit_rate =
+ invalid_audio.constraints->audio.min_bit_rate - 1;
+ EXPECT_FALSE(invalid_audio.IsValid());
}
TEST(AnswerMessagesTest, InvalidVideoConstraintsCauseError) {
Answer invalid_video = kValidAnswer;
- invalid_video.constraints.value().video.max_pixels_per_second = -1.0;
- auto value_or_error = invalid_video.ToJson();
- EXPECT_TRUE(value_or_error.is_error());
+ invalid_video.constraints->video.max_pixels_per_second = -1.0;
+ EXPECT_FALSE(invalid_video.IsValid());
}
TEST(AnswerMessagesTest, InvalidDisplayDescriptionsCauseError) {
Answer invalid_display = kValidAnswer;
- invalid_display.display.value().aspect_ratio = {0, 0};
- auto value_or_error = invalid_display.ToJson();
- EXPECT_TRUE(value_or_error.is_error());
+ invalid_display.display->aspect_ratio = {0, 0};
+ EXPECT_FALSE(invalid_display.IsValid());
}
TEST(AnswerMessagesTest, InvalidUdpPortsCauseError) {
Answer invalid_port = kValidAnswer;
invalid_port.udp_port = 65536;
- auto value_or_error = invalid_port.ToJson();
- EXPECT_TRUE(value_or_error.is_error());
+ EXPECT_FALSE(invalid_port.IsValid());
+}
+
+TEST(AnswerMessagesTest, CanParseValidAnswerJson) {
+ Answer answer;
+ ExpectSuccessOnParse(kValidAnswerJson, &answer);
+ ExpectEqualsValidAnswerJson(answer);
+}
+
+// In practice, the rtpExtensions, receiverRtcpDscp, and receiverRtcpEventLog
+// fields may be missing from some receivers. We handle this case by treating
+// them as empty.
+TEST(AnswerMessagesTest, SucceedsWithMissingRtpFields) {
+ ExpectSuccessOnParse(R"({
+ "udpPort": 1234,
+ "sendIndexes": [1, 3],
+ "ssrcs": [1233324, 2234222],
+ "receiverGetStatus": true
+ })");
+}
+
+TEST(AnswerMessagesTest, ErrorOnEmptyAnswer) {
+ ExpectFailureOnParse("{}");
+}
+
+TEST(AnswerMessagesTest, ErrorOnMissingUdpPort) {
+ ExpectFailureOnParse(R"({
+ "sendIndexes": [1, 3],
+ "ssrcs": [1233324, 2234222],
+ "receiverGetStatus": true
+ })");
+}
+
+TEST(AnswerMessagesTest, ErrorOnMissingSsrcs) {
+ ExpectFailureOnParse(R"({
+ "udpPort": 1234,
+ "sendIndexes": [1, 3],
+ "receiverGetStatus": true
+ })");
+}
+
+TEST(AnswerMessagesTest, ErrorOnMissingSendIndexes) {
+ ExpectFailureOnParse(R"({
+ "udpPort": 1234,
+ "ssrcs": [1233324, 2234222],
+ "receiverGetStatus": true
+ })");
+}
+
+TEST(AnswerMessagesTest, AssumesNoReportingIfGetStatusFalse) {
+ Answer answer;
+ ExpectSuccessOnParse(R"({
+ "udpPort": 1234,
+ "sendIndexes": [1, 3],
+ "ssrcs": [1233324, 2234222]
+ })",
+ &answer);
+
+ EXPECT_FALSE(answer.supports_wifi_status_reporting);
+}
+
+TEST(AnswerMessagesTest, AllowsReceiverSideScaling) {
+ Answer answer;
+ ExpectSuccessOnParse(R"({
+ "udpPort": 1234,
+ "sendIndexes": [1, 3],
+ "ssrcs": [1233324, 2234222],
+ "display": {
+ "dimensions": {
+ "width": 1920,
+ "height": 1080,
+ "frameRate": "60000/1001"
+ },
+ "aspectRatio": "64:27",
+ "scaling": "receiver"
+ }
+ })",
+ &answer);
+ ASSERT_TRUE(answer.display.has_value());
+ EXPECT_EQ(answer.display->aspect_ratio_constraint.value(),
+ AspectRatioConstraint::kVariable);
+}
+
+TEST(AnswerMessagesTest, AssumesMinBitRateIfOmitted) {
+ Answer answer;
+ ExpectSuccessOnParse(R"({
+ "udpPort": 1234,
+ "sendIndexes": [1, 3],
+ "ssrcs": [1233324, 2234222],
+ "constraints": {
+ "audio": {
+ "maxSampleRate": 96000,
+ "maxChannels": 5,
+ "maxBitRate": 320000,
+ "maxDelay": 5000
+ },
+ "video": {
+ "maxPixelsPerSecond": 62208000,
+ "maxDimensions": {
+ "width": 1920,
+ "height": 1080,
+ "frameRate": "60"
+ },
+ "maxBitRate": 10000000,
+ "maxDelay": 5000
+ }
+ },
+ "receiverGetStatus": true
+ })",
+ &answer);
+
+ EXPECT_EQ(32000, answer.constraints->audio.min_bit_rate);
+ EXPECT_EQ(300000, answer.constraints->video.min_bit_rate);
+}
+
+// Instead of testing all possible json parsing options for validity, we
+// can instead directly test the IsValid() methods.
+TEST(AnswerMessagesTest, AudioConstraintsIsValid) {
+ constexpr AudioConstraints kInvalidSampleRate{0, 456, 300, 9920,
+ milliseconds(123)};
+ constexpr AudioConstraints kInvalidMaxChannels{123, 0, 300, 9920,
+ milliseconds(123)};
+ constexpr AudioConstraints kInvalidMinBitRate{123, 456, 0, 9920,
+ milliseconds(123)};
+ constexpr AudioConstraints kInvalidMaxBitRate{123, 456, 300, 0,
+ milliseconds(123)};
+ constexpr AudioConstraints kInvalidMaxDelay{123, 456, 300, 0,
+ milliseconds(0)};
+
+ EXPECT_TRUE(kValidAudioConstraints.IsValid());
+ EXPECT_FALSE(kInvalidSampleRate.IsValid());
+ EXPECT_FALSE(kInvalidMaxChannels.IsValid());
+ EXPECT_FALSE(kInvalidMinBitRate.IsValid());
+ EXPECT_FALSE(kInvalidMaxBitRate.IsValid());
+ EXPECT_FALSE(kInvalidMaxDelay.IsValid());
+}
+
+TEST(AnswerMessagesTest, DimensionsIsValid) {
+ // NOTE: in some cases (such as min dimensions) a frame rate of zero is valid.
+ constexpr Dimensions kValidZeroFrameRate{1920, 1080, SimpleFraction{0, 60}};
+ constexpr Dimensions kInvalidWidth{0, 1080, SimpleFraction{60, 1}};
+ constexpr Dimensions kInvalidHeight{1920, 0, SimpleFraction{60, 1}};
+ constexpr Dimensions kInvalidFrameRateZeroDenominator{1920, 1080,
+ SimpleFraction{60, 0}};
+ constexpr Dimensions kInvalidFrameRateNegativeNumerator{
+ 1920, 1080, SimpleFraction{-1, 30}};
+ constexpr Dimensions kInvalidFrameRateNegativeDenominator{
+ 1920, 1080, SimpleFraction{30, -1}};
+
+ EXPECT_TRUE(kValidDimensions.IsValid());
+ EXPECT_TRUE(kValidZeroFrameRate.IsValid());
+ EXPECT_FALSE(kInvalidWidth.IsValid());
+ EXPECT_FALSE(kInvalidHeight.IsValid());
+ EXPECT_FALSE(kInvalidFrameRateZeroDenominator.IsValid());
+ EXPECT_FALSE(kInvalidFrameRateNegativeNumerator.IsValid());
+ EXPECT_FALSE(kInvalidFrameRateNegativeDenominator.IsValid());
+}
+
+TEST(AnswerMessagesTest, VideoConstraintsIsValid) {
+ VideoConstraints invalid_max_pixels_per_second = kValidVideoConstraints;
+ invalid_max_pixels_per_second.max_pixels_per_second = 0;
+
+ VideoConstraints invalid_min_dimensions = kValidVideoConstraints;
+ invalid_min_dimensions.min_dimensions->width = 0;
+
+ VideoConstraints invalid_max_dimensions = kValidVideoConstraints;
+ invalid_max_dimensions.max_dimensions.height = 0;
+
+ VideoConstraints invalid_min_bit_rate = kValidVideoConstraints;
+ invalid_min_bit_rate.min_bit_rate = 0;
+
+ VideoConstraints invalid_max_bit_rate = kValidVideoConstraints;
+ invalid_max_bit_rate.max_bit_rate = invalid_max_bit_rate.min_bit_rate - 1;
+
+ VideoConstraints invalid_max_delay = kValidVideoConstraints;
+ invalid_max_delay.max_delay = milliseconds(0);
+
+ EXPECT_TRUE(kValidVideoConstraints.IsValid());
+ EXPECT_FALSE(invalid_max_pixels_per_second.IsValid());
+ EXPECT_FALSE(invalid_min_dimensions.IsValid());
+ EXPECT_FALSE(invalid_max_dimensions.IsValid());
+ EXPECT_FALSE(invalid_min_bit_rate.IsValid());
+ EXPECT_FALSE(invalid_max_bit_rate.IsValid());
+ EXPECT_FALSE(invalid_max_delay.IsValid());
+}
+
+TEST(AnswerMessagesTest, ConstraintsIsValid) {
+ VideoConstraints invalid_video_constraints = kValidVideoConstraints;
+ invalid_video_constraints.max_pixels_per_second = 0;
+
+ AudioConstraints invalid_audio_constraints = kValidAudioConstraints;
+ invalid_audio_constraints.max_bit_rate = 0;
+
+ const Constraints valid{kValidAudioConstraints, kValidVideoConstraints};
+ const Constraints invalid_audio{kValidAudioConstraints,
+ invalid_video_constraints};
+ const Constraints invalid_video{invalid_audio_constraints,
+ kValidVideoConstraints};
+
+ EXPECT_TRUE(valid.IsValid());
+ EXPECT_FALSE(invalid_audio.IsValid());
+ EXPECT_FALSE(invalid_video.IsValid());
+}
+
+TEST(AnswerMessagesTest, AspectRatioIsValid) {
+ constexpr AspectRatio kValid{16, 9};
+ constexpr AspectRatio kInvalidWidth{0, 9};
+ constexpr AspectRatio kInvalidHeight{16, 0};
+
+ EXPECT_TRUE(kValid.IsValid());
+ EXPECT_FALSE(kInvalidWidth.IsValid());
+ EXPECT_FALSE(kInvalidHeight.IsValid());
+}
+
+TEST(AnswerMessagesTest, AspectRatioParseAndValidate) {
+ const Json::Value kValid = "16:9";
+ const Json::Value kWrongDelimiter = "16-9";
+ const Json::Value kTooManyFields = "16:9:3";
+ const Json::Value kTooFewFields = "1:";
+ const Json::Value kNoDelimiter = "12345";
+ const Json::Value kNegativeWidth = "-123:2345";
+ const Json::Value kNegativeHeight = "22:-7";
+ const Json::Value kNegativeBoth = "22:-7";
+ const Json::Value kNonNumberWidth = "twenty2#:9";
+ const Json::Value kNonNumberHeight = "2:thirty";
+ const Json::Value kZeroWidth = "0:9";
+ const Json::Value kZeroHeight = "16:0";
+
+ AspectRatio out;
+ EXPECT_TRUE(AspectRatio::ParseAndValidate(kValid, &out));
+ EXPECT_EQ(out.width, 16);
+ EXPECT_EQ(out.height, 9);
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kWrongDelimiter, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kTooManyFields, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kTooFewFields, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kWrongDelimiter, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kNoDelimiter, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kNegativeWidth, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kNegativeHeight, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kNegativeBoth, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kNonNumberWidth, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kNonNumberHeight, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kZeroWidth, &out));
+ EXPECT_FALSE(AspectRatio::ParseAndValidate(kZeroHeight, &out));
+}
+
+TEST(AnswerMessagesTest, DisplayDescriptionParseAndValidate) {
+ Json::Value valid_scaling;
+ valid_scaling["scaling"] = "receiver";
+ Json::Value invalid_scaling;
+ invalid_scaling["scaling"] = "embedder";
+ Json::Value invalid_scaling_valid_ratio;
+ invalid_scaling_valid_ratio["scaling"] = "embedder";
+ invalid_scaling_valid_ratio["aspectRatio"] = "16:9";
+
+ Json::Value dimensions;
+ dimensions["width"] = 1920;
+ dimensions["height"] = 1080;
+ dimensions["frameRate"] = "30";
+ Json::Value valid_dimensions;
+ valid_dimensions["dimensions"] = dimensions;
+
+ Json::Value dimensions_invalid = dimensions;
+ dimensions_invalid["frameRate"] = "infinity";
+ Json::Value invalid_dimensions;
+ invalid_dimensions["dimensions"] = dimensions_invalid;
+
+ DisplayDescription out;
+ ASSERT_TRUE(DisplayDescription::ParseAndValidate(valid_scaling, &out));
+ ASSERT_TRUE(out.aspect_ratio_constraint.has_value());
+ EXPECT_EQ(out.aspect_ratio_constraint.value(),
+ AspectRatioConstraint::kVariable);
+
+ EXPECT_FALSE(DisplayDescription::ParseAndValidate(invalid_scaling, &out));
+ EXPECT_TRUE(
+ DisplayDescription::ParseAndValidate(invalid_scaling_valid_ratio, &out));
+
+ ASSERT_TRUE(DisplayDescription::ParseAndValidate(valid_dimensions, &out));
+ ASSERT_TRUE(out.dimensions.has_value());
+ EXPECT_EQ(1920, out.dimensions->width);
+ EXPECT_EQ(1080, out.dimensions->height);
+ EXPECT_EQ((SimpleFraction{30, 1}), out.dimensions->frame_rate);
+
+ EXPECT_FALSE(DisplayDescription::ParseAndValidate(invalid_dimensions, &out));
}
+TEST(AnswerMessagesTest, DisplayDescriptionIsValid) {
+ const DisplayDescription kInvalidEmptyDescription{
+ absl::optional<Dimensions>{}, absl::optional<AspectRatio>{},
+ absl::optional<AspectRatioConstraint>{}};
+
+ DisplayDescription has_valid_dimensions = kInvalidEmptyDescription;
+ has_valid_dimensions.dimensions =
+ absl::optional<Dimensions>(kValidDimensions);
+
+ DisplayDescription has_invalid_dimensions = kInvalidEmptyDescription;
+ has_invalid_dimensions.dimensions =
+ absl::optional<Dimensions>(kValidDimensions);
+ has_invalid_dimensions.dimensions->width = 0;
+
+ DisplayDescription has_aspect_ratio = kInvalidEmptyDescription;
+ has_aspect_ratio.aspect_ratio =
+ absl::optional<AspectRatio>{AspectRatio{16, 9}};
+
+ DisplayDescription has_invalid_aspect_ratio = kInvalidEmptyDescription;
+ has_invalid_aspect_ratio.aspect_ratio =
+ absl::optional<AspectRatio>{AspectRatio{0, 20}};
+
+ DisplayDescription has_aspect_ratio_constraint = kInvalidEmptyDescription;
+ has_aspect_ratio_constraint.aspect_ratio_constraint =
+ absl::optional<AspectRatioConstraint>(AspectRatioConstraint::kFixed);
+
+ DisplayDescription has_constraint_and_dimensions =
+ has_aspect_ratio_constraint;
+ has_constraint_and_dimensions.dimensions =
+ absl::optional<Dimensions>(kValidDimensions);
+
+ EXPECT_FALSE(kInvalidEmptyDescription.IsValid());
+ EXPECT_TRUE(has_valid_dimensions.IsValid());
+ EXPECT_FALSE(has_invalid_dimensions.IsValid());
+ EXPECT_TRUE(has_aspect_ratio.IsValid());
+ EXPECT_FALSE(has_invalid_aspect_ratio.IsValid());
+ EXPECT_FALSE(has_aspect_ratio_constraint.IsValid());
+ EXPECT_TRUE(has_constraint_and_dimensions.IsValid());
+}
+
+// Instead of being tested here, Answer's IsValid is checked in all other
+// relevant tests.
+
} // namespace cast
} // namespace openscreen
diff --git a/cast/streaming/message_util.h b/cast/streaming/message_util.h
deleted file mode 100644
index c986f0ca..00000000
--- a/cast/streaming/message_util.h
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CAST_STREAMING_MESSAGE_UTIL_H_
-#define CAST_STREAMING_MESSAGE_UTIL_H_
-
-#include <vector>
-
-#include "absl/strings/string_view.h"
-#include "json/value.h"
-#include "platform/base/error.h"
-
-// This file contains helper methods that are used by both answer and offer
-// messages, but should not be publicly exposed/consumed.
-namespace openscreen {
-namespace cast {
-
-inline Error CreateParseError(const std::string& type) {
- return Error(Error::Code::kJsonParseError, "Failed to parse " + type);
-}
-
-inline Error CreateParameterError(const std::string& type) {
- return Error(Error::Code::kParameterInvalid, "Invalid parameter: " + type);
-}
-
-inline ErrorOr<bool> ParseBool(const Json::Value& parent,
- const std::string& field) {
- const Json::Value& value = parent[field];
- if (!value.isBool()) {
- return CreateParseError("bool field " + field);
- }
- return value.asBool();
-}
-
-inline ErrorOr<int> ParseInt(const Json::Value& parent,
- const std::string& field) {
- const Json::Value& value = parent[field];
- if (!value.isInt()) {
- return CreateParseError("integer field: " + field);
- }
- return value.asInt();
-}
-
-inline ErrorOr<uint32_t> ParseUint(const Json::Value& parent,
- const std::string& field) {
- const Json::Value& value = parent[field];
- if (!value.isUInt()) {
- return CreateParseError("unsigned integer field: " + field);
- }
- return value.asUInt();
-}
-
-inline ErrorOr<std::string> ParseString(const Json::Value& parent,
- const std::string& field) {
- const Json::Value& value = parent[field];
- if (!value.isString()) {
- return CreateParseError("string field: " + field);
- }
- return value.asString();
-}
-
-} // namespace cast
-} // namespace openscreen
-
-#endif // CAST_STREAMING_MESSAGE_UTIL_H_
diff --git a/cast/streaming/offer_messages.cc b/cast/streaming/offer_messages.cc
index caa6babf..ff6845a3 100644
--- a/cast/streaming/offer_messages.cc
+++ b/cast/streaming/offer_messages.cc
@@ -6,6 +6,7 @@
#include <inttypes.h>
+#include <limits>
#include <string>
#include <utility>
@@ -13,10 +14,10 @@
#include "absl/strings/numbers.h"
#include "absl/strings/str_split.h"
#include "cast/streaming/constants.h"
-#include "cast/streaming/message_util.h"
#include "cast/streaming/receiver_session.h"
#include "platform/base/error.h"
#include "util/big_endian.h"
+#include "util/json/json_helpers.h"
#include "util/json/json_serialization.h"
#include "util/osp_logging.h"
#include "util/stringprintf.h"
@@ -33,7 +34,7 @@ constexpr char kStreamType[] = "type";
ErrorOr<RtpPayloadType> ParseRtpPayloadType(const Json::Value& parent,
const std::string& field) {
- auto t = ParseInt(parent, field);
+ auto t = json::ParseInt(parent, field);
if (!t) {
return t.error();
}
@@ -49,14 +50,14 @@ ErrorOr<RtpPayloadType> ParseRtpPayloadType(const Json::Value& parent,
ErrorOr<int> ParseRtpTimebase(const Json::Value& parent,
const std::string& field) {
- auto error_or_raw = ParseString(parent, field);
+ auto error_or_raw = json::ParseString(parent, field);
if (!error_or_raw) {
return error_or_raw.error();
}
const auto fraction = SimpleFraction::FromString(error_or_raw.value());
if (fraction.is_error() || !fraction.value().is_positive()) {
- return CreateParseError("RTP timebase");
+ return json::CreateParseError("RTP timebase");
}
// The spec demands a leading 1, so this isn't really a fraction.
OSP_DCHECK(fraction.value().numerator == 1);
@@ -71,7 +72,7 @@ constexpr int kAesStringLength = kAesBytesSize * kHexDigitsPerByte;
ErrorOr<std::array<uint8_t, kAesBytesSize>> ParseAesHexBytes(
const Json::Value& parent,
const std::string& field) {
- auto hex_string = ParseString(parent, field);
+ auto hex_string = json::ParseString(parent, field);
if (!hex_string) {
return hex_string.error();
}
@@ -91,24 +92,24 @@ ErrorOr<std::array<uint8_t, kAesBytesSize>> ParseAesHexBytes(
WriteBigEndian(quads[1], bytes.data() + 8);
return bytes;
}
- return CreateParseError("AES hex string bytes");
+ return json::CreateParseError("AES hex string bytes");
}
ErrorOr<Stream> ParseStream(const Json::Value& value, Stream::Type type) {
- auto index = ParseInt(value, "index");
+ auto index = json::ParseInt(value, "index");
if (!index) {
return index.error();
}
// If channel is omitted, the default value is used later.
- auto channels = ParseInt(value, "channels");
+ auto channels = json::ParseInt(value, "channels");
if (channels.is_value() && channels.value() <= 0) {
- return CreateParameterError("channel");
+ return json::CreateParameterError("channel");
}
- auto codec_name = ParseString(value, "codecName");
+ auto codec_name = json::ParseString(value, "codecName");
if (!codec_name) {
return codec_name.error();
}
- auto rtp_profile = ParseString(value, "rtpProfile");
+ auto rtp_profile = json::ParseString(value, "rtpProfile");
if (!rtp_profile) {
return rtp_profile.error();
}
@@ -116,7 +117,7 @@ ErrorOr<Stream> ParseStream(const Json::Value& value, Stream::Type type) {
if (!rtp_payload_type) {
return rtp_payload_type.error();
}
- auto ssrc = ParseUint(value, "ssrc");
+ auto ssrc = json::ParseUint(value, "ssrc");
if (!ssrc) {
return ssrc.error();
}
@@ -133,19 +134,19 @@ ErrorOr<Stream> ParseStream(const Json::Value& value, Stream::Type type) {
return rtp_timebase.error();
}
- auto target_delay = ParseInt(value, "targetDelay");
+ auto target_delay = json::ParseInt(value, "targetDelay");
std::chrono::milliseconds target_delay_ms = kDefaultTargetPlayoutDelay;
if (target_delay) {
auto d = std::chrono::milliseconds(target_delay.value());
if (d >= kMinTargetPlayoutDelay && d <= kMaxTargetPlayoutDelay) {
target_delay_ms = d;
} else {
- return CreateParameterError("target delay");
+ return json::CreateParameterError("target delay");
}
}
- auto receiver_rtcp_event_log = ParseBool(value, "receiverRtcpEventLog");
- auto receiver_rtcp_dscp = ParseString(value, "receiverRtcpDscp");
+ auto receiver_rtcp_event_log = json::ParseBool(value, "receiverRtcpEventLog");
+ auto receiver_rtcp_dscp = json::ParseString(value, "receiverRtcpDscp");
return Stream{index.value(),
type,
channels.value(type == Stream::Type::kAudioSource
@@ -167,28 +168,28 @@ ErrorOr<AudioStream> ParseAudioStream(const Json::Value& value) {
if (!stream) {
return stream.error();
}
- auto bit_rate = ParseInt(value, "bitRate");
+ auto bit_rate = json::ParseInt(value, "bitRate");
if (!bit_rate) {
return bit_rate.error();
}
// A bit rate of 0 is valid for some codec types, so we don't enforce here.
if (bit_rate.value() < 0) {
- return CreateParameterError("bit rate");
+ return json::CreateParameterError("bit rate");
}
return AudioStream{stream.value(), bit_rate.value()};
}
ErrorOr<Resolution> ParseResolution(const Json::Value& value) {
- auto width = ParseInt(value, "width");
+ auto width = json::ParseInt(value, "width");
if (!width) {
return width.error();
}
- auto height = ParseInt(value, "height");
+ auto height = json::ParseInt(value, "height");
if (!height) {
return height.error();
}
if (width.value() <= 0 || height.value() <= 0) {
- return CreateParameterError("resolution");
+ return json::CreateParameterError("resolution");
}
return Resolution{width.value(), height.value()};
}
@@ -223,7 +224,7 @@ ErrorOr<VideoStream> ParseVideoStream(const Json::Value& value) {
return resolutions.error();
}
- auto raw_max_frame_rate = ParseString(value, "maxFrameRate");
+ auto raw_max_frame_rate = json::ParseString(value, "maxFrameRate");
SimpleFraction max_frame_rate{kDefaultMaxFrameRate, 1};
if (raw_max_frame_rate.is_value()) {
auto parsed = SimpleFraction::FromString(raw_max_frame_rate.value());
@@ -232,11 +233,11 @@ ErrorOr<VideoStream> ParseVideoStream(const Json::Value& value) {
}
}
- auto profile = ParseString(value, "profile");
- auto protection = ParseString(value, "protection");
- auto max_bit_rate = ParseInt(value, "maxBitRate");
- auto level = ParseString(value, "level");
- auto error_recovery_mode = ParseString(value, "errorRecoveryMode");
+ auto profile = json::ParseString(value, "profile");
+ auto protection = json::ParseString(value, "protection");
+ auto max_bit_rate = json::ParseInt(value, "maxBitRate");
+ auto level = json::ParseString(value, "level");
+ auto error_recovery_mode = json::ParseString(value, "errorRecoveryMode");
return VideoStream{stream.value(),
max_frame_rate,
max_bit_rate.value(4 << 20),
@@ -276,7 +277,7 @@ ErrorOr<Json::Value> Stream::ToJson() const {
target_delay.count() <= 0 ||
target_delay.count() > std::numeric_limits<int>::max() ||
rtp_timebase < 1) {
- return CreateParameterError("Stream");
+ return json::CreateParameterError("Stream");
}
Json::Value root;
@@ -315,7 +316,7 @@ std::string CastMode::ToString() const {
ErrorOr<Json::Value> AudioStream::ToJson() const {
// A bit rate of 0 is valid for some codec types, so we don't enforce here.
if (bit_rate < 0) {
- return CreateParameterError("AudioStream");
+ return json::CreateParameterError("AudioStream");
}
auto error_or_stream = stream.ToJson();
@@ -329,7 +330,7 @@ ErrorOr<Json::Value> AudioStream::ToJson() const {
ErrorOr<Json::Value> Resolution::ToJson() const {
if (width <= 0 || height <= 0) {
- return CreateParameterError("Resolution");
+ return json::CreateParameterError("Resolution");
}
Json::Value root;
@@ -340,7 +341,7 @@ ErrorOr<Json::Value> Resolution::ToJson() const {
ErrorOr<Json::Value> VideoStream::ToJson() const {
if (max_bit_rate <= 0 || !max_frame_rate.is_positive()) {
- return CreateParameterError("VideoStream");
+ return json::CreateParameterError("VideoStream");
}
auto error_or_stream = stream.ToJson();
@@ -372,18 +373,18 @@ ErrorOr<Json::Value> VideoStream::ToJson() const {
ErrorOr<Offer> Offer::Parse(const Json::Value& root) {
CastMode cast_mode = CastMode::Parse(root["castMode"].asString());
- const ErrorOr<bool> get_status = ParseBool(root, "receiverGetStatus");
+ const ErrorOr<bool> get_status = json::ParseBool(root, "receiverGetStatus");
Json::Value supported_streams = root[kSupportedStreams];
if (!supported_streams.isArray()) {
- return CreateParseError("supported streams in offer");
+ return json::CreateParseError("supported streams in offer");
}
std::vector<AudioStream> audio_streams;
std::vector<VideoStream> video_streams;
for (Json::ArrayIndex i = 0; i < supported_streams.size(); ++i) {
const Json::Value& fields = supported_streams[i];
- auto type = ParseString(fields, kStreamType);
+ auto type = json::ParseString(fields, kStreamType);
if (!type) {
return type.error();
}
diff --git a/cast/streaming/receiver_session.cc b/cast/streaming/receiver_session.cc
index 5d2e74bd..467fd27d 100644
--- a/cast/streaming/receiver_session.cc
+++ b/cast/streaming/receiver_session.cc
@@ -12,14 +12,15 @@
#include "absl/strings/numbers.h"
#include "cast/streaming/environment.h"
#include "cast/streaming/message_port.h"
-#include "cast/streaming/message_util.h"
#include "cast/streaming/offer_messages.h"
#include "cast/streaming/receiver.h"
+#include "util/json/json_helpers.h"
#include "util/osp_logging.h"
namespace openscreen {
namespace cast {
+/// NOTE: Constants here are all taken from the Cast V2: Mirroring Control
// JSON message field values specific to the Receiver Session.
static constexpr char kMessageTypeOffer[] = "OFFER";
@@ -28,6 +29,20 @@ static constexpr char kOfferMessageBody[] = "offer";
static constexpr char kKeyType[] = "type";
static constexpr char kSequenceNumber[] = "seqNum";
+/// Protocol specification: http://goto.google.com/mirroring-control-protocol
+// TODO(jophba): document the protocol in a public repository.
+static constexpr char kMessageKeyType[] = "type";
+static constexpr char kMessageTypeAnswer[] = "ANSWER";
+
+/// ANSWER message fields.
+static constexpr char kAnswerMessageBody[] = "answer";
+static constexpr char kResult[] = "result";
+static constexpr char kResultOk[] = "ok";
+static constexpr char kResultError[] = "error";
+static constexpr char kErrorMessageBody[] = "error";
+static constexpr char kErrorCode[] = "code";
+static constexpr char kErrorDescription[] = "description";
+
// Using statements for constructor readability.
using Preferences = ReceiverSession::Preferences;
using ConfiguredReceivers = ReceiverSession::ConfiguredReceivers;
@@ -76,7 +91,30 @@ const Stream* SelectStream(const std::vector<Codec>& preferred_codecs,
}
return nullptr;
}
+// Helper method that creates an invalid Answer response.
+Json::Value CreateInvalidAnswerMessage(Error error) {
+ Json::Value message_root;
+ message_root[kMessageKeyType] = kMessageTypeAnswer;
+ message_root[kResult] = kResultError;
+ message_root[kErrorMessageBody][kErrorCode] = static_cast<int>(error.code());
+ message_root[kErrorMessageBody][kErrorDescription] = error.message();
+
+ return message_root;
+}
+// Helper method that creates an Answer response. May be valid or invalid.
+Json::Value CreateAnswerMessage(const Answer& answer) {
+ if (!answer.IsValid()) {
+ return CreateInvalidAnswerMessage(Error(Error::Code::kParameterInvalid,
+ "Answer struct in invalid state"));
+ }
+
+ Json::Value message_root;
+ message_root[kMessageKeyType] = kMessageTypeAnswer;
+ message_root[kAnswerMessageBody] = answer.ToJson();
+ message_root[kResult] = kResultOk;
+ return message_root;
+}
} // namespace
Preferences::Preferences() = default;
@@ -129,21 +167,22 @@ void ReceiverSession::OnMessage(absl::string_view sender_id,
}
// TODO(jophba): add sender connected/disconnected messaging.
- auto sequence_number = ParseInt(message_json.value(), kSequenceNumber);
- if (!sequence_number) {
+ int sequence_number;
+ if (!json::ParseAndValidateInt(message_json.value()[kSequenceNumber],
+ &sequence_number)) {
OSP_LOG_WARN << "Invalid message sequence number";
return;
}
- auto key_or_error = ParseString(message_json.value(), kKeyType);
- if (!key_or_error) {
+ std::string key;
+ if (!json::ParseAndValidateString(message_json.value()[kKeyType], &key)) {
OSP_LOG_WARN << "Invalid message key";
return;
}
Message parsed_message{sender_id.data(), message_namespace.data(),
- sequence_number.value()};
- if (key_or_error.value() == kMessageTypeOffer) {
+ sequence_number};
+ if (key == kMessageTypeOffer) {
parsed_message.body = std::move(message_json.value()[kOfferMessageBody]);
if (parsed_message.body.isNull()) {
OSP_LOG_WARN << "Invalid message offer body";
@@ -180,7 +219,6 @@ void ReceiverSession::OnOffer(Message* message) {
SelectStream(preferences_.video_codecs, offer.value().video_streams);
}
- cast_mode_ = offer.value().cast_mode;
auto receivers =
TrySpawningReceivers(selected_audio_stream, selected_video_stream);
if (receivers) {
@@ -188,9 +226,9 @@ void ReceiverSession::OnOffer(Message* message) {
ConstructAnswer(message, selected_audio_stream, selected_video_stream);
client_->OnNegotiated(this, std::move(receivers.value()));
- message->body = answer.ToAnswerMessage();
+ message->body = CreateAnswerMessage(answer);
} else {
- message->body = CreateInvalidAnswer(receivers.error());
+ message->body = CreateInvalidAnswerMessage(receivers.error());
}
SendMessage(message);
@@ -266,20 +304,20 @@ Answer ReceiverSession::ConstructAnswer(
absl::optional<Constraints> constraints;
if (preferences_.constraints) {
- constraints = *preferences_.constraints;
+ constraints = absl::optional<Constraints>(*preferences_.constraints);
}
absl::optional<DisplayDescription> display;
if (preferences_.display_description) {
- display = *preferences_.display_description;
+ display =
+ absl::optional<DisplayDescription>(*preferences_.display_description);
}
- return Answer{cast_mode_,
- environment_->GetBoundLocalEndpoint().port,
+ return Answer{environment_->GetBoundLocalEndpoint().port,
std::move(stream_indexes),
std::move(stream_ssrcs),
- constraints,
- display,
+ std::move(constraints),
+ std::move(display),
std::vector<int>{}, // receiver_rtcp_event_log
std::vector<int>{}, // receiver_rtcp_dscp
supports_wifi_status_reporting_};
diff --git a/cast/streaming/receiver_session.h b/cast/streaming/receiver_session.h
index 44cf864b..a4053356 100644
--- a/cast/streaming/receiver_session.h
+++ b/cast/streaming/receiver_session.h
@@ -10,6 +10,11 @@
#include <utility>
#include <vector>
+// TODO(jophba): remove public abseil dependencies. Will require modifying
+// either Optional or ConfiguredReceivers, as the compiler currently has an
+// error.
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
#include "cast/streaming/answer_messages.h"
#include "cast/streaming/message_port.h"
#include "cast/streaming/offer_messages.h"
@@ -52,6 +57,8 @@ class ReceiverSession final : public MessagePort::Client {
// If the receiver is audio- or video-only, either of the receivers
// may be nullptr. However, in the majority of cases they will be populated.
+ // TODO(jophba): remove AudioStream, VideoStream from public API.
+ // TODO(jophba): remove absl::optional from public API.
absl::optional<ConfiguredReceiver<AudioStream>> audio;
absl::optional<ConfiguredReceiver<VideoStream>> video;
};
@@ -153,7 +160,6 @@ class ReceiverSession final : public MessagePort::Client {
MessagePort* const message_port_;
const Preferences preferences_;
- CastMode cast_mode_;
bool supports_wifi_status_reporting_ = false;
ReceiverPacketRouter packet_router_;
diff --git a/cast/streaming/receiver_session_unittest.cc b/cast/streaming/receiver_session_unittest.cc
index afedde2f..697a61e4 100644
--- a/cast/streaming/receiver_session_unittest.cc
+++ b/cast/streaming/receiver_session_unittest.cc
@@ -12,6 +12,7 @@
#include "platform/base/ip_address.h"
#include "platform/test/fake_clock.h"
#include "platform/test/fake_task_runner.h"
+#include "util/chrono_helpers.h"
using ::testing::_;
using ::testing::Invoke;
@@ -314,7 +315,6 @@ TEST_F(ReceiverSessionTest, CanNegotiateWithDefaultPreferences) {
// Spot check the answer body fields. We have more in depth testing
// of answer behavior in answer_messages_unittest, but here we can
// ensure that the ReceiverSession properly configured the answer.
- EXPECT_EQ("mirroring", answer_body["castMode"].asString());
EXPECT_EQ(1337, answer_body["sendIndexes"][0].asInt());
EXPECT_EQ(31338, answer_body["sendIndexes"][1].asInt());
EXPECT_LT(0, answer_body["udpPort"].asInt());
@@ -361,15 +361,19 @@ TEST_F(ReceiverSessionTest, CanNegotiateWithCustomConstraints) {
auto message_port = std::make_unique<SimpleMessagePort>();
StrictMock<FakeClient> client;
- auto constraints = std::unique_ptr<Constraints>{new Constraints{
+ auto constraints = std::make_unique<Constraints>(Constraints{
AudioConstraints{1, 2, 3, 4},
- VideoConstraints{3.14159, Dimensions{320, 240, SimpleFraction{24, 1}},
+
+ VideoConstraints{3.14159,
+ absl::optional<Dimensions>(
+ Dimensions{320, 240, SimpleFraction{24, 1}}),
Dimensions{1920, 1080, SimpleFraction{144, 1}}, 3000,
- 90000000, std::chrono::milliseconds(1000)}}};
+ 90000000, milliseconds(1000)}});
- auto display = std::unique_ptr<DisplayDescription>{new DisplayDescription{
- Dimensions{640, 480, SimpleFraction{60, 1}}, AspectRatio{16, 9},
- AspectRatioConstraint::kFixed}};
+ auto display = std::make_unique<DisplayDescription>(DisplayDescription{
+ absl::optional<Dimensions>(Dimensions{640, 480, SimpleFraction{60, 1}}),
+ absl::optional<AspectRatio>(AspectRatio{16, 9}),
+ absl::optional<AspectRatioConstraint>(AspectRatioConstraint::kFixed)});
auto environment = MakeEnvironment();
ReceiverSession session(
diff --git a/cast/streaming/session_config.cc b/cast/streaming/session_config.cc
index 65117029..f6f4aade 100644
--- a/cast/streaming/session_config.cc
+++ b/cast/streaming/session_config.cc
@@ -4,6 +4,8 @@
#include "cast/streaming/session_config.h"
+#include <utility>
+
namespace openscreen {
namespace cast {
@@ -19,8 +21,15 @@ SessionConfig::SessionConfig(Ssrc sender_ssrc,
rtp_timebase(rtp_timebase),
channels(channels),
target_playout_delay(target_playout_delay),
- aes_secret_key(aes_secret_key),
- aes_iv_mask(aes_iv_mask) {}
+ aes_secret_key(std::move(aes_secret_key)),
+ aes_iv_mask(std::move(aes_iv_mask)) {}
+
+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;
} // namespace cast
} // namespace openscreen
diff --git a/cast/streaming/session_config.h b/cast/streaming/session_config.h
index f1fc0299..cf87667e 100644
--- a/cast/streaming/session_config.h
+++ b/cast/streaming/session_config.h
@@ -25,11 +25,11 @@ struct SessionConfig final {
std::chrono::milliseconds target_playout_delay,
std::array<uint8_t, 16> aes_secret_key,
std::array<uint8_t, 16> aes_iv_mask);
- SessionConfig(const SessionConfig&) = default;
- SessionConfig(SessionConfig&&) noexcept = default;
- SessionConfig& operator=(const SessionConfig&) = default;
- SessionConfig& operator=(SessionConfig&&) noexcept = default;
- ~SessionConfig() = default;
+ SessionConfig(const SessionConfig& other);
+ SessionConfig(SessionConfig&& other) noexcept;
+ SessionConfig& operator=(const SessionConfig& other);
+ SessionConfig& operator=(SessionConfig&& other) noexcept;
+ ~SessionConfig();
// The sender and receiver's SSRC identifiers. Note: SSRC identifiers
// are defined as unsigned 32 bit integers here: