aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Bayles <jophba@chromium.org>2020-06-05 14:14:54 -0700
committerCommit Bot <commit-bot@chromium.org>2020-06-05 23:27:56 +0000
commit3afe77d7e243959f39568a58c8bff451eea87cbd (patch)
treee6986fedf34a47d1e8946b7b18b16175311c007e
parent977bb0c2f35a96500ecbd09b236ab39b6ddc55fd (diff)
downloadopenscreen-3afe77d7e243959f39568a58c8bff451eea87cbd.tar.gz
Implement Answer parsing
This patch adds Answer parsing and testing, similar to how Offer messages are currently parsed. As part of this work, the following improvements are also included: 1. To avoid Abseil usage in public APIs, a new Optional type with unit tests is included. 2. message_util.h helpers have been greatly expanded, moved to util/json/parsing_helpers.h, and unit tests added. 3. SimpleFraction has been moved from util/ to platform/base/, so it can be properly used in public APIs. 4. SessionConfig has been cleaned up to follow coding style guidelines. 5. ANSWER message creation (that encapsulates the Answer struct) has been moved to the ReceiverSession. Bug: b/152633271, b/158030843 Change-Id: I59c20a140a5174d45378fb9b647ccbe5e6d23d1b Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/2219571 Commit-Queue: Jordan Bayles <jophba@chromium.org> Reviewed-by: mark a. foltz <mfoltz@chromium.org> Reviewed-by: Ryan Keane <rwkeane@google.com>
-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
-rw-r--r--util/BUILD.gn2
-rw-r--r--util/json/json_helpers.h209
-rw-r--r--util/json/json_helpers_unittest.cc209
-rw-r--r--util/simple_fraction.cc62
-rw-r--r--util/simple_fraction.h3
-rw-r--r--util/simple_fraction_unittest.cc16
16 files changed, 1481 insertions, 334 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:
diff --git a/util/BUILD.gn b/util/BUILD.gn
index 52f18bd8..000b3ea6 100644
--- a/util/BUILD.gn
+++ b/util/BUILD.gn
@@ -38,6 +38,7 @@ source_set("util") {
"crypto/sha2.h",
"hashing.h",
"integer_division.h",
+ "json/json_helpers.h",
"json/json_serialization.cc",
"json/json_serialization.h",
"json/json_value.cc",
@@ -87,6 +88,7 @@ source_set("unittests") {
"crypto/secure_hash_unittest.cc",
"crypto/sha2_unittest.cc",
"integer_division_unittest.cc",
+ "json/json_helpers_unittest.cc",
"json/json_serialization_unittest.cc",
"json/json_value_unittest.cc",
"operation_loop_unittest.cc",
diff --git a/util/json/json_helpers.h b/util/json/json_helpers.h
new file mode 100644
index 00000000..a4c43479
--- /dev/null
+++ b/util/json/json_helpers.h
@@ -0,0 +1,209 @@
+// 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 UTIL_JSON_JSON_HELPERS_H_
+#define UTIL_JSON_JSON_HELPERS_H_
+
+#include <chrono>
+#include <functional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "json/value.h"
+#include "platform/base/error.h"
+#include "util/chrono_helpers.h"
+#include "util/simple_fraction.h"
+
+// This file contains helper methods for parsing JSON, in an attempt to
+// reduce boilerplate code when working with JsonCpp.
+namespace openscreen {
+namespace json {
+
+// TODO(jophba): remove these methods after refactoring offer messaging.
+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();
+}
+
+// TODO(jophba): offer messaging should use these methods instead.
+inline bool ParseBool(const Json::Value& value, bool* out) {
+ if (!value.isBool()) {
+ return false;
+ }
+ *out = value.asBool();
+ return true;
+}
+
+// A general note about parsing primitives. "Validation" in this context
+// generally means ensuring that the values are non-negative. There are
+// currently no cases in our usage of JSON strings where we accept negative
+// values. If this changes in the future, care must be taken to ensure
+// that we don't break anything in existing code.
+inline bool ParseAndValidateDouble(const Json::Value& value, double* out) {
+ if (!value.isDouble()) {
+ return false;
+ }
+ const double d = value.asDouble();
+ if (d < 0) {
+ return false;
+ }
+ *out = d;
+ return true;
+}
+
+inline bool ParseAndValidateInt(const Json::Value& value, int* out) {
+ if (!value.isInt()) {
+ return false;
+ }
+ int i = value.asInt();
+ if (i < 0) {
+ return false;
+ }
+ *out = i;
+ return true;
+}
+
+inline bool ParseAndValidateUint(const Json::Value& value, uint32_t* out) {
+ if (!value.isUInt()) {
+ return false;
+ }
+ *out = value.asUInt();
+ return true;
+}
+
+inline bool ParseAndValidateString(const Json::Value& value, std::string* out) {
+ if (!value.isString()) {
+ return false;
+ }
+ *out = value.asString();
+ return true;
+}
+
+// We want to be more robust when we parse fractions then just
+// allowing strings, this will parse numeral values such as
+// value: 50 as well as value: "50" and value: "100/2".
+inline bool ParseAndValidateSimpleFraction(const Json::Value& value,
+ SimpleFraction* out) {
+ if (value.isInt()) {
+ int parsed = value.asInt();
+ if (parsed < 0) {
+ return false;
+ }
+ *out = SimpleFraction{parsed, 1};
+ return true;
+ }
+
+ if (value.isString()) {
+ auto fraction_or_error = SimpleFraction::FromString(value.asString());
+ if (!fraction_or_error) {
+ return false;
+ }
+
+ if (!fraction_or_error.value().is_positive() ||
+ !fraction_or_error.value().is_defined()) {
+ return false;
+ }
+ *out = std::move(fraction_or_error.value());
+ return true;
+ }
+ return false;
+}
+
+inline bool ParseAndValidateMilliseconds(const Json::Value& value,
+ milliseconds* out) {
+ int out_ms;
+ if (!ParseAndValidateInt(value, &out_ms) || out_ms < 0) {
+ return false;
+ }
+ *out = milliseconds(out_ms);
+ return true;
+}
+
+template <typename T>
+using Parser = std::function<bool(const Json::Value&, T*)>;
+
+// NOTE: array parsing methods reset the output vector to an empty vector in
+// any error case. This is especially useful for optional arrays.
+template <typename T>
+bool ParseAndValidateArray(const Json::Value& value,
+ Parser<T> parser,
+ std::vector<T>* out) {
+ out->clear();
+ if (!value.isArray() || value.empty()) {
+ return false;
+ }
+
+ out->reserve(value.size());
+ for (Json::ArrayIndex i = 0; i < value.size(); ++i) {
+ T v;
+ if (!parser(value[i], &v)) {
+ out->clear();
+ return false;
+ }
+ out->push_back(v);
+ }
+
+ return true;
+}
+
+inline bool ParseAndValidateIntArray(const Json::Value& value,
+ std::vector<int>* out) {
+ return ParseAndValidateArray<int>(value, ParseAndValidateInt, out);
+}
+
+inline bool ParseAndValidateUintArray(const Json::Value& value,
+ std::vector<uint32_t>* out) {
+ return ParseAndValidateArray<uint32_t>(value, ParseAndValidateUint, out);
+}
+
+inline bool ParseAndValidateStringArray(const Json::Value& value,
+ std::vector<std::string>* out) {
+ return ParseAndValidateArray<std::string>(value, ParseAndValidateString, out);
+}
+
+} // namespace json
+} // namespace openscreen
+
+#endif // UTIL_JSON_JSON_HELPERS_H_
diff --git a/util/json/json_helpers_unittest.cc b/util/json/json_helpers_unittest.cc
new file mode 100644
index 00000000..fdac1897
--- /dev/null
+++ b/util/json/json_helpers_unittest.cc
@@ -0,0 +1,209 @@
+// Copyright 2020 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.
+
+#include "util/json/json_helpers.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "util/chrono_helpers.h"
+
+namespace openscreen {
+namespace json {
+namespace {
+
+using ::testing::ElementsAre;
+
+const Json::Value kNone;
+const Json::Value kEmptyString = "";
+const Json::Value kEmptyArray(Json::arrayValue);
+
+struct Dummy {
+ int value;
+
+ constexpr bool operator==(const Dummy& other) const {
+ return other.value == value;
+ }
+};
+
+bool ParseAndValidateDummy(const Json::Value& value, Dummy* out) {
+ int value_out;
+ if (!ParseAndValidateInt(value, &value_out)) {
+ return false;
+ }
+ *out = Dummy{value_out};
+ return true;
+}
+
+} // namespace
+
+TEST(ParsingHelpersTest, ParseAndValidateDouble) {
+ const Json::Value kValid = 13.37;
+ const Json::Value kNotDouble = "coffee beans";
+ const Json::Value kNegativeDouble = -4.2;
+ const Json::Value kZeroDouble = 0.0;
+
+ double out;
+ EXPECT_TRUE(ParseAndValidateDouble(kValid, &out));
+ EXPECT_DOUBLE_EQ(13.37, out);
+ EXPECT_TRUE(ParseAndValidateDouble(kZeroDouble, &out));
+ EXPECT_DOUBLE_EQ(0.0, out);
+ EXPECT_FALSE(ParseAndValidateDouble(kNotDouble, &out));
+ EXPECT_FALSE(ParseAndValidateDouble(kNegativeDouble, &out));
+ EXPECT_FALSE(ParseAndValidateDouble(kNone, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateInt) {
+ const Json::Value kValid = 1337;
+ const Json::Value kNotInt = "cold brew";
+ const Json::Value kNegativeInt = -42;
+ const Json::Value kZeroInt = 0;
+
+ int out;
+ EXPECT_TRUE(ParseAndValidateInt(kValid, &out));
+ EXPECT_EQ(1337, out);
+ EXPECT_TRUE(ParseAndValidateInt(kZeroInt, &out));
+ EXPECT_EQ(0, out);
+ EXPECT_FALSE(ParseAndValidateInt(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateInt(kNotInt, &out));
+ EXPECT_FALSE(ParseAndValidateInt(kNegativeInt, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateUint) {
+ const Json::Value kValid = 1337u;
+ const Json::Value kNotUint = "espresso";
+ const Json::Value kZeroUint = 0u;
+
+ uint32_t out;
+ EXPECT_TRUE(ParseAndValidateUint(kValid, &out));
+ EXPECT_EQ(1337u, out);
+ EXPECT_TRUE(ParseAndValidateUint(kZeroUint, &out));
+ EXPECT_EQ(0u, out);
+ EXPECT_FALSE(ParseAndValidateUint(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateUint(kNotUint, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateString) {
+ const Json::Value kValid = "macchiato";
+ const Json::Value kNotString = 42;
+
+ std::string out;
+ EXPECT_TRUE(ParseAndValidateString(kValid, &out));
+ EXPECT_EQ("macchiato", out);
+ EXPECT_TRUE(ParseAndValidateString(kEmptyString, &out));
+ EXPECT_EQ("", out);
+ EXPECT_FALSE(ParseAndValidateString(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateString(kNotString, &out));
+}
+
+// Simple fraction validity is tested extensively in its unit tests, so we
+// just check the major cases here.
+TEST(ParsingHelpersTest, ParseAndValidateSimpleFraction) {
+ const Json::Value kValid = "42/30";
+ const Json::Value kValidNumber = "42";
+ const Json::Value kUndefined = "5/0";
+ const Json::Value kNegative = "10/-2";
+ const Json::Value kInvalidNumber = "-1";
+ const Json::Value kNotSimpleFraction = "latte";
+
+ SimpleFraction out;
+ EXPECT_TRUE(ParseAndValidateSimpleFraction(kValid, &out));
+ EXPECT_EQ((SimpleFraction{42, 30}), out);
+ EXPECT_TRUE(ParseAndValidateSimpleFraction(kValidNumber, &out));
+ EXPECT_EQ((SimpleFraction{42, 1}), out);
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kUndefined, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kNegative, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kInvalidNumber, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kNotSimpleFraction, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateSimpleFraction(kEmptyString, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateMilliseconds) {
+ const Json::Value kValid = 1000;
+ const Json::Value kValidFloat = 500.0;
+ const Json::Value kNegativeNumber = -120;
+ const Json::Value kZeroNumber = 0;
+ const Json::Value kNotNumber = "affogato";
+
+ milliseconds out;
+ EXPECT_TRUE(ParseAndValidateMilliseconds(kValid, &out));
+ EXPECT_EQ(milliseconds(1000), out);
+ EXPECT_TRUE(ParseAndValidateMilliseconds(kValidFloat, &out));
+ EXPECT_EQ(milliseconds(500), out);
+ EXPECT_TRUE(ParseAndValidateMilliseconds(kZeroNumber, &out));
+ EXPECT_EQ(milliseconds(0), out);
+ EXPECT_FALSE(ParseAndValidateMilliseconds(kNone, &out));
+ EXPECT_FALSE(ParseAndValidateMilliseconds(kNegativeNumber, &out));
+ EXPECT_FALSE(ParseAndValidateMilliseconds(kNotNumber, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateArray) {
+ Json::Value valid_dummy_array;
+ valid_dummy_array[0] = 123;
+ valid_dummy_array[1] = 456;
+
+ Json::Value invalid_dummy_array;
+ invalid_dummy_array[0] = "iced coffee";
+ invalid_dummy_array[1] = 456;
+
+ std::vector<Dummy> out;
+ EXPECT_TRUE(ParseAndValidateArray<Dummy>(valid_dummy_array,
+ ParseAndValidateDummy, &out));
+ EXPECT_THAT(out, ElementsAre(Dummy{123}, Dummy{456}));
+ EXPECT_FALSE(ParseAndValidateArray<Dummy>(invalid_dummy_array,
+ ParseAndValidateDummy, &out));
+ EXPECT_FALSE(
+ ParseAndValidateArray<Dummy>(kEmptyArray, ParseAndValidateDummy, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateIntArray) {
+ Json::Value valid_int_array;
+ valid_int_array[0] = 123;
+ valid_int_array[1] = 456;
+
+ Json::Value invalid_int_array;
+ invalid_int_array[0] = "iced coffee";
+ invalid_int_array[1] = 456;
+
+ std::vector<int> out;
+ EXPECT_TRUE(ParseAndValidateIntArray(valid_int_array, &out));
+ EXPECT_THAT(out, ElementsAre(123, 456));
+ EXPECT_FALSE(ParseAndValidateIntArray(invalid_int_array, &out));
+ EXPECT_FALSE(ParseAndValidateIntArray(kEmptyArray, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateUintArray) {
+ Json::Value valid_uint_array;
+ valid_uint_array[0] = 123u;
+ valid_uint_array[1] = 456u;
+
+ Json::Value invalid_uint_array;
+ invalid_uint_array[0] = "breve";
+ invalid_uint_array[1] = 456u;
+
+ std::vector<uint32_t> out;
+ EXPECT_TRUE(ParseAndValidateUintArray(valid_uint_array, &out));
+ EXPECT_THAT(out, ElementsAre(123u, 456u));
+ EXPECT_FALSE(ParseAndValidateUintArray(invalid_uint_array, &out));
+ EXPECT_FALSE(ParseAndValidateUintArray(kEmptyArray, &out));
+}
+
+TEST(ParsingHelpersTest, ParseAndValidateStringArray) {
+ Json::Value valid_string_array;
+ valid_string_array[0] = "nitro cold brew";
+ valid_string_array[1] = "doppio espresso";
+
+ Json::Value invalid_string_array;
+ invalid_string_array[0] = "mocha latte";
+ invalid_string_array[1] = 456;
+
+ std::vector<std::string> out;
+ EXPECT_TRUE(ParseAndValidateStringArray(valid_string_array, &out));
+ EXPECT_THAT(out, ElementsAre("nitro cold brew", "doppio espresso"));
+ EXPECT_FALSE(ParseAndValidateStringArray(invalid_string_array, &out));
+ EXPECT_FALSE(ParseAndValidateStringArray(kEmptyArray, &out));
+}
+
+} // namespace json
+} // namespace openscreen
diff --git a/util/simple_fraction.cc b/util/simple_fraction.cc
index a98d825c..7d9d5f9e 100644
--- a/util/simple_fraction.cc
+++ b/util/simple_fraction.cc
@@ -4,43 +4,71 @@
#include "util/simple_fraction.h"
+#include <stdlib.h>
+
#include <cmath>
#include <limits>
+#include <sstream>
+#include <utility>
#include <vector>
-#include "absl/strings/str_cat.h"
-#include "absl/strings/str_split.h"
-#include "util/osp_logging.h"
-
namespace openscreen {
+namespace {
-// static
-ErrorOr<SimpleFraction> SimpleFraction::FromString(absl::string_view value) {
- std::vector<absl::string_view> fields = absl::StrSplit(value, '/');
- if (fields.size() != 1 && fields.size() != 2) {
+constexpr char kDelimiter[] = "/";
+
+// The Linux implementation of strtol is overly lenient on parsing strings, e.g.
+// the string "not a number" is a valid number=0. We wrap it here to avoid
+// complicated checking in usage.
+ErrorOr<int> StringToLong(const std::string& str) {
+ if (str.empty()) {
return Error::Code::kParameterInvalid;
}
- int numerator;
- int denominator = 1;
- if (!absl::SimpleAtoi(fields[0], &numerator)) {
+ char* end_pointer;
+ errno = 0;
+ const int output = strtol(str.data(), &end_pointer, 10);
+ if (*end_pointer != '\0' || errno != 0) {
return Error::Code::kParameterInvalid;
}
+ return output;
+}
+} // namespace
- if (fields.size() == 2) {
- if (!absl::SimpleAtoi(fields[1], &denominator)) {
- return Error::Code::kParameterInvalid;
- }
+// static
+ErrorOr<SimpleFraction> SimpleFraction::FromString(const std::string& value) {
+ // Zeroth case: empty string.
+ if (value.empty()) {
+ return Error::Code::kParameterInvalid;
}
- return SimpleFraction{numerator, denominator};
+ std::size_t delimiter_pos = value.find(kDelimiter);
+ // First case: simple number, not a fraction.
+ if (delimiter_pos == std::string::npos) {
+ ErrorOr<int> numerator = StringToLong(value);
+ if (numerator.is_error()) {
+ return std::move(numerator.error());
+ }
+ return SimpleFraction{numerator.value(), 1};
+ }
+ // Second case: proper fraction.
+ const std::string first_field = value.substr(0, delimiter_pos);
+ const std::string second_field = value.substr(delimiter_pos + 1);
+ ErrorOr<int> numerator = StringToLong(first_field);
+ ErrorOr<int> denominator = StringToLong(second_field);
+ if (numerator.is_error() || denominator.is_error()) {
+ return Error::Code::kParameterInvalid;
+ }
+ return SimpleFraction{numerator.value(), denominator.value()};
}
std::string SimpleFraction::ToString() const {
if (denominator == 1) {
return std::to_string(numerator);
}
- return absl::StrCat(numerator, "/", denominator);
+ std::ostringstream ss;
+ ss << numerator << kDelimiter << denominator;
+ return ss.str();
}
bool SimpleFraction::operator==(const SimpleFraction& other) const {
diff --git a/util/simple_fraction.h b/util/simple_fraction.h
index f8ab5083..03662dee 100644
--- a/util/simple_fraction.h
+++ b/util/simple_fraction.h
@@ -7,7 +7,6 @@
#include <string>
-#include "absl/strings/string_view.h"
#include "platform/base/error.h"
namespace openscreen {
@@ -26,7 +25,7 @@ namespace openscreen {
// 4. A SimpleFraction is "positive" if and only if it is defined and at least
// equal to zero. Since reductions are not performed, -1/-1 is negative.
struct SimpleFraction {
- static ErrorOr<SimpleFraction> FromString(absl::string_view value);
+ static ErrorOr<SimpleFraction> FromString(const std::string& value);
std::string ToString() const;
bool operator==(const SimpleFraction& other) const;
diff --git a/util/simple_fraction_unittest.cc b/util/simple_fraction_unittest.cc
index 7cdbfeec..d9443df0 100644
--- a/util/simple_fraction_unittest.cc
+++ b/util/simple_fraction_unittest.cc
@@ -4,6 +4,8 @@
#include "util/simple_fraction.h"
+#include <math.h>
+
#include <limits>
#include "gtest/gtest.h"
@@ -15,16 +17,15 @@ namespace {
constexpr int kMin = std::numeric_limits<int>::min();
constexpr int kMax = std::numeric_limits<int>::max();
-void ExpectFromStringEquals(absl::string_view s,
- const SimpleFraction& expected) {
- const ErrorOr<SimpleFraction> f = SimpleFraction::FromString(s);
- EXPECT_TRUE(f.is_value());
+void ExpectFromStringEquals(const char* s, const SimpleFraction& expected) {
+ const ErrorOr<SimpleFraction> f = SimpleFraction::FromString(std::string(s));
+ EXPECT_TRUE(f.is_value()) << "from string: '" << s << "'";
EXPECT_EQ(expected, f.value());
}
-void ExpectFromStringError(absl::string_view s) {
- const auto f = SimpleFraction::FromString(s);
- EXPECT_TRUE(f.is_error());
+void ExpectFromStringError(const char* s) {
+ const auto f = SimpleFraction::FromString(std::string(s));
+ EXPECT_TRUE(f.is_error()) << "from string: '" << s << "'";
}
} // namespace
@@ -91,6 +92,7 @@ TEST(SimpleFractionTest, Positivity) {
TEST(SimpleFractionTest, CastToDouble) {
EXPECT_DOUBLE_EQ(0.0, static_cast<double>(SimpleFraction{0, 1}));
EXPECT_DOUBLE_EQ(1.0, static_cast<double>(SimpleFraction{1, 1}));
+ EXPECT_TRUE(isnan(static_cast<double>(SimpleFraction{1, 0})));
EXPECT_DOUBLE_EQ(1.0, static_cast<double>(SimpleFraction{kMax, kMax}));
EXPECT_DOUBLE_EQ(1.0, static_cast<double>(SimpleFraction{kMin, kMin}));
}