| /* | |
| * 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 <algorithm> | |
| #include <cstddef> | |
| #include <optional> | |
| #include <string> | |
| #include <vector> | |
| #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<Codec>& codecs1, | |
| const int codec1_id, | |
| const std::vector<Codec>& 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<bool(int, int)> 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 (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<absl::string_view> redundant_payloads_1 = | |
| split(red_parameters_1->second, '/'); | |
| std::vector<absl::string_view> 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) { | |
| return true; | |
| } | |
| // Exactly one lacks parameters. | |
| // Allow match if it is an audio RED codec and at least one of the | |
| // codecs has an unassigned payload type or they have the same ID. | |
| if (codec_to_match.type == Codec::Type::kAudio && | |
| codec_to_match.name == kRedCodecName && | |
| (codec_to_match.id == Codec::kIdNotSet || | |
| potential_match.id == Codec::kIdNotSet || | |
| codec_to_match.id == potential_match.id)) { | |
| 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<VP9Profile> default_profile = | |
| ParseSdpForVP9Profile({})) { | |
| updated_params.insert( | |
| {kVP9FmtpProfileId, VP9ProfileToString(*default_profile)}); | |
| } | |
| } | |
| } | |
| if (absl::EqualsIgnoreCase(name, kAv1CodecName)) { | |
| if (!HasParameter(params, kAv1FmtpProfile)) { | |
| if (std::optional<AV1Profile> 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<H265ProfileTierLevel> 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); | |
| if (left_codec.type != right_codec.type) { | |
| return false; | |
| } | |
| 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<Codec> FindMatchingCodec(const std::vector<Codec>& codecs1, | |
| const std::vector<Codec>& 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(); | |
| 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; | |
| } | |
| // audio/RED should ignore the parameters which specify payload types so | |
| // can not be compared. | |
| if (rtp_codec.kind == MediaType::AUDIO && | |
| absl::EqualsIgnoreCase(rtp_codec.name, kRedCodecName)) { | |
| return true; | |
| } | |
| return 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 && | |
| absl::EqualsIgnoreCase(rtp_codec.name, kRedCodecName)) { | |
| return true; | |
| } | |
| return params1 == params2; | |
| } | |
| } // namespace webrtc |