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 <hta@webrtc.org>
Reviewed-by: Taylor Brandstetter <deadbeef@webrtc.org>
Commit-Queue: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35107}
diff --git a/media/base/media_constants.cc b/media/base/media_constants.cc
index 4e1fd1f..83be53a 100644
--- a/media/base/media_constants.cc
+++ b/media/base/media_constants.cc
@@ -39,6 +39,9 @@
 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 617ba44..313b0f8 100644
--- a/media/base/media_constants.h
+++ b/media/base/media_constants.h
@@ -42,6 +42,7 @@
 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 615dbff..6b8f254 100644
--- a/pc/media_session.cc
+++ b/pc/media_session.cc
@@ -654,6 +654,11 @@
 }
 
 template <class C>
+static bool IsRedCodec(const C& codec) {
+  return absl::EqualsIgnoreCase(codec.name, kRedCodecName);
+}
+
+template <class C>
 static bool IsRtxCodec(const C& codec) {
   return absl::EqualsIgnoreCase(codec.name, kRtxCodecName);
 }
@@ -800,6 +805,11 @@
         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 @@
 }
 
 // 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 <class C>
 static bool FindMatchingCodec(const std::vector<C>& codecs1,
                               const std::vector<C>& 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 @@
                                    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<std::string> redundant_payloads_1;
+          std::vector<std::string> 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 @@
 
 // Find the codec in `codec_list` that `rtx_codec` is associated with.
 template <class C>
-static const C* GetAssociatedCodec(const std::vector<C>& codec_list,
-                                   const C& rtx_codec) {
+static const C* GetAssociatedCodecForRtx(const std::vector<C>& codec_list,
+                                         const C& rtx_codec) {
   std::string associated_pt_str;
   if (!rtx_codec.GetParam(kCodecParamAssociatedPayloadType,
                           &associated_pt_str)) {
@@ -887,7 +946,7 @@
     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 @@
   return associated_codec;
 }
 
+// Find the codec in `codec_list` that `red_codec` is associated with.
+template <class C>
+static const C* GetAssociatedCodecForRed(const std::vector<C>& 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<std::string> 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 @@
 static void MergeCodecs(const std::vector<C>& reference_codecs,
                         std::vector<C>* 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<C>(reference_codecs, *offered_codecs,
                               reference_codec, nullptr)) {
       C codec = reference_codec;
@@ -915,14 +1013,14 @@
     }
   }
 
-  // Add all new RTX codecs.
+  // Add all new RTX or RED codecs.
   for (const C& reference_codec : reference_codecs) {
     if (IsRtxCodec(reference_codec) &&
         !FindMatchingCodec<C>(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 @@
           rtc::ToString(matching_codec.id);
       used_pltypes->FindAndSetIdUsed(&rtx_codec);
       offered_codecs->push_back(rtx_codec);
+    } else if (IsRedCodec(reference_codec) &&
+               !FindMatchingCodec<C>(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<C>(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 @@
   Codecs filtered_codecs;
   std::set<std::string> 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 @@
       }
     } 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 @@
             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<std::string> 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 067dc14..c5a4a30 100644
--- a/pc/peer_connection_media_unittest.cc
+++ b/pc/peer_connection_media_unittest.cc
@@ -1303,6 +1303,258 @@
             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<cricket::AudioCodec> caller_fake_codecs;
+  caller_fake_codecs.push_back(cricket::AudioCodec(100, "foo", 0, 0, 1));
+  auto caller_fake_engine = std::make_unique<FakeMediaEngine>();
+  caller_fake_engine->SetAudioCodecs(caller_fake_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine));
+
+  std::vector<cricket::AudioCodec> 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<FakeMediaEngine>();
+  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<cricket::AudioCodec> 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<FakeMediaEngine>();
+  caller_fake_engine->SetAudioCodecs(caller_fake_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine));
+
+  std::vector<cricket::AudioCodec> 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<FakeMediaEngine>();
+  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<cricket::AudioCodec> 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<FakeMediaEngine>();
+  caller_fake_engine->SetAudioCodecs(caller_fake_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine));
+
+  std::vector<cricket::AudioCodec> 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<FakeMediaEngine>();
+  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<cricket::AudioCodec> 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<FakeMediaEngine>();
+  caller_fake_engine->SetAudioCodecs(caller_fake_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine));
+
+  std::vector<cricket::AudioCodec> 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<FakeMediaEngine>();
+  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<cricket::AudioCodec> 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<FakeMediaEngine>();
+  caller_fake_engine->SetAudioCodecs(caller_fake_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine));
+
+  std::vector<cricket::AudioCodec> 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<FakeMediaEngine>();
+  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<cricket::AudioCodec> 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<FakeMediaEngine>();
+  caller_fake_engine->SetAudioCodecs(caller_fake_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(caller_fake_engine));
+
+  std::vector<cricket::AudioCodec> 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<FakeMediaEngine>();
+  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 <typename C>
 bool CompareCodecs(const std::vector<webrtc::RtpCodecCapability>& capabilities,
                    const std::vector<C>& codecs) {