diff options
Diffstat (limited to 'cast')
-rw-r--r-- | cast/streaming/answer_messages.cc | 473 | ||||
-rw-r--r-- | cast/streaming/answer_messages.h | 78 | ||||
-rw-r--r-- | cast/streaming/answer_messages_unittest.cc | 509 | ||||
-rw-r--r-- | cast/streaming/message_util.h | 66 | ||||
-rw-r--r-- | cast/streaming/offer_messages.cc | 69 | ||||
-rw-r--r-- | cast/streaming/receiver_session.cc | 70 | ||||
-rw-r--r-- | cast/streaming/receiver_session.h | 8 | ||||
-rw-r--r-- | cast/streaming/receiver_session_unittest.cc | 18 | ||||
-rw-r--r-- | cast/streaming/session_config.cc | 13 | ||||
-rw-r--r-- | cast/streaming/session_config.h | 10 |
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: |