webrtc_m130/media/base/codec_comparators.cc
Henrik Boström ede69fd577 Make IsSameRtpCodecIgnoringLevel work for any codec.
Prior to this CL, IsSameRtpCodecIgnoringLevel() only ignored level IDs
if the codec was H265, incorrectly considering, for example, different
levels of H264 Baseline as not equal.
- This CL fixes that problem by using IsSameCodecSpecific() which is
  already used in other places, reducing the risk of different
  comparisons using different comparison rules.

This also fixes https://crbug.com/webrtc/391340599 where
setParameters() would throw if unrecognized SDP FMTP parameters were
added to a codec as part of SDP negotiation via SDP munging.

This CL makes the following WPT tests pass:
- external/wpt/webrtc/protocol/h264-unidirectional-codec-offer.https.html
- fast/peerconnection/RTCRtpSender-setParameters.html

Bug: chromium:381407888, webrtc:391340599
Change-Id: I5991403b56c86ba97e670996c6687f6315dde304
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/374043
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Henrik Boström <hbos@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#43797}
2025-01-24 05:37:17 -08:00

415 lines
17 KiB
C++

/*
* 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/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 {
using cricket::Codec;
// 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, cricket::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, cricket::kAv1FmtpTier, "0");
}
bool AV1IsSameTier(const CodecParameterMap& left,
const CodecParameterMap& right) {
return AV1GetTierOrDefault(left) == AV1GetTierOrDefault(right);
}
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, cricket::kAv1FmtpLevelIdx, "5");
}
bool AV1IsSameLevelIdx(const CodecParameterMap& left,
const CodecParameterMap& right) {
return AV1GetLevelIdxOrDefault(left) == AV1GetLevelIdxOrDefault(right);
}
#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, cricket::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(cricket::kH264CodecName))
return H264IsSameProfile(params1, params2) &&
H264IsSamePacketizationMode(params1, params2);
if (either_name_matches(cricket::kVp9CodecName))
return VP9IsSameProfile(params1, params2);
if (either_name_matches(cricket::kAv1CodecName))
return AV1IsSameProfile(params1, params2) &&
AV1IsSameTier(params1, params2) &&
AV1IsSameLevelIdx(params1, params2);
#ifdef RTC_ENABLE_H265
if (either_name_matches(cricket::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(cricket::kCodecParamAssociatedPayloadType,
&apt_value_1) ||
!potential_match.GetParam(cricket::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(cricket::kCodecParamNotInNameValueFormat);
auto red_parameters_2 =
potential_match.params.find(cricket::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<absl::string_view> redundant_payloads_1 =
rtc::split(red_parameters_1->second, '/');
std::vector<absl::string_view> redundant_payloads_2 =
rtc::split(red_parameters_2->second, '/');
// note: rtc::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 (rtc::FromString(redundant_payloads_1[i], &red_value_1) &&
rtc::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, cricket::kVp9CodecName)) {
if (!HasParameter(params, kVP9FmtpProfileId)) {
if (std::optional<VP9Profile> default_profile =
ParseSdpForVP9Profile({})) {
updated_params.insert(
{kVP9FmtpProfileId, VP9ProfileToString(*default_profile)});
}
}
}
if (absl::EqualsIgnoreCase(name, cricket::kAv1CodecName)) {
if (!HasParameter(params, cricket::kAv1FmtpProfile)) {
if (std::optional<AV1Profile> default_profile =
ParseSdpForAV1Profile({})) {
updated_params.insert({cricket::kAv1FmtpProfile,
AV1ProfileToString(*default_profile).data()});
}
}
if (!HasParameter(params, cricket::kAv1FmtpTier)) {
updated_params.insert({cricket::kAv1FmtpTier, AV1GetTierOrDefault({})});
}
if (!HasParameter(params, cricket::kAv1FmtpLevelIdx)) {
updated_params.insert(
{cricket::kAv1FmtpLevelIdx, AV1GetLevelIdxOrDefault({})});
}
}
if (absl::EqualsIgnoreCase(name, cricket::kH264CodecName)) {
if (!HasParameter(params, cricket::kH264FmtpPacketizationMode)) {
updated_params.insert({cricket::kH264FmtpPacketizationMode,
H264GetPacketizationModeOrDefault({})});
}
}
#ifdef RTC_ENABLE_H265
if (absl::EqualsIgnoreCase(name, cricket::kH265CodecName)) {
if (std::optional<H265ProfileTierLevel> default_params =
ParseSdpForH265ProfileTierLevel({})) {
if (!HasParameter(params, cricket::kH265FmtpProfileId)) {
updated_params.insert({cricket::kH265FmtpProfileId,
H265ProfileToString(default_params->profile)});
}
if (!HasParameter(params, cricket::kH265FmtpLevelId)) {
updated_params.insert({cricket::kH265FmtpLevelId,
H265LevelToString(default_params->level)});
}
if (!HasParameter(params, cricket::kH265FmtpTierFlag)) {
updated_params.insert({cricket::kH265FmtpTierFlag,
H265TierToString(default_params->tier)});
}
}
if (!HasParameter(params, cricket::kH265FmtpTxMode)) {
updated_params.insert(
{cricket::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 clockrate is specified, it must match the actual
// clockrate. 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.
// TODO(juberti): Treat a zero clockrate as 8000Hz, the RTP default
// clockrate.
return ((right_codec.clockrate == 0 /*&& clockrate == 8000*/) ||
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();
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 == cricket::MediaType::MEDIA_TYPE_VIDEO &&
rtp_codec.IsMediaCodec()) {
return IsSameCodecSpecific(rtp_codec.name, params1, rtp_codec2.name,
params2);
}
return params1 == params2;
}
} // namespace webrtc