// Copyright 2019 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "cast/streaming/public/offer_messages.h" #include #include #include #include #include #include #include #include "cast/streaming/public/constants.h" #include "platform/base/error.h" #include "util/big_endian.h" #include "util/enum_name_table.h" #include "util/json/json_helpers.h" #include "util/json/json_serialization.h" #include "util/osp_logging.h" #include "util/string_util.h" #include "util/stringprintf.h" namespace openscreen::cast { namespace { constexpr char kSupportedStreams[] = "supportedStreams"; constexpr char kAudioSourceType[] = "audio_source"; constexpr char kVideoSourceType[] = "video_source"; constexpr char kStreamType[] = "type"; [[nodiscard]] constexpr bool CodecParameterIsValid(VideoCodec codec, std::string_view parameter) { if (parameter.empty()) { return true; } switch (codec) { using enum VideoCodec; case kVp8: return parameter.starts_with("vp08"); case kVp9: return parameter.starts_with("vp09"); case kAv1: return parameter.starts_with("av01"); case kHevc: return parameter.starts_with("hev1"); case kH264: return parameter.starts_with("avc1"); case kNotSpecified: return false; } OSP_NOTREACHED(); } bool CodecParameterIsValid(AudioCodec codec, const std::string& codec_parameter) { if (codec_parameter.empty()) { return true; } switch (codec) { case AudioCodec::kAac: return codec_parameter.starts_with("mp4a."); // Opus doesn't use codec parameters. case AudioCodec::kOpus: // fallthrough case AudioCodec::kNotSpecified: return false; } OSP_NOTREACHED(); } EnumNameTable kCastModeNames{ {{"mirroring", CastMode::kMirroring}, {"remoting", CastMode::kRemoting}}}; bool TryParseRtpPayloadType(const Json::Value& value, RtpPayloadType* out) { int t; if (!json::TryParseInt(value, &t)) { return false; } uint8_t t_small = t; if (t_small != t || !IsRtpPayloadType(t_small)) { return false; } *out = static_cast(t_small); return true; } bool TryParseRtpTimebase(const Json::Value& value, int* out) { std::string raw_timebase; if (!json::TryParseString(value, &raw_timebase)) { return false; } // The spec demands a leading 1, so this isn't really a fraction. const auto fraction = SimpleFraction::FromString(raw_timebase); if (fraction.is_error() || !fraction.value().is_positive() || fraction.value().numerator() != 1) { return false; } *out = fraction.value().denominator(); return true; } // For a hex byte, the conversion is 4 bits to 1 character, e.g. // 0b11110001 becomes F1, so 1 byte is two characters. constexpr int kHexDigitsPerByte = 2; constexpr int kAesBytesSize = 16; constexpr int kAesStringLength = kAesBytesSize * kHexDigitsPerByte; bool TryParseAesHexBytes(const Json::Value& value, std::array* out) { std::string hex_string; if (!json::TryParseString(value, &hex_string)) { return false; } constexpr int kHexDigitsPerScanField = 16; constexpr int kNumScanFields = kAesStringLength / kHexDigitsPerScanField; uint64_t quads[kNumScanFields]; int chars_scanned; if (hex_string.size() == kAesStringLength && sscanf(hex_string.c_str(), "%16" SCNx64 "%16" SCNx64 "%n", &quads[0], &quads[1], &chars_scanned) == kNumScanFields && chars_scanned == kAesStringLength && std::none_of(hex_string.begin(), hex_string.end(), [](char c) { return std::isspace(c); })) { WriteBigEndian(quads[0], out->data()); WriteBigEndian(quads[1], out->data() + 8); return true; } return false; } std::string_view ToString(Stream::Type type) { switch (type) { case Stream::Type::kAudioSource: return kAudioSourceType; case Stream::Type::kVideoSource: return kVideoSourceType; default: { OSP_NOTREACHED(); } } } bool TryParseResolutions(const Json::Value& value, std::vector* out) { out->clear(); // Some legacy senders don't provide resolutions, so just return empty. if (!value.isArray() || value.empty()) { return false; } for (Json::ArrayIndex i = 0; i < value.size(); ++i) { auto resolution = Resolution::TryParse(value[i]); if (resolution.is_error()) { out->clear(); return false; } out->push_back(std::move(resolution.value())); } return true; } } // namespace ErrorOr Stream::TryParse(const Json::Value& value, Stream::Type type) { if (!value.isObject()) { return Error(Error::Code::kJsonParseError, "Stream is not a JSON object"); } Stream out; out.type = type; if (!json::TryParseInt(value["index"], &out.index) || !json::TryParseUint(value["ssrc"], &out.ssrc) || !TryParseRtpPayloadType(value["rtpPayloadType"], &out.rtp_payload_type) || !TryParseRtpTimebase(value["timeBase"], &out.rtp_timebase)) { return Error(Error::Code::kJsonParseError, "Offer stream has missing or invalid mandatory field"); } if (!json::TryParseInt(value["channels"], &out.channels)) { out.channels = out.type == Stream::Type::kAudioSource ? kDefaultNumAudioChannels : kDefaultNumVideoChannels; } else if (out.channels <= 0) { return Error(Error::Code::kJsonParseError, "Invalid channel count"); } if (!TryParseAesHexBytes(value["aesKey"], &out.aes_key) || !TryParseAesHexBytes(value["aesIvMask"], &out.aes_iv_mask)) { return Error(Error::Code::kUnencryptedOffer, "Offer stream must have both a valid aesKey and aesIvMask"); } if (out.rtp_timebase < std::min(kDefaultAudioMinSampleRate, kRtpVideoTimebase) || out.rtp_timebase > kRtpVideoTimebase) { return Error(Error::Code::kJsonParseError, "rtp_timebase (sample rate)"); } out.target_delay = kDefaultTargetPlayoutDelay; int target_delay; if (json::TryParseInt(value["targetDelay"], &target_delay)) { auto d = std::chrono::milliseconds(target_delay); if (kMinTargetPlayoutDelay <= d && d <= kMaxTargetPlayoutDelay) { out.target_delay = d; } } json::TryParseBool(value["receiverRtcpEventLog"], &out.receiver_rtcp_event_log); int dscp_value; if (json::TryParseInt(value["receiverRtcpDscp"], &dscp_value)) { // DSCP values are clamped to [0, 63]. if (dscp_value < 0 || dscp_value > 63) { return Error(Error::Code::kJsonParseError, "receiverRtcpDscp (invalid DSCP value)"); } out.receiver_rtcp_dscp = dscp_value; } json::TryParseStringArray(value["rtpExtensions"], &out.rtp_extensions); json::TryParseString(value["codecParameter"], &out.codec_parameter); return out; } Json::Value Stream::ToJson() const { OSP_CHECK(IsValid()); Json::Value root; root["index"] = index; root["type"] = std::string(ToString(type)); root["channels"] = channels; root["rtpPayloadType"] = static_cast(rtp_payload_type); // rtpProfile is technically required by the spec, although it is always set // to cast. We set it here to be compliant with all spec implementers. root["rtpProfile"] = "cast"; static_assert(sizeof(ssrc) <= sizeof(Json::UInt), "this code assumes Ssrc fits in a Json::UInt"); root["ssrc"] = static_cast(ssrc); root["targetDelay"] = static_cast(target_delay.count()); root["aesKey"] = HexEncode(aes_key.data(), aes_key.size()); root["aesIvMask"] = HexEncode(aes_iv_mask.data(), aes_iv_mask.size()); root["receiverRtcpEventLog"] = receiver_rtcp_event_log; if (receiver_rtcp_dscp.has_value()) { root["receiverRtcpDscp"] = receiver_rtcp_dscp.value(); } root["timeBase"] = "1/" + std::to_string(rtp_timebase); root["codecParameter"] = codec_parameter; if (!rtp_extensions.empty()) { root["rtpExtensions"] = json::PrimitiveVectorToJson(rtp_extensions); } return root; } bool Stream::IsValid() const { return channels >= 1 && index >= 0 && target_delay.count() > 0 && target_delay.count() <= std::numeric_limits::max() && rtp_timebase >= 1; } ErrorOr AudioStream::TryParse(const Json::Value& value) { if (!value.isObject()) { return Error(Error::Code::kJsonParseError, "Audio stream is not a JSON object"); } auto stream_or_error = Stream::TryParse(value, Stream::Type::kAudioSource); if (stream_or_error.is_error()) { return stream_or_error.error(); } AudioStream out; out.stream = std::move(stream_or_error.value()); std::string codec_name; if (!json::TryParseInt(value["bitRate"], &out.bit_rate) || out.bit_rate < 0 || !json::TryParseString(value[kCodecName], &codec_name)) { return Error(Error::Code::kJsonParseError, "Invalid audio stream field"); } ErrorOr codec = StringToAudioCodec(codec_name); if (!codec) { return Error(Error::Code::kUnknownCodec, "Codec is not known, can't use stream"); } out.codec = codec.value(); if (!CodecParameterIsValid(codec.value(), out.stream.codec_parameter)) { return Error(Error::Code::kInvalidCodecParameter, StringFormat("Invalid audio codec parameter ({} for codec {})", out.stream.codec_parameter.c_str(), CodecToString(codec.value()))); } return out; } Json::Value AudioStream::ToJson() const { OSP_CHECK(IsValid()); Json::Value out = stream.ToJson(); out[kCodecName] = CodecToString(codec); out["bitRate"] = bit_rate; return out; } bool AudioStream::IsValid() const { return bit_rate >= 0 && stream.IsValid(); } ErrorOr VideoStream::TryParse(const Json::Value& value) { if (!value.isObject()) { return Error(Error::Code::kJsonParseError, "Video stream is not a JSON object"); } auto stream_or_error = Stream::TryParse(value, Stream::Type::kVideoSource); if (stream_or_error.is_error()) { return stream_or_error.error(); } VideoStream out; out.stream = std::move(stream_or_error.value()); std::string codec_name; if (!json::TryParseString(value[kCodecName], &codec_name)) { return Error(Error::Code::kJsonParseError, "Video stream missing codec"); } ErrorOr codec = StringToVideoCodec(codec_name); if (!codec) { return Error(Error::Code::kUnknownCodec, "Codec is not known, can't use stream"); } out.codec = codec.value(); if (!CodecParameterIsValid(codec.value(), out.stream.codec_parameter)) { return Error(Error::Code::kInvalidCodecParameter, StringFormat("Invalid video codec parameter ({} for codec {})", out.stream.codec_parameter.c_str(), CodecToString(codec.value()))); } out.max_frame_rate = SimpleFraction{kDefaultMaxFrameRate, 1}; std::string raw_max_frame_rate; if (json::TryParseString(value["maxFrameRate"], &raw_max_frame_rate)) { auto parsed = SimpleFraction::FromString(raw_max_frame_rate); if (parsed.is_value() && parsed.value().is_positive()) { out.max_frame_rate = parsed.value(); } } TryParseResolutions(value["resolutions"], &out.resolutions); json::TryParseString(value["profile"], &out.profile); json::TryParseString(value["protection"], &out.protection); json::TryParseString(value["level"], &out.level); json::TryParseString(value["errorRecoveryMode"], &out.error_recovery_mode); if (!json::TryParseInt(value["maxBitRate"], &out.max_bit_rate)) { out.max_bit_rate = 4 << 20; } return out; } Json::Value VideoStream::ToJson() const { OSP_CHECK(IsValid()); Json::Value out = stream.ToJson(); out["codecName"] = CodecToString(codec); out["maxFrameRate"] = max_frame_rate.ToString(); out["maxBitRate"] = max_bit_rate; out["protection"] = protection; out["profile"] = profile; out["level"] = level; out["errorRecoveryMode"] = error_recovery_mode; Json::Value rs; for (auto resolution : resolutions) { rs.append(resolution.ToJson()); } out["resolutions"] = std::move(rs); return out; } bool VideoStream::IsValid() const { return max_bit_rate > 0 && max_frame_rate.is_positive(); } // static ErrorOr Offer::TryParse(const Json::Value& root) { if (!root.isObject()) { return Error(Error::Code::kJsonParseError, "null offer"); } const ErrorOr cast_mode = GetEnum(kCastModeNames, root["castMode"].asString()); Json::Value supported_streams = root[kSupportedStreams]; if (!supported_streams.isArray()) { return Error(Error::Code::kJsonParseError, "supported streams in offer"); } std::vector audio_streams; std::vector video_streams; using Dscp = std::optional; std::optional receiver_rtcp_dscp; for (Json::ArrayIndex i = 0; i < supported_streams.size(); ++i) { const Json::Value& fields = supported_streams[i]; std::string type; if (!json::TryParseString(fields[kStreamType], &type)) { return Error(Error::Code::kJsonParseError, "Missing stream type"); } Error error = Error::None(); if (type == kAudioSourceType) { auto stream_or_error = AudioStream::TryParse(fields); if (stream_or_error.is_value()) { auto stream = std::move(stream_or_error.value()); if (!receiver_rtcp_dscp) { receiver_rtcp_dscp.emplace(stream.stream.receiver_rtcp_dscp); } else if (stream.stream.receiver_rtcp_dscp != *receiver_rtcp_dscp) { return Error(Error::Code::kJsonParseError, "Mixed DSCP values in offer"); } audio_streams.push_back(std::move(stream)); } else { error = stream_or_error.error(); } } else if (type == kVideoSourceType) { auto stream_or_error = VideoStream::TryParse(fields); if (stream_or_error.is_value()) { auto stream = std::move(stream_or_error.value()); if (!receiver_rtcp_dscp) { receiver_rtcp_dscp.emplace(stream.stream.receiver_rtcp_dscp); } else if (stream.stream.receiver_rtcp_dscp != *receiver_rtcp_dscp) { return Error(Error::Code::kJsonParseError, "Mixed DSCP values in offer"); } video_streams.push_back(std::move(stream)); } else { error = stream_or_error.error(); } } if (!error.ok()) { if (error.code() == Error::Code::kUnknownCodec) { OSP_VLOG << "Dropping audio stream due to unknown codec: " << error; continue; } else { return error; } } } return Offer{cast_mode.value(CastMode::kMirroring), std::move(audio_streams), std::move(video_streams)}; } Json::Value Offer::ToJson() const { OSP_CHECK(IsValid()); Json::Value root; root["castMode"] = GetEnumName(kCastModeNames, cast_mode).value(); Json::Value streams; for (auto& stream : audio_streams) { streams.append(stream.ToJson()); } for (auto& stream : video_streams) { streams.append(stream.ToJson()); } root[kSupportedStreams] = std::move(streams); return root; } bool Offer::IsValid() const { return std::ranges::all_of( audio_streams, [](const AudioStream& a) { return a.IsValid(); }) && std::ranges::all_of(video_streams, [](const VideoStream& v) { return v.IsValid(); }); } } // namespace openscreen::cast