diff options
Diffstat (limited to 'cast/streaming/offer_messages.cc')
-rw-r--r-- | cast/streaming/offer_messages.cc | 577 |
1 files changed, 285 insertions, 292 deletions
diff --git a/cast/streaming/offer_messages.cc b/cast/streaming/offer_messages.cc index cea500cd..a162f09f 100644 --- a/cast/streaming/offer_messages.cc +++ b/cast/streaming/offer_messages.cc @@ -14,7 +14,6 @@ #include "absl/strings/match.h" #include "absl/strings/numbers.h" #include "absl/strings/str_split.h" -#include "cast/streaming/capture_recommendations.h" #include "cast/streaming/constants.h" #include "platform/base/error.h" #include "util/big_endian.h" @@ -34,36 +33,78 @@ constexpr char kAudioSourceType[] = "audio_source"; constexpr char kVideoSourceType[] = "video_source"; constexpr char kStreamType[] = "type"; -ErrorOr<RtpPayloadType> ParseRtpPayloadType(const Json::Value& parent, - const std::string& field) { - auto t = json::ParseInt(parent, field); - if (!t) { - return t.error(); +bool CodecParameterIsValid(VideoCodec codec, + const std::string& codec_parameter) { + if (codec_parameter.empty()) { + return true; + } + switch (codec) { + case VideoCodec::kVp8: + return absl::StartsWith(codec_parameter, "vp08"); + case VideoCodec::kVp9: + return absl::StartsWith(codec_parameter, "vp09"); + case VideoCodec::kAv1: + return absl::StartsWith(codec_parameter, "av01"); + case VideoCodec::kHevc: + return absl::StartsWith(codec_parameter, "hev1"); + case VideoCodec::kH264: + return absl::StartsWith(codec_parameter, "avc1"); + case VideoCodec::kNotSpecified: + return false; + } + OSP_NOTREACHED(); +} + +bool CodecParameterIsValid(AudioCodec codec, + const std::string& codec_parameter) { + if (codec_parameter.empty()) { + return true; + } + switch (codec) { + case AudioCodec::kAac: + return absl::StartsWith(codec_parameter, "mp4a."); + + // Opus doesn't use codec parameters. + case AudioCodec::kOpus: // fallthrough + case AudioCodec::kNotSpecified: + return false; } + OSP_NOTREACHED(); +} + +EnumNameTable<CastMode, 2> kCastModeNames{ + {{"mirroring", CastMode::kMirroring}, {"remoting", CastMode::kRemoting}}}; - uint8_t t_small = t.value(); - if (t_small != t.value() || !IsRtpPayloadType(t_small)) { - return Error(Error::Code::kParameterInvalid, - "Received invalid RTP Payload Type."); +bool TryParseRtpPayloadType(const Json::Value& value, RtpPayloadType* out) { + int t; + if (!json::TryParseInt(value, &t)) { + return false; } - return static_cast<RtpPayloadType>(t_small); + uint8_t t_small = t; + if (t_small != t || !IsRtpPayloadType(t_small)) { + return false; + } + + *out = static_cast<RtpPayloadType>(t_small); + return true; } -ErrorOr<int> ParseRtpTimebase(const Json::Value& parent, - const std::string& field) { - auto error_or_raw = json::ParseString(parent, field); - if (!error_or_raw) { - return error_or_raw.error(); +bool TryParseRtpTimebase(const Json::Value& value, int* out) { + std::string raw_timebase; + if (!json::TryParseString(value, &raw_timebase)) { + return false; } // The spec demands a leading 1, so this isn't really a fraction. - const auto fraction = SimpleFraction::FromString(error_or_raw.value()); + const auto fraction = SimpleFraction::FromString(raw_timebase); if (fraction.is_error() || !fraction.value().is_positive() || - fraction.value().numerator != 1) { - return json::CreateParseError("RTP timebase"); + fraction.value().numerator() != 1) { + return false; } - return fraction.value().denominator; + + *out = fraction.value().denominator(); + return true; } // For a hex byte, the conversion is 4 bits to 1 character, e.g. @@ -71,226 +112,118 @@ ErrorOr<int> ParseRtpTimebase(const Json::Value& parent, constexpr int kHexDigitsPerByte = 2; constexpr int kAesBytesSize = 16; constexpr int kAesStringLength = kAesBytesSize * kHexDigitsPerByte; -ErrorOr<std::array<uint8_t, kAesBytesSize>> ParseAesHexBytes( - const Json::Value& parent, - const std::string& field) { - auto hex_string = json::ParseString(parent, field); - if (!hex_string) { - return hex_string.error(); +bool TryParseAesHexBytes(const Json::Value& value, + std::array<uint8_t, kAesBytesSize>* out) { + std::string hex_string; + if (!json::TryParseString(value, &hex_string)) { + return false; } constexpr int kHexDigitsPerScanField = 16; constexpr int kNumScanFields = kAesStringLength / kHexDigitsPerScanField; uint64_t quads[kNumScanFields]; int chars_scanned; - if (hex_string.value().size() == kAesStringLength && - sscanf(hex_string.value().c_str(), "%16" SCNx64 "%16" SCNx64 "%n", - &quads[0], &quads[1], &chars_scanned) == kNumScanFields && + if (hex_string.size() == kAesStringLength && + sscanf(hex_string.c_str(), "%16" SCNx64 "%16" SCNx64 "%n", &quads[0], + &quads[1], &chars_scanned) == kNumScanFields && chars_scanned == kAesStringLength && - std::none_of(hex_string.value().begin(), hex_string.value().end(), + std::none_of(hex_string.begin(), hex_string.end(), [](char c) { return std::isspace(c); })) { - std::array<uint8_t, kAesBytesSize> bytes; - WriteBigEndian(quads[0], bytes.data()); - WriteBigEndian(quads[1], bytes.data() + 8); - return bytes; - } - return json::CreateParseError("AES hex string bytes"); -} - -ErrorOr<Stream> ParseStream(const Json::Value& value, Stream::Type type) { - auto index = json::ParseInt(value, "index"); - if (!index) { - return index.error(); - } - // If channel is omitted, the default value is used later. - auto channels = json::ParseInt(value, "channels"); - if (channels.is_value() && channels.value() <= 0) { - return json::CreateParameterError("channel"); - } - auto rtp_profile = json::ParseString(value, "rtpProfile"); - if (!rtp_profile) { - return rtp_profile.error(); - } - auto rtp_payload_type = ParseRtpPayloadType(value, "rtpPayloadType"); - if (!rtp_payload_type) { - return rtp_payload_type.error(); - } - auto ssrc = json::ParseUint(value, "ssrc"); - if (!ssrc) { - return ssrc.error(); - } - auto aes_key = ParseAesHexBytes(value, "aesKey"); - auto aes_iv_mask = ParseAesHexBytes(value, "aesIvMask"); - if (!aes_key || !aes_iv_mask) { - return Error(Error::Code::kUnencryptedOffer, - "Offer stream must have both a valid aesKey and aesIvMask"); - } - auto rtp_timebase = ParseRtpTimebase(value, "timeBase"); - if (!rtp_timebase) { - return rtp_timebase.error(); - } - if (rtp_timebase.value() < - std::min(capture_recommendations::kDefaultAudioMinSampleRate, - kRtpVideoTimebase) || - rtp_timebase.value() > kRtpVideoTimebase) { - return json::CreateParameterError("rtp_timebase (sample rate)"); + WriteBigEndian(quads[0], out->data()); + WriteBigEndian(quads[1], out->data() + 8); + return true; } - 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 (kMinTargetPlayoutDelay <= d && d <= kMaxTargetPlayoutDelay) { - target_delay_ms = d; - } - } - - 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 - ? kDefaultNumAudioChannels - : kDefaultNumVideoChannels), - rtp_payload_type.value(), - ssrc.value(), - target_delay_ms, - aes_key.value(), - aes_iv_mask.value(), - receiver_rtcp_event_log.value({}), - receiver_rtcp_dscp.value({}), - rtp_timebase.value()}; + return false; } -ErrorOr<AudioStream> ParseAudioStream(const Json::Value& value) { - auto stream = ParseStream(value, Stream::Type::kAudioSource); - if (!stream) { - return stream.error(); - } - auto bit_rate = json::ParseInt(value, "bitRate"); - if (!bit_rate) { - return bit_rate.error(); - } - - auto codec_name = json::ParseString(value, "codecName"); - if (!codec_name) { - return codec_name.error(); - } - ErrorOr<AudioCodec> codec = StringToAudioCodec(codec_name.value()); - if (!codec) { - return Error(Error::Code::kUnknownCodec, - "Codec is not known, can't use stream"); - } - - // A bit rate of 0 is valid for some codec types, so we don't enforce here. - if (bit_rate.value() < 0) { - return json::CreateParameterError("bit rate"); +absl::string_view ToString(Stream::Type type) { + switch (type) { + case Stream::Type::kAudioSource: + return kAudioSourceType; + case Stream::Type::kVideoSource: + return kVideoSourceType; + default: { + OSP_NOTREACHED(); + } } - return AudioStream{stream.value(), codec.value(), bit_rate.value()}; } -ErrorOr<Resolution> ParseResolution(const Json::Value& value) { - auto width = json::ParseInt(value, "width"); - if (!width) { - return width.error(); - } - auto height = json::ParseInt(value, "height"); - if (!height) { - return height.error(); - } - if (width.value() <= 0 || height.value() <= 0) { - return json::CreateParameterError("resolution"); - } - return Resolution{width.value(), height.value()}; -} +bool TryParseResolutions(const Json::Value& value, + std::vector<Resolution>* out) { + out->clear(); -ErrorOr<std::vector<Resolution>> ParseResolutions(const Json::Value& parent, - const std::string& field) { - std::vector<Resolution> resolutions; // Some legacy senders don't provide resolutions, so just return empty. - const Json::Value& value = parent[field]; if (!value.isArray() || value.empty()) { - return resolutions; + return false; } for (Json::ArrayIndex i = 0; i < value.size(); ++i) { - auto r = ParseResolution(value[i]); - if (!r) { - return r.error(); + Resolution resolution; + if (!Resolution::TryParse(value[i], &resolution)) { + out->clear(); + return false; } - resolutions.push_back(r.value()); + out->push_back(std::move(resolution)); } - return resolutions; + return true; } -ErrorOr<VideoStream> ParseVideoStream(const Json::Value& value) { - auto stream = ParseStream(value, Stream::Type::kVideoSource); - if (!stream) { - return stream.error(); +} // namespace + +Error Stream::TryParse(const Json::Value& value, + Stream::Type type, + Stream* out) { + out->type = type; + + if (!json::TryParseInt(value["index"], &out->index) || + !json::TryParseUint(value["ssrc"], &out->ssrc) || + !TryParseRtpPayloadType(value["rtpPayloadType"], + &out->rtp_payload_type) || + !TryParseRtpTimebase(value["timeBase"], &out->rtp_timebase)) { + return Error(Error::Code::kJsonParseError, + "Offer stream has missing or invalid mandatory field"); } - auto codec_name = json::ParseString(value, "codecName"); - if (!codec_name) { - return codec_name.error(); + + if (!json::TryParseInt(value["channels"], &out->channels)) { + out->channels = out->type == Stream::Type::kAudioSource + ? kDefaultNumAudioChannels + : kDefaultNumVideoChannels; + } else if (out->channels <= 0) { + return Error(Error::Code::kJsonParseError, "Invalid channel count"); } - ErrorOr<VideoCodec> codec = StringToVideoCodec(codec_name.value()); - if (!codec) { - return Error(Error::Code::kUnknownCodec, - "Codec is not known, can't use stream"); + + if (!TryParseAesHexBytes(value["aesKey"], &out->aes_key) || + !TryParseAesHexBytes(value["aesIvMask"], &out->aes_iv_mask)) { + return Error(Error::Code::kUnencryptedOffer, + "Offer stream must have both a valid aesKey and aesIvMask"); } - auto resolutions = ParseResolutions(value, "resolutions"); - if (!resolutions) { - return resolutions.error(); + if (out->rtp_timebase < + std::min(kDefaultAudioMinSampleRate, kRtpVideoTimebase) || + out->rtp_timebase > kRtpVideoTimebase) { + return Error(Error::Code::kJsonParseError, "rtp_timebase (sample rate)"); } - 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()); - if (parsed.is_value() && parsed.value().is_positive()) { - max_frame_rate = parsed.value(); + out->target_delay = kDefaultTargetPlayoutDelay; + int target_delay; + if (json::TryParseInt(value["targetDelay"], &target_delay)) { + auto d = std::chrono::milliseconds(target_delay); + if (kMinTargetPlayoutDelay <= d && d <= kMaxTargetPlayoutDelay) { + out->target_delay = d; } } - 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(), - codec.value(), - max_frame_rate, - max_bit_rate.value(4 << 20), - protection.value({}), - profile.value({}), - level.value({}), - resolutions.value(), - error_recovery_mode.value({})}; -} + json::TryParseBool(value["receiverRtcpEventLog"], + &out->receiver_rtcp_event_log); + json::TryParseString(value["receiverRtcpDscp"], &out->receiver_rtcp_dscp); + json::TryParseString(value["codecParameter"], &out->codec_parameter); -absl::string_view ToString(Stream::Type type) { - switch (type) { - case Stream::Type::kAudioSource: - return kAudioSourceType; - case Stream::Type::kVideoSource: - return kVideoSourceType; - default: { - OSP_NOTREACHED(); - } - } + return Error::None(); } -EnumNameTable<CastMode, 2> kCastModeNames{ - {{"mirroring", CastMode::kMirroring}, {"remoting", CastMode::kRemoting}}}; - -} // namespace - -ErrorOr<Json::Value> Stream::ToJson() const { - if (channels < 1 || index < 0 || target_delay.count() <= 0 || - target_delay.count() > std::numeric_limits<int>::max() || - rtp_timebase < 1) { - return json::CreateParameterError("Stream"); - } +Json::Value Stream::ToJson() const { + OSP_DCHECK(IsValid()); Json::Value root; root["index"] = index; @@ -304,152 +237,212 @@ ErrorOr<Json::Value> Stream::ToJson() const { "this code assumes Ssrc fits in a Json::UInt"); root["ssrc"] = static_cast<Json::UInt>(ssrc); root["targetDelay"] = static_cast<int>(target_delay.count()); - root["aesKey"] = HexEncode(aes_key); - root["aesIvMask"] = HexEncode(aes_iv_mask); + root["aesKey"] = HexEncode(aes_key.data(), aes_key.size()); + root["aesIvMask"] = HexEncode(aes_iv_mask.data(), aes_iv_mask.size()); root["receiverRtcpEventLog"] = receiver_rtcp_event_log; root["receiverRtcpDscp"] = receiver_rtcp_dscp; root["timeBase"] = "1/" + std::to_string(rtp_timebase); + root["codecParameter"] = codec_parameter; return root; } -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 json::CreateParameterError("AudioStream"); +bool Stream::IsValid() const { + return channels >= 1 && index >= 0 && target_delay.count() > 0 && + target_delay.count() <= std::numeric_limits<int>::max() && + rtp_timebase >= 1; +} + +Error AudioStream::TryParse(const Json::Value& value, AudioStream* out) { + Error error = + Stream::TryParse(value, Stream::Type::kAudioSource, &out->stream); + if (!error.ok()) { + return error; } - auto error_or_stream = stream.ToJson(); - if (error_or_stream.is_error()) { - return error_or_stream; + std::string codec_name; + if (!json::TryParseInt(value["bitRate"], &out->bit_rate) || + out->bit_rate < 0 || + !json::TryParseString(value[kCodecName], &codec_name)) { + return Error(Error::Code::kJsonParseError, "Invalid audio stream field"); } + ErrorOr<AudioCodec> codec = StringToAudioCodec(codec_name); + if (!codec) { + return Error(Error::Code::kUnknownCodec, + "Codec is not known, can't use stream"); + } + out->codec = codec.value(); + if (!CodecParameterIsValid(codec.value(), out->stream.codec_parameter)) { + return Error(Error::Code::kInvalidCodecParameter, + StringPrintf("Invalid audio codec parameter (%s for codec %s)", + out->stream.codec_parameter.c_str(), + CodecToString(codec.value()))); + } + return Error::None(); +} + +Json::Value AudioStream::ToJson() const { + OSP_DCHECK(IsValid()); + + Json::Value out = stream.ToJson(); + out[kCodecName] = CodecToString(codec); + out["bitRate"] = bit_rate; + return out; +} - error_or_stream.value()["codecName"] = CodecToString(codec); - error_or_stream.value()["bitRate"] = bit_rate; - return error_or_stream; +bool AudioStream::IsValid() const { + return bit_rate >= 0 && stream.IsValid(); } -ErrorOr<Json::Value> Resolution::ToJson() const { - if (width <= 0 || height <= 0) { - return json::CreateParameterError("Resolution"); +Error VideoStream::TryParse(const Json::Value& value, VideoStream* out) { + Error error = + Stream::TryParse(value, Stream::Type::kVideoSource, &out->stream); + if (!error.ok()) { + return error; } - Json::Value root; - root["width"] = width; - root["height"] = height; - return root; -} + std::string codec_name; + if (!json::TryParseString(value[kCodecName], &codec_name)) { + return Error(Error::Code::kJsonParseError, "Video stream missing codec"); + } + ErrorOr<VideoCodec> codec = StringToVideoCodec(codec_name); + if (!codec) { + return Error(Error::Code::kUnknownCodec, + "Codec is not known, can't use stream"); + } + out->codec = codec.value(); + if (!CodecParameterIsValid(codec.value(), out->stream.codec_parameter)) { + return Error(Error::Code::kInvalidCodecParameter, + StringPrintf("Invalid video codec parameter (%s for codec %s)", + out->stream.codec_parameter.c_str(), + CodecToString(codec.value()))); + } -ErrorOr<Json::Value> VideoStream::ToJson() const { - if (max_bit_rate <= 0 || !max_frame_rate.is_positive()) { - return json::CreateParameterError("VideoStream"); + out->max_frame_rate = SimpleFraction{kDefaultMaxFrameRate, 1}; + std::string raw_max_frame_rate; + if (json::TryParseString(value["maxFrameRate"], &raw_max_frame_rate)) { + auto parsed = SimpleFraction::FromString(raw_max_frame_rate); + if (parsed.is_value() && parsed.value().is_positive()) { + out->max_frame_rate = parsed.value(); + } } - auto error_or_stream = stream.ToJson(); - if (error_or_stream.is_error()) { - return error_or_stream; + TryParseResolutions(value["resolutions"], &out->resolutions); + json::TryParseString(value["profile"], &out->profile); + json::TryParseString(value["protection"], &out->protection); + json::TryParseString(value["level"], &out->level); + json::TryParseString(value["errorRecoveryMode"], &out->error_recovery_mode); + if (!json::TryParseInt(value["maxBitRate"], &out->max_bit_rate)) { + out->max_bit_rate = 4 << 20; } - auto& stream = error_or_stream.value(); - stream["codecName"] = CodecToString(codec); - stream["maxFrameRate"] = max_frame_rate.ToString(); - stream["maxBitRate"] = max_bit_rate; - stream["protection"] = protection; - stream["profile"] = profile; - stream["level"] = level; - stream["errorRecoveryMode"] = error_recovery_mode; + return Error::None(); +} + +Json::Value VideoStream::ToJson() const { + OSP_DCHECK(IsValid()); + + Json::Value out = stream.ToJson(); + out["codecName"] = CodecToString(codec); + out["maxFrameRate"] = max_frame_rate.ToString(); + out["maxBitRate"] = max_bit_rate; + out["protection"] = protection; + out["profile"] = profile; + out["level"] = level; + out["errorRecoveryMode"] = error_recovery_mode; Json::Value rs; for (auto resolution : resolutions) { - auto eoj = resolution.ToJson(); - if (eoj.is_error()) { - return eoj; - } - rs.append(eoj.value()); + rs.append(resolution.ToJson()); } - stream["resolutions"] = std::move(rs); - return error_or_stream; + out["resolutions"] = std::move(rs); + return out; +} + +bool VideoStream::IsValid() const { + return max_bit_rate > 0 && max_frame_rate.is_positive(); } // static ErrorOr<Offer> Offer::Parse(const Json::Value& root) { + Offer out; + Error error = TryParse(root, &out); + return error.ok() ? ErrorOr<Offer>(std::move(out)) + : ErrorOr<Offer>(std::move(error)); +} + +// static +Error Offer::TryParse(const Json::Value& root, Offer* out) { if (!root.isObject()) { - return json::CreateParseError("null offer"); + return Error(Error::Code::kJsonParseError, "null offer"); } - ErrorOr<CastMode> cast_mode = + const ErrorOr<CastMode> cast_mode = GetEnum(kCastModeNames, root["castMode"].asString()); - const ErrorOr<bool> get_status = json::ParseBool(root, "receiverGetStatus"); - Json::Value supported_streams = root[kSupportedStreams]; if (!supported_streams.isArray()) { - return json::CreateParseError("supported streams in offer"); + return Error(Error::Code::kJsonParseError, "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 = json::ParseString(fields, kStreamType); - if (!type) { - return type.error(); + std::string type; + if (!json::TryParseString(fields[kStreamType], &type)) { + return Error(Error::Code::kJsonParseError, "Missing stream type"); } - if (type.value() == kAudioSourceType) { - auto stream = ParseAudioStream(fields); - if (!stream) { - if (stream.error().code() == Error::Code::kUnknownCodec) { - OSP_DVLOG << "Dropping audio stream due to unknown codec: " - << stream.error(); - continue; - } else { - return stream.error(); - } + Error error; + if (type == kAudioSourceType) { + AudioStream stream; + error = AudioStream::TryParse(fields, &stream); + if (error.ok()) { + audio_streams.push_back(std::move(stream)); + } + } else if (type == kVideoSourceType) { + VideoStream stream; + error = VideoStream::TryParse(fields, &stream); + if (error.ok()) { + video_streams.push_back(std::move(stream)); } - audio_streams.push_back(std::move(stream.value())); - } else if (type.value() == kVideoSourceType) { - auto stream = ParseVideoStream(fields); - if (!stream) { - if (stream.error().code() == Error::Code::kUnknownCodec) { - OSP_DVLOG << "Dropping video stream due to unknown codec: " - << stream.error(); - continue; - } else { - return stream.error(); - } + } + + if (!error.ok()) { + if (error.code() == Error::Code::kUnknownCodec) { + OSP_VLOG << "Dropping audio stream due to unknown codec: " << error; + continue; + } else { + return error; } - video_streams.push_back(std::move(stream.value())); } } - return Offer{cast_mode.value(CastMode::kMirroring), get_status.value({}), - std::move(audio_streams), std::move(video_streams)}; + *out = Offer{cast_mode.value(CastMode::kMirroring), std::move(audio_streams), + std::move(video_streams)}; + return Error::None(); } -ErrorOr<Json::Value> Offer::ToJson() const { +Json::Value Offer::ToJson() const { + OSP_DCHECK(IsValid()); Json::Value root; - root["castMode"] = GetEnumName(kCastModeNames, cast_mode).value(); - root["receiverGetStatus"] = supports_wifi_status_reporting; - Json::Value streams; - for (auto& as : audio_streams) { - auto eoj = as.ToJson(); - if (eoj.is_error()) { - return eoj; - } - streams.append(eoj.value()); + for (auto& stream : audio_streams) { + streams.append(stream.ToJson()); } - for (auto& vs : video_streams) { - auto eoj = vs.ToJson(); - if (eoj.is_error()) { - return eoj; - } - streams.append(eoj.value()); + for (auto& stream : video_streams) { + streams.append(stream.ToJson()); } root[kSupportedStreams] = std::move(streams); return root; } +bool Offer::IsValid() const { + return std::all_of(audio_streams.begin(), audio_streams.end(), + [](const AudioStream& a) { return a.IsValid(); }) && + std::all_of(video_streams.begin(), video_streams.end(), + [](const VideoStream& v) { return v.IsValid(); }); +} } // namespace cast } // namespace openscreen |