/* * Copyright 2024 The WebRTC Project Authors. All rights reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "media/base/codec_comparators.h" #include #include #include #include #include #include "absl/algorithm/container.h" #include "absl/functional/any_invocable.h" #include "absl/strings/match.h" #include "absl/strings/string_view.h" #include "api/media_types.h" #include "api/rtp_parameters.h" #include "api/video_codecs/av1_profile.h" #include "api/video_codecs/h264_profile_level_id.h" #ifdef RTC_ENABLE_H265 #include "api/video_codecs/h265_profile_tier_level.h" #endif #include "api/video_codecs/vp9_profile.h" #include "media/base/codec.h" #include "media/base/media_constants.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" #include "rtc_base/string_encode.h" namespace webrtc { namespace { // TODO(bugs.webrtc.org/15847): remove code duplication of IsSameCodecSpecific // in api/video_codecs/sdp_video_format.cc std::string GetFmtpParameterOrDefault(const CodecParameterMap& params, const std::string& name, const std::string& default_value) { const auto it = params.find(name); if (it != params.end()) { return it->second; } return default_value; } bool HasParameter(const CodecParameterMap& params, const std::string& name) { return params.find(name) != params.end(); } std::string H264GetPacketizationModeOrDefault(const CodecParameterMap& params) { // If packetization-mode is not present, default to "0". // https://tools.ietf.org/html/rfc6184#section-6.2 return GetFmtpParameterOrDefault(params, kH264FmtpPacketizationMode, "0"); } bool H264IsSamePacketizationMode(const CodecParameterMap& left, const CodecParameterMap& right) { return H264GetPacketizationModeOrDefault(left) == H264GetPacketizationModeOrDefault(right); } std::string AV1GetTierOrDefault(const CodecParameterMap& params) { // If the parameter is not present, the tier MUST be inferred to be 0. // https://aomediacodec.github.io/av1-rtp-spec/#72-sdp-parameters return GetFmtpParameterOrDefault(params, kAv1FmtpTier, "0"); } std::string AV1GetLevelIdxOrDefault(const CodecParameterMap& params) { // If the parameter is not present, it MUST be inferred to be 5 (level 3.1). // https://aomediacodec.github.io/av1-rtp-spec/#72-sdp-parameters return GetFmtpParameterOrDefault(params, kAv1FmtpLevelIdx, "5"); } #ifdef RTC_ENABLE_H265 std::string GetH265TxModeOrDefault(const CodecParameterMap& params) { // If TxMode is not present, a value of "SRST" must be inferred. // https://tools.ietf.org/html/rfc7798@section-7.1 return GetFmtpParameterOrDefault(params, kH265FmtpTxMode, "SRST"); } bool IsSameH265TxMode(const CodecParameterMap& left, const CodecParameterMap& right) { return absl::EqualsIgnoreCase(GetH265TxModeOrDefault(left), GetH265TxModeOrDefault(right)); } #endif // Some (video) codecs are actually families of codecs and rely on parameters // to distinguish different incompatible family members. bool IsSameCodecSpecific(const std::string& name1, const CodecParameterMap& params1, const std::string& name2, const CodecParameterMap& params2) { // The names might not necessarily match, so check both. auto either_name_matches = [&](const std::string name) { return absl::EqualsIgnoreCase(name, name1) || absl::EqualsIgnoreCase(name, name2); }; if (either_name_matches(kH264CodecName)) return H264IsSameProfile(params1, params2) && H264IsSamePacketizationMode(params1, params2); if (either_name_matches(kVp9CodecName)) return VP9IsSameProfile(params1, params2); // https://aomediacodec.github.io/av1-rtp-spec/#723-usage-with-the-sdp-offeranswer-model // These media configuration parameters are asymmetrical and the answerer // MAY declare its own media configuration // TODO(bugs.webrtc.org/396434695): for backward compability we currently // compare profile. if (either_name_matches(kAv1CodecName)) return AV1IsSameProfile(params1, params2); #ifdef RTC_ENABLE_H265 if (either_name_matches(kH265CodecName)) { return H265IsSameProfile(params1, params2) && H265IsSameTier(params1, params2) && IsSameH265TxMode(params1, params2); } #endif return true; } bool ReferencedCodecsMatch(const std::vector& codecs1, const int codec1_id, const std::vector& codecs2, const int codec2_id) { const Codec* codec1 = FindCodecById(codecs1, codec1_id); const Codec* codec2 = FindCodecById(codecs2, codec2_id); return codec1 != nullptr && codec2 != nullptr && codec1->Matches(*codec2); } bool MatchesWithReferenceAttributesAndComparator( const Codec& codec_to_match, const Codec& potential_match, absl::AnyInvocable reference_comparator) { if (!MatchesWithCodecRules(codec_to_match, potential_match)) { return false; } Codec::ResiliencyType resiliency_type = codec_to_match.GetResiliencyType(); if (resiliency_type == Codec::ResiliencyType::kRtx) { int apt_value_1 = 0; int apt_value_2 = 0; if (!codec_to_match.GetParam(kCodecParamAssociatedPayloadType, &apt_value_1) || !potential_match.GetParam(kCodecParamAssociatedPayloadType, &apt_value_2)) { RTC_LOG(LS_WARNING) << "RTX missing associated payload type."; return false; } if (reference_comparator(apt_value_1, apt_value_2)) { return true; } return false; } if (resiliency_type == Codec::ResiliencyType::kRed) { auto red_parameters_1 = codec_to_match.params.find(kCodecParamNotInNameValueFormat); auto red_parameters_2 = potential_match.params.find(kCodecParamNotInNameValueFormat); bool has_parameters_1 = red_parameters_1 != codec_to_match.params.end(); bool has_parameters_2 = red_parameters_2 != potential_match.params.end(); // If codec_to_match has unassigned PT and no parameter, // we assume that it'll be assigned later and return a match. // Note - this should be deleted. It's untidy. if (potential_match.id == Codec::kIdNotSet && !has_parameters_2) { return true; } if (codec_to_match.id == Codec::kIdNotSet && !has_parameters_1) { return true; } if (has_parameters_1 && has_parameters_2) { // Different levels of redundancy between offer and answer are OK // since RED is considered to be declarative. std::vector redundant_payloads_1 = split(red_parameters_1->second, '/'); std::vector redundant_payloads_2 = split(red_parameters_2->second, '/'); // note: split returns at least 1 string even on empty strings. size_t smallest_size = std::min(redundant_payloads_1.size(), redundant_payloads_2.size()); // If the smaller list is equivalent to the longer list, we consider them // equivalent even if size differs. for (size_t i = 0; i < smallest_size; i++) { int red_value_1; int red_value_2; if (FromString(redundant_payloads_1[i], &red_value_1) && FromString(redundant_payloads_2[i], &red_value_2)) { if (!reference_comparator(red_value_1, red_value_2)) { return false; } } else { // At least one parameter was not an integer. // This is a syntax error, but we allow it here if the whole parameter // equals the other parameter, in order to not generate more errors // by duplicating the bad parameter. return red_parameters_1->second == red_parameters_2->second; } } return true; } if (!has_parameters_1 && !has_parameters_2) { // Both parameters are missing. Happens for video RED. return true; } return false; } return true; // Not a codec with a PT-valued reference. } CodecParameterMap InsertDefaultParams(const std::string& name, const CodecParameterMap& params) { CodecParameterMap updated_params = params; if (absl::EqualsIgnoreCase(name, kVp9CodecName)) { if (!HasParameter(params, kVP9FmtpProfileId)) { if (std::optional default_profile = ParseSdpForVP9Profile({})) { updated_params.insert( {kVP9FmtpProfileId, VP9ProfileToString(*default_profile)}); } } } if (absl::EqualsIgnoreCase(name, kAv1CodecName)) { if (!HasParameter(params, kAv1FmtpProfile)) { if (std::optional default_profile = ParseSdpForAV1Profile({})) { updated_params.insert( {kAv1FmtpProfile, AV1ProfileToString(*default_profile).data()}); } } if (!HasParameter(params, kAv1FmtpTier)) { updated_params.insert({kAv1FmtpTier, AV1GetTierOrDefault({})}); } if (!HasParameter(params, kAv1FmtpLevelIdx)) { updated_params.insert({kAv1FmtpLevelIdx, AV1GetLevelIdxOrDefault({})}); } } if (absl::EqualsIgnoreCase(name, kH264CodecName)) { if (!HasParameter(params, kH264FmtpPacketizationMode)) { updated_params.insert( {kH264FmtpPacketizationMode, H264GetPacketizationModeOrDefault({})}); } } #ifdef RTC_ENABLE_H265 if (absl::EqualsIgnoreCase(name, kH265CodecName)) { if (std::optional default_params = ParseSdpForH265ProfileTierLevel({})) { if (!HasParameter(params, kH265FmtpProfileId)) { updated_params.insert( {kH265FmtpProfileId, H265ProfileToString(default_params->profile)}); } if (!HasParameter(params, kH265FmtpLevelId)) { updated_params.insert( {kH265FmtpLevelId, H265LevelToString(default_params->level)}); } if (!HasParameter(params, kH265FmtpTierFlag)) { updated_params.insert( {kH265FmtpTierFlag, H265TierToString(default_params->tier)}); } } if (!HasParameter(params, kH265FmtpTxMode)) { updated_params.insert({kH265FmtpTxMode, GetH265TxModeOrDefault({})}); } } #endif return updated_params; } } // namespace bool MatchesWithCodecRules(const Codec& left_codec, const Codec& right_codec) { // Match the codec id/name based on the typical static/dynamic name rules. // Matching is case-insensitive. // We support the ranges [96, 127] and more recently [35, 65]. // https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-1 // Within those ranges we match by codec name, outside by codec id. // We also match by name if either ID is unassigned. // Since no codecs are assigned an id in the range [66, 95] by us, these will // never match. const int kLowerDynamicRangeMin = 35; const int kLowerDynamicRangeMax = 65; const int kUpperDynamicRangeMin = 96; const int kUpperDynamicRangeMax = 127; const bool is_id_in_dynamic_range = (left_codec.id >= kLowerDynamicRangeMin && left_codec.id <= kLowerDynamicRangeMax) || (left_codec.id >= kUpperDynamicRangeMin && left_codec.id <= kUpperDynamicRangeMax); const bool is_codec_id_in_dynamic_range = (right_codec.id >= kLowerDynamicRangeMin && right_codec.id <= kLowerDynamicRangeMax) || (right_codec.id >= kUpperDynamicRangeMin && right_codec.id <= kUpperDynamicRangeMax); bool matches_id; if ((is_id_in_dynamic_range && is_codec_id_in_dynamic_range) || left_codec.id == Codec::kIdNotSet || right_codec.id == Codec::kIdNotSet) { matches_id = absl::EqualsIgnoreCase(left_codec.name, right_codec.name); } else { matches_id = (left_codec.id == right_codec.id); } auto matches_type_specific = [&]() { switch (left_codec.type) { case Codec::Type::kAudio: // If a nonzero bitrate is specified, it must match the // actual bitrate, unless the codec is VBR (0), where we just force the // supplied value. The number of channels must match exactly, with the // exception that channels=0 is treated synonymously as channels=1, per // RFC 4566 section 6: " [The channels] parameter is OPTIONAL and may be // omitted if the number of channels is one." // Preference is ignored. return ((left_codec.clockrate == right_codec.clockrate) && (right_codec.bitrate == 0 || left_codec.bitrate <= 0 || left_codec.bitrate == right_codec.bitrate) && ((right_codec.channels < 2 && left_codec.channels < 2) || left_codec.channels == right_codec.channels)); case Codec::Type::kVideo: return IsSameCodecSpecific(left_codec.name, left_codec.params, right_codec.name, right_codec.params); } }; return matches_id && matches_type_specific(); } bool MatchesWithReferenceAttributes(const Codec& codec1, const Codec& codec2) { return MatchesWithReferenceAttributesAndComparator( codec1, codec2, [](int a, int b) { return a == b; }); } // Finds a codec in `codecs2` that matches `codec_to_match`, which is // a member of `codecs1`. If `codec_to_match` is an RED or RTX codec, both // the codecs themselves and their associated codecs must match. std::optional FindMatchingCodec(const std::vector& codecs1, const std::vector& codecs2, const Codec& codec_to_match) { // `codec_to_match` should be a member of `codecs1`, in order to look up // RED/RTX codecs' associated codecs correctly. If not, that's a programming // error. RTC_DCHECK(absl::c_any_of(codecs1, [&codec_to_match](const Codec& codec) { return &codec == &codec_to_match; })); for (const Codec& potential_match : codecs2) { if (MatchesWithReferenceAttributesAndComparator( codec_to_match, potential_match, [&codecs1, &codecs2](int a, int b) { return ReferencedCodecsMatch(codecs1, a, codecs2, b); })) { return potential_match; } } return std::nullopt; } bool IsSameRtpCodec(const Codec& codec, const RtpCodec& rtp_codec) { RtpCodecParameters rtp_codec2 = codec.ToCodecParameters(); return absl::EqualsIgnoreCase(rtp_codec.name, rtp_codec2.name) && rtp_codec.kind == rtp_codec2.kind && rtp_codec.num_channels == rtp_codec2.num_channels && rtp_codec.clock_rate == rtp_codec2.clock_rate && InsertDefaultParams(rtp_codec.name, rtp_codec.parameters) == InsertDefaultParams(rtp_codec2.name, rtp_codec2.parameters); } bool IsSameRtpCodecIgnoringLevel(const Codec& codec, const RtpCodec& rtp_codec) { RtpCodecParameters rtp_codec2 = codec.ToCodecParameters(); if (!absl::EqualsIgnoreCase(rtp_codec.name, rtp_codec2.name) || rtp_codec.kind != rtp_codec2.kind || rtp_codec.num_channels != rtp_codec2.num_channels || rtp_codec.clock_rate != rtp_codec2.clock_rate) { return false; } CodecParameterMap params1 = InsertDefaultParams(rtp_codec.name, rtp_codec.parameters); CodecParameterMap params2 = InsertDefaultParams(rtp_codec2.name, rtp_codec2.parameters); // Some video codecs are compatible with others (e.g. same profile but // different level). This comparison looks at the relevant parameters, // ignoring ones that are either irrelevant or unrecognized. if (rtp_codec.kind == MediaType::VIDEO && rtp_codec.IsMediaCodec()) { return IsSameCodecSpecific(rtp_codec.name, params1, rtp_codec2.name, params2); } // audio/RED should ignore the parameters which specify payload types so // can not be compared. if (rtp_codec.kind == MediaType::AUDIO && rtp_codec.name == kRedCodecName) { return true; } return params1 == params2; } } // namespace webrtc