From 7145a1421bd05e76db8e01b96cd91203e2fe211c Mon Sep 17 00:00:00 2001 From: Philipp Hancke Date: Tue, 28 Sep 2021 07:46:06 +0200 Subject: [PATCH] red: fix fmtp payload type collision handling along the lines of RTX handling but with limited support for missing fmtp lines because of video/red. BUG=webrtc:13178 Change-Id: Ia866c0e857da6da2ef1e4b81b51f90f534c7bb83 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/231948 Reviewed-by: Harald Alvestrand Reviewed-by: Taylor Brandstetter Commit-Queue: Harald Alvestrand Cr-Commit-Position: refs/heads/main@{#35107} --- media/base/media_constants.cc | 3 + media/base/media_constants.h | 1 + pc/media_session.cc | 155 ++++++++++++++-- pc/peer_connection_media_unittest.cc | 252 +++++++++++++++++++++++++++ 4 files changed, 400 insertions(+), 11 deletions(-) diff --git a/media/base/media_constants.cc b/media/base/media_constants.cc index 4e1fd1f09a..83be53ae09 100644 --- a/media/base/media_constants.cc +++ b/media/base/media_constants.cc @@ -39,6 +39,9 @@ const char kCodecParamRtxTime[] = "rtx-time"; const char kCodecParamAssociatedPayloadType[] = "apt"; const char kCodecParamAssociatedCodecName[] = "acn"; +// Parameters that do not follow the key-value convention +// are treated as having the empty string as key. +const char kCodecParamNotInNameValueFormat[] = ""; const char kOpusCodecName[] = "opus"; const char kIsacCodecName[] = "ISAC"; diff --git a/media/base/media_constants.h b/media/base/media_constants.h index 617ba44975..313b0f8df9 100644 --- a/media/base/media_constants.h +++ b/media/base/media_constants.h @@ -42,6 +42,7 @@ extern const char kCodecParamRtxTime[]; extern const char kCodecParamAssociatedPayloadType[]; extern const char kCodecParamAssociatedCodecName[]; +extern const char kCodecParamNotInNameValueFormat[]; extern const char kOpusCodecName[]; extern const char kIsacCodecName[]; diff --git a/pc/media_session.cc b/pc/media_session.cc index 615dbff6c1..6b8f2544ad 100644 --- a/pc/media_session.cc +++ b/pc/media_session.cc @@ -653,6 +653,11 @@ static bool ContainsRtxCodec(const std::vector& codecs) { return false; } +template +static bool IsRedCodec(const C& codec) { + return absl::EqualsIgnoreCase(codec.name, kRedCodecName); +} + template static bool IsRtxCodec(const C& codec) { return absl::EqualsIgnoreCase(codec.name, kRtxCodecName); @@ -800,6 +805,11 @@ static void NegotiateCodecs(const std::vector& local_codecs, if (rtx_time_it != theirs.params.end()) { negotiated.SetParam(kCodecParamRtxTime, rtx_time_it->second); } + } else if (IsRedCodec(negotiated)) { + const auto red_it = theirs.params.find(kCodecParamNotInNameValueFormat); + if (red_it != theirs.params.end()) { + negotiated.SetParam(kCodecParamNotInNameValueFormat, red_it->second); + } } if (absl::EqualsIgnoreCase(ours.name, kH264CodecName)) { webrtc::H264GenerateProfileLevelIdForAnswer(ours.params, theirs.params, @@ -829,15 +839,16 @@ static void NegotiateCodecs(const std::vector& local_codecs, } // Finds a codec in `codecs2` that matches `codec_to_match`, which is -// a member of `codecs1`. If `codec_to_match` is an RTX codec, both +// a member of `codecs1`. If `codec_to_match` is an RED or RTX codec, both // the codecs themselves and their associated codecs must match. template static bool FindMatchingCodec(const std::vector& codecs1, const std::vector& codecs2, const C& codec_to_match, C* found_codec) { - // `codec_to_match` should be a member of `codecs1`, in order to look up RTX - // codecs' associated codecs correctly. If not, that's a programming error. + // `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 C& codec) { return &codec == &codec_to_match; })); @@ -857,6 +868,54 @@ static bool FindMatchingCodec(const std::vector& codecs1, apt_value_2)) { continue; } + } else if (IsRedCodec(codec_to_match)) { + 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) { + // Mixed reference codecs (i.e. 111/112) are not supported. + // Different levels of redundancy between offer and answer are + // since RED is considered to be declarative. + std::vector redundant_payloads_1; + std::vector redundant_payloads_2; + rtc::split(red_parameters_1->second, '/', &redundant_payloads_1); + rtc::split(red_parameters_2->second, '/', &redundant_payloads_2); + if (redundant_payloads_1.size() > 0 && + redundant_payloads_2.size() > 0) { + bool consistent = true; + for (size_t i = 1; i < redundant_payloads_1.size(); i++) { + if (redundant_payloads_1[i] != redundant_payloads_1[0]) { + consistent = false; + break; + } + } + for (size_t i = 1; i < redundant_payloads_2.size(); i++) { + if (redundant_payloads_2[i] != redundant_payloads_2[0]) { + consistent = false; + break; + } + } + if (!consistent) { + continue; + } + + int red_value_1; + int red_value_2; + if (rtc::FromString(redundant_payloads_1[0], &red_value_1) && + rtc::FromString(redundant_payloads_2[0], &red_value_2)) { + if (!ReferencedCodecsMatch(codecs1, red_value_1, codecs2, + red_value_2)) { + continue; + } + } + } + } else if (has_parameters_1 != has_parameters_2) { + continue; + } } if (found_codec) { *found_codec = potential_match; @@ -869,8 +928,8 @@ static bool FindMatchingCodec(const std::vector& codecs1, // Find the codec in `codec_list` that `rtx_codec` is associated with. template -static const C* GetAssociatedCodec(const std::vector& codec_list, - const C& rtx_codec) { +static const C* GetAssociatedCodecForRtx(const std::vector& codec_list, + const C& rtx_codec) { std::string associated_pt_str; if (!rtx_codec.GetParam(kCodecParamAssociatedPayloadType, &associated_pt_str)) { @@ -887,7 +946,7 @@ static const C* GetAssociatedCodec(const std::vector& codec_list, return nullptr; } - // Find the associated reference codec for the reference RTX codec. + // Find the associated codec for the RTX codec. const C* associated_codec = FindCodecById(codec_list, associated_pt); if (!associated_codec) { RTC_LOG(LS_WARNING) << "Couldn't find associated codec with payload type " @@ -897,6 +956,43 @@ static const C* GetAssociatedCodec(const std::vector& codec_list, return associated_codec; } +// Find the codec in `codec_list` that `red_codec` is associated with. +template +static const C* GetAssociatedCodecForRed(const std::vector& codec_list, + const C& red_codec) { + std::string fmtp; + if (!red_codec.GetParam(kCodecParamNotInNameValueFormat, &fmtp)) { + // Normal for video/RED. + RTC_LOG(LS_WARNING) << "RED codec " << red_codec.name + << " is missing an associated payload type."; + return nullptr; + } + + std::vector redundant_payloads; + rtc::split(fmtp, '/', &redundant_payloads); + if (redundant_payloads.size() < 2) { + return nullptr; + } + + std::string associated_pt_str = redundant_payloads[0]; + int associated_pt; + if (!rtc::FromString(associated_pt_str, &associated_pt)) { + RTC_LOG(LS_WARNING) << "Couldn't convert first payload type " + << associated_pt_str << " of RED codec " + << red_codec.name << " to an integer."; + return nullptr; + } + + // Find the associated codec for the RED codec. + const C* associated_codec = FindCodecById(codec_list, associated_pt); + if (!associated_codec) { + RTC_LOG(LS_WARNING) << "Couldn't find associated codec with payload type " + << associated_pt << " for RED codec " << red_codec.name + << "."; + } + return associated_codec; +} + // Adds all codecs from `reference_codecs` to `offered_codecs` that don't // already exist in `offered_codecs` and ensure the payload types don't // collide. @@ -904,9 +1000,11 @@ template static void MergeCodecs(const std::vector& reference_codecs, std::vector* offered_codecs, UsedPayloadTypes* used_pltypes) { - // Add all new codecs that are not RTX codecs. + // Add all new codecs that are not RTX/RED codecs. + // The two-pass splitting of the loops means preferring payload types + // of actual codecs with respect to collisions. for (const C& reference_codec : reference_codecs) { - if (!IsRtxCodec(reference_codec) && + if (!IsRtxCodec(reference_codec) && !IsRedCodec(reference_codec) && !FindMatchingCodec(reference_codecs, *offered_codecs, reference_codec, nullptr)) { C codec = reference_codec; @@ -915,14 +1013,14 @@ static void MergeCodecs(const std::vector& reference_codecs, } } - // Add all new RTX codecs. + // Add all new RTX or RED codecs. for (const C& reference_codec : reference_codecs) { if (IsRtxCodec(reference_codec) && !FindMatchingCodec(reference_codecs, *offered_codecs, reference_codec, nullptr)) { C rtx_codec = reference_codec; const C* associated_codec = - GetAssociatedCodec(reference_codecs, rtx_codec); + GetAssociatedCodecForRtx(reference_codecs, rtx_codec); if (!associated_codec) { continue; } @@ -940,6 +1038,27 @@ static void MergeCodecs(const std::vector& reference_codecs, rtc::ToString(matching_codec.id); used_pltypes->FindAndSetIdUsed(&rtx_codec); offered_codecs->push_back(rtx_codec); + } else if (IsRedCodec(reference_codec) && + !FindMatchingCodec(reference_codecs, *offered_codecs, + reference_codec, nullptr)) { + C red_codec = reference_codec; + const C* associated_codec = + GetAssociatedCodecForRed(reference_codecs, red_codec); + if (associated_codec) { + C matching_codec; + if (!FindMatchingCodec(reference_codecs, *offered_codecs, + *associated_codec, &matching_codec)) { + RTC_LOG(LS_WARNING) << "Couldn't find matching " + << associated_codec->name << " codec."; + continue; + } + + red_codec.params[kCodecParamNotInNameValueFormat] = + rtc::ToString(matching_codec.id) + "/" + + rtc::ToString(matching_codec.id); + } + used_pltypes->FindAndSetIdUsed(&red_codec); + offered_codecs->push_back(red_codec); } } } @@ -956,6 +1075,7 @@ static Codecs MatchCodecPreference( Codecs filtered_codecs; std::set kept_codecs_ids; bool want_rtx = false; + bool want_red = false; for (const auto& codec_preference : codec_preferences) { auto found_codec = absl::c_find_if( @@ -980,10 +1100,12 @@ static Codecs MatchCodecPreference( } } else if (IsRtxCodec(codec_preference)) { want_rtx = true; + } else if (IsRedCodec(codec_preference)) { + want_red = true; } } - if (want_rtx) { + if (want_rtx || want_red) { for (const auto& codec : codecs) { if (IsRtxCodec(codec)) { const auto apt = @@ -992,6 +1114,17 @@ static Codecs MatchCodecPreference( kept_codecs_ids.count(apt->second) > 0) { filtered_codecs.push_back(codec); } + } else if (IsRedCodec(codec)) { + const auto fmtp = + codec.params.find(cricket::kCodecParamNotInNameValueFormat); + if (fmtp != codec.params.end()) { + std::vector redundant_payloads; + rtc::split(fmtp->second, '/', &redundant_payloads); + if (redundant_payloads.size() > 0 && + kept_codecs_ids.count(redundant_payloads[0]) > 0) { + filtered_codecs.push_back(codec); + } + } } } } diff --git a/pc/peer_connection_media_unittest.cc b/pc/peer_connection_media_unittest.cc index 067dc14c50..c5a4a303b3 100644 --- a/pc/peer_connection_media_unittest.cc +++ b/pc/peer_connection_media_unittest.cc @@ -1303,6 +1303,258 @@ TEST_P(PeerConnectionMediaTest, audio_options.combined_audio_video_bwe); } +// Test that if a RED codec refers to another codec in its fmtp line, but that +// codec's payload type was reassigned for some reason (either the remote +// endpoint selected a different payload type or there was a conflict), the RED +// fmtp line is modified to refer to the correct payload type. +TEST_P(PeerConnectionMediaTest, RedFmtpPayloadTypeReassigned) { + std::vector caller_fake_codecs; + caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1)); + auto caller_fake_engine = std::make_unique(); + caller_fake_engine->SetAudioCodecs(caller_fake_codecs); + auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine)); + + std::vector callee_fake_codecs; + callee_fake_codecs.push_back(cricket::AudioCodec(120, "foo", 0, 0, 1)); + callee_fake_codecs.push_back( + cricket::AudioCodec(121, cricket::kRedCodecName, 0, 0, 1)); + callee_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "120/120"); + auto callee_fake_engine = std::make_unique(); + callee_fake_engine->SetAudioCodecs(callee_fake_codecs); + auto callee = CreatePeerConnectionWithAudio(std::move(callee_fake_engine)); + + // Offer from the caller establishes 100 as the "foo" payload type. + auto offer = caller->CreateOfferAndSetAsLocal(); + callee->SetRemoteDescription(std::move(offer)); + auto answer = callee->CreateAnswerAndSetAsLocal(); + auto answer_description = + cricket::GetFirstAudioContentDescription(answer->description()); + ASSERT_EQ(1u, answer_description->codecs().size()); + + // Offer from the callee should respect the established payload type, and + // attempt to add RED, which should refer to the correct payload type. + offer = callee->CreateOfferAndSetAsLocal(); + auto* offer_description = + cricket::GetFirstAudioContentDescription(offer->description()); + ASSERT_EQ(2u, offer_description->codecs().size()); + for (const auto& codec : offer_description->codecs()) { + if (codec.name == "foo") { + ASSERT_EQ(100, codec.id); + } else if (codec.name == cricket::kRedCodecName) { + std::string fmtp; + ASSERT_TRUE(codec.GetParam("", &fmtp)); + EXPECT_EQ("100/100", fmtp); + } + } +} + +// Test that RED without fmtp does match RED without fmtp. +TEST_P(PeerConnectionMediaTest, RedFmtpPayloadTypeNoFmtpMatchNoFmtp) { + std::vector caller_fake_codecs; + caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1)); + caller_fake_codecs.push_back( + cricket::AudioCodec(101, cricket::kRedCodecName, 0, 0, 1)); + auto caller_fake_engine = std::make_unique(); + caller_fake_engine->SetAudioCodecs(caller_fake_codecs); + auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine)); + + std::vector callee_fake_codecs; + callee_fake_codecs.push_back(cricket::AudioCodec(120, "foo", 0, 0, 1)); + callee_fake_codecs.push_back( + cricket::AudioCodec(121, cricket::kRedCodecName, 0, 0, 1)); + auto callee_fake_engine = std::make_unique(); + callee_fake_engine->SetAudioCodecs(callee_fake_codecs); + auto callee = CreatePeerConnectionWithAudio(std::move(callee_fake_engine)); + + // Offer from the caller establishes 100 as the "foo" payload type. + // Red (without fmtp) is negotiated. + auto offer = caller->CreateOfferAndSetAsLocal(); + callee->SetRemoteDescription(std::move(offer)); + auto answer = callee->CreateAnswerAndSetAsLocal(); + auto answer_description = + cricket::GetFirstAudioContentDescription(answer->description()); + ASSERT_EQ(2u, answer_description->codecs().size()); + + // Offer from the callee should respect the established payload type, and + // attempt to add RED. + offer = callee->CreateOfferAndSetAsLocal(); + auto* offer_description = + cricket::GetFirstAudioContentDescription(offer->description()); + ASSERT_EQ(2u, offer_description->codecs().size()); + for (const auto& codec : offer_description->codecs()) { + if (codec.name == "foo") { + ASSERT_EQ(100, codec.id); + } else if (codec.name == cricket::kRedCodecName) { + ASSERT_EQ(101, codec.id); + } + } +} + +// Test that RED without fmtp does not match RED with fmtp. +TEST_P(PeerConnectionMediaTest, RedFmtpPayloadTypeNoFmtpNoMatchFmtp) { + std::vector caller_fake_codecs; + caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1)); + caller_fake_codecs.push_back( + cricket::AudioCodec(101, cricket::kRedCodecName, 0, 0, 1)); + auto caller_fake_engine = std::make_unique(); + caller_fake_engine->SetAudioCodecs(caller_fake_codecs); + auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine)); + + std::vector callee_fake_codecs; + callee_fake_codecs.push_back(cricket::AudioCodec(120, "foo", 0, 0, 1)); + callee_fake_codecs.push_back( + cricket::AudioCodec(121, cricket::kRedCodecName, 0, 0, 1)); + callee_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "120/120"); + auto callee_fake_engine = std::make_unique(); + callee_fake_engine->SetAudioCodecs(callee_fake_codecs); + auto callee = CreatePeerConnectionWithAudio(std::move(callee_fake_engine)); + + // Offer from the caller establishes 100 as the "foo" payload type. + // It should not negotiate RED. + auto offer = caller->CreateOfferAndSetAsLocal(); + callee->SetRemoteDescription(std::move(offer)); + auto answer = callee->CreateAnswerAndSetAsLocal(); + auto answer_description = + cricket::GetFirstAudioContentDescription(answer->description()); + ASSERT_EQ(1u, answer_description->codecs().size()); + + // Offer from the callee should respect the established payload type, and + // attempt to add RED, which should refer to the correct payload type. + offer = callee->CreateOfferAndSetAsLocal(); + auto* offer_description = + cricket::GetFirstAudioContentDescription(offer->description()); + ASSERT_EQ(2u, offer_description->codecs().size()); + for (const auto& codec : offer_description->codecs()) { + if (codec.name == "foo") { + ASSERT_EQ(100, codec.id); + } else if (codec.name == cricket::kRedCodecName) { + std::string fmtp; + ASSERT_TRUE( + codec.GetParam(cricket::kCodecParamNotInNameValueFormat, &fmtp)); + EXPECT_EQ("100/100", fmtp); + } + } +} + +// Test that RED with fmtp must match base codecs. +TEST_P(PeerConnectionMediaTest, RedFmtpPayloadTypeMustMatchBaseCodecs) { + std::vector caller_fake_codecs; + caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1)); + caller_fake_codecs.push_back( + cricket::AudioCodec(101, cricket::kRedCodecName, 0, 0, 1)); + caller_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "100/100"); + auto caller_fake_engine = std::make_unique(); + caller_fake_engine->SetAudioCodecs(caller_fake_codecs); + auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine)); + + std::vector callee_fake_codecs; + callee_fake_codecs.push_back(cricket::AudioCodec(120, "foo", 0, 0, 1)); + callee_fake_codecs.push_back( + cricket::AudioCodec(121, cricket::kRedCodecName, 0, 0, 1)); + callee_fake_codecs.push_back(cricket::AudioCodec(122, "bar", 0, 0, 1)); + callee_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "122/122"); + auto callee_fake_engine = std::make_unique(); + callee_fake_engine->SetAudioCodecs(callee_fake_codecs); + auto callee = CreatePeerConnectionWithAudio(std::move(callee_fake_engine)); + + // Offer from the caller establishes 100 as the "foo" payload type. + // It should not negotiate RED since RED is associated with foo, not bar. + auto offer = caller->CreateOfferAndSetAsLocal(); + callee->SetRemoteDescription(std::move(offer)); + auto answer = callee->CreateAnswerAndSetAsLocal(); + auto answer_description = + cricket::GetFirstAudioContentDescription(answer->description()); + ASSERT_EQ(1u, answer_description->codecs().size()); +} + +// Test behaviour when the RED fmtp attempts to specify different codecs +// which is not supported. +TEST_P(PeerConnectionMediaTest, RedFmtpPayloadMixed) { + std::vector caller_fake_codecs; + caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1)); + caller_fake_codecs.push_back(cricket::AudioCodec(102, "bar", 0, 0, 1)); + caller_fake_codecs.push_back( + cricket::AudioCodec(101, cricket::kRedCodecName, 0, 0, 1)); + caller_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "100/102"); + auto caller_fake_engine = std::make_unique(); + caller_fake_engine->SetAudioCodecs(caller_fake_codecs); + auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine)); + + std::vector callee_fake_codecs; + callee_fake_codecs.push_back(cricket::AudioCodec(120, "foo", 0, 0, 1)); + callee_fake_codecs.push_back( + cricket::AudioCodec(121, cricket::kRedCodecName, 0, 0, 1)); + callee_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "120/120"); + auto callee_fake_engine = std::make_unique(); + callee_fake_engine->SetAudioCodecs(callee_fake_codecs); + auto callee = CreatePeerConnectionWithAudio(std::move(callee_fake_engine)); + + // Offer from the caller establishes 100 as the "foo" payload type. + auto offer = caller->CreateOfferAndSetAsLocal(); + callee->SetRemoteDescription(std::move(offer)); + auto answer = callee->CreateAnswerAndSetAsLocal(); + auto answer_description = + cricket::GetFirstAudioContentDescription(answer->description()); + // RED is not negotiated. + ASSERT_EQ(1u, answer_description->codecs().size()); +} + +// Test behaviour when the RED fmtp attempts to negotiate different levels of +// redundancy. +TEST_P(PeerConnectionMediaTest, RedFmtpPayloadDifferentRedundancy) { + std::vector caller_fake_codecs; + caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1)); + caller_fake_codecs.push_back( + cricket::AudioCodec(101, cricket::kRedCodecName, 0, 0, 1)); + caller_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "100/100"); + auto caller_fake_engine = std::make_unique(); + caller_fake_engine->SetAudioCodecs(caller_fake_codecs); + auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine)); + + std::vector callee_fake_codecs; + callee_fake_codecs.push_back(cricket::AudioCodec(120, "foo", 0, 0, 1)); + callee_fake_codecs.push_back( + cricket::AudioCodec(121, cricket::kRedCodecName, 0, 0, 1)); + callee_fake_codecs.back().SetParam(cricket::kCodecParamNotInNameValueFormat, + "120/120/120"); + auto callee_fake_engine = std::make_unique(); + callee_fake_engine->SetAudioCodecs(callee_fake_codecs); + auto callee = CreatePeerConnectionWithAudio(std::move(callee_fake_engine)); + + // Offer from the caller establishes 100 as the "foo" payload type. + auto offer = caller->CreateOfferAndSetAsLocal(); + callee->SetRemoteDescription(std::move(offer)); + auto answer = callee->CreateAnswerAndSetAsLocal(); + auto answer_description = + cricket::GetFirstAudioContentDescription(answer->description()); + // RED is negotiated. + ASSERT_EQ(2u, answer_description->codecs().size()); + + // Offer from the callee should respect the established payload type, and + // attempt to add RED, which should refer to the correct payload type. + offer = callee->CreateOfferAndSetAsLocal(); + auto* offer_description = + cricket::GetFirstAudioContentDescription(offer->description()); + ASSERT_EQ(2u, offer_description->codecs().size()); + for (const auto& codec : offer_description->codecs()) { + if (codec.name == "foo") { + ASSERT_EQ(100, codec.id); + } else if (codec.name == cricket::kRedCodecName) { + std::string fmtp; + ASSERT_TRUE( + codec.GetParam(cricket::kCodecParamNotInNameValueFormat, &fmtp)); + EXPECT_EQ("100/100", fmtp); + } + } +} + template bool CompareCodecs(const std::vector& capabilities, const std::vector& codecs) {