[Unified Plan] Fix issues with recycling m= sections

Previously, the PeerConnection would look at the pending local
and remote descriptions also to determine if an m= section is
recycled. That is not quite spec compliant and breaks down under
some edge cases. This changes the PeerConnection to look only at
the *current* local or remote description (i.e., the descriptions
from the last time the PeerConnection was in a stable signaling
state) to determine if an m= section is recycled.

Additionally, the MediaSessionFactory only looked at the local
description to determine if an m= section is recycled. The full
criteria requires looking at the current local and current remote
m= sections. This change adds a state enum to the
MediaDescriptionOptions so that the MediaSessionFactory knows if
a media section is being recycled without duplicating the logic
in PeerConnection.

Tests are added to cover additional edge cases.

Bug: chromium:899680
Change-Id: I5bcf0f88957a61653269ed8bb50b2018500bc1d5
Reviewed-on: https://webrtc-review.googlesource.com/c/111293
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#25959}
diff --git a/pc/mediasession.cc b/pc/mediasession.cc
index 7239af8..263f369 100644
--- a/pc/mediasession.cc
+++ b/pc/mediasession.cc
@@ -18,6 +18,7 @@
 #include <unordered_map>
 #include <utility>
 
+#include "absl/memory/memory.h"
 #include "absl/strings/match.h"
 #include "absl/types/optional.h"
 #include "api/cryptoparams.h"
@@ -289,20 +290,15 @@
 }
 
 // Finds all StreamParams of all media types and attach them to stream_params.
-static void GetCurrentStreamParams(const SessionDescription* sdesc,
-                                   StreamParamsVec* stream_params) {
-  RTC_DCHECK(stream_params);
-  if (!sdesc) {
-    return;
-  }
-  for (const ContentInfo& content : sdesc->contents()) {
-    if (!content.media_description()) {
-      continue;
-    }
-    for (const StreamParams& params : content.media_description()->streams()) {
-      stream_params->push_back(params);
+static StreamParamsVec GetCurrentStreamParams(
+    const std::vector<const ContentInfo*>& active_local_contents) {
+  StreamParamsVec stream_params;
+  for (const ContentInfo* content : active_local_contents) {
+    for (const StreamParams& params : content->media_description()->streams()) {
+      stream_params.push_back(params);
     }
   }
+  return stream_params;
 }
 
 // Filters the data codecs for the data channel type.
@@ -642,6 +638,23 @@
   return true;
 }
 
+static std::vector<const ContentInfo*> GetActiveContents(
+    const SessionDescription& description,
+    const MediaSessionOptions& session_options) {
+  std::vector<const ContentInfo*> active_contents;
+  for (size_t i = 0; i < description.contents().size(); ++i) {
+    RTC_DCHECK_LT(i, session_options.media_description_options.size());
+    const ContentInfo& content = description.contents()[i];
+    const MediaDescriptionOptions& media_options =
+        session_options.media_description_options[i];
+    if (!content.rejected && !media_options.stopped &&
+        content.name == media_options.mid) {
+      active_contents.push_back(&content);
+    }
+  }
+  return active_contents;
+}
+
 template <class C>
 static bool ContainsRtxCodec(const std::vector<C>& codecs) {
   for (const auto& codec : codecs) {
@@ -1265,17 +1278,28 @@
 SessionDescription* MediaSessionDescriptionFactory::CreateOffer(
     const MediaSessionOptions& session_options,
     const SessionDescription* current_description) const {
-  std::unique_ptr<SessionDescription> offer(new SessionDescription());
+  // Must have options for each existing section.
+  if (current_description) {
+    RTC_DCHECK_LE(current_description->contents().size(),
+                  session_options.media_description_options.size());
+  }
 
   IceCredentialsIterator ice_credentials(
       session_options.pooled_ice_credentials);
-  StreamParamsVec current_streams;
-  GetCurrentStreamParams(current_description, &current_streams);
+
+  std::vector<const ContentInfo*> current_active_contents;
+  if (current_description) {
+    current_active_contents =
+        GetActiveContents(*current_description, session_options);
+  }
+
+  StreamParamsVec current_streams =
+      GetCurrentStreamParams(current_active_contents);
 
   AudioCodecs offer_audio_codecs;
   VideoCodecs offer_video_codecs;
   DataCodecs offer_data_codecs;
-  GetCodecsForOffer(current_description, &offer_audio_codecs,
+  GetCodecsForOffer(current_active_contents, &offer_audio_codecs,
                     &offer_video_codecs, &offer_data_codecs);
 
   if (!session_options.vad_enabled) {
@@ -1287,14 +1311,10 @@
 
   RtpHeaderExtensions audio_rtp_extensions;
   RtpHeaderExtensions video_rtp_extensions;
-  GetRtpHdrExtsToOffer(session_options, current_description,
+  GetRtpHdrExtsToOffer(current_active_contents, session_options.is_unified_plan,
                        &audio_rtp_extensions, &video_rtp_extensions);
 
-  // Must have options for each existing section.
-  if (current_description) {
-    RTC_DCHECK(current_description->contents().size() <=
-               session_options.media_description_options.size());
-  }
+  auto offer = absl::make_unique<SessionDescription>();
 
   // Iterate through the media description options, matching with existing media
   // descriptions in |current_description|.
@@ -1306,7 +1326,7 @@
         msection_index < current_description->contents().size()) {
       current_content = &current_description->contents()[msection_index];
       // Media type must match unless this media section is being recycled.
-      RTC_DCHECK(current_content->rejected ||
+      RTC_DCHECK(current_content->name != media_description_options.mid ||
                  IsMediaContentOfType(current_content,
                                       media_description_options.type));
     }
@@ -1391,25 +1411,21 @@
     return nullptr;
   }
 
+  // Must have options for exactly as many sections as in the offer.
+  RTC_DCHECK_EQ(offer->contents().size(),
+                session_options.media_description_options.size());
+
   IceCredentialsIterator ice_credentials(
       session_options.pooled_ice_credentials);
 
-  // The answer contains the intersection of the codecs in the offer with the
-  // codecs we support. As indicated by XEP-0167, we retain the same payload ids
-  // from the offer in the answer.
-  std::unique_ptr<SessionDescription> answer(new SessionDescription());
+  std::vector<const ContentInfo*> current_active_contents;
+  if (current_description) {
+    current_active_contents =
+        GetActiveContents(*current_description, session_options);
+  }
 
-  StreamParamsVec current_streams;
-  GetCurrentStreamParams(current_description, &current_streams);
-
-  // If the offer supports BUNDLE, and we want to use it too, create a BUNDLE
-  // group in the answer with the appropriate content names.
-  const ContentGroup* offer_bundle = offer->GetGroupByName(GROUP_TYPE_BUNDLE);
-  ContentGroup answer_bundle(GROUP_TYPE_BUNDLE);
-  // Transport info shared by the bundle group.
-  std::unique_ptr<TransportInfo> bundle_transport;
-
-  answer->set_extmap_allow_mixed(offer->extmap_allow_mixed());
+  StreamParamsVec current_streams =
+      GetCurrentStreamParams(current_active_contents);
 
   // Get list of all possible codecs that respects existing payload type
   // mappings and uses a single payload type space.
@@ -1420,7 +1436,7 @@
   AudioCodecs answer_audio_codecs;
   VideoCodecs answer_video_codecs;
   DataCodecs answer_data_codecs;
-  GetCodecsForAnswer(current_description, offer, &answer_audio_codecs,
+  GetCodecsForAnswer(current_active_contents, *offer, &answer_audio_codecs,
                      &answer_video_codecs, &answer_data_codecs);
 
   if (!session_options.vad_enabled) {
@@ -1430,9 +1446,17 @@
   FilterDataCodecs(&answer_data_codecs,
                    session_options.data_channel_type == DCT_SCTP);
 
-  // Must have options for exactly as many sections as in the offer.
-  RTC_DCHECK(offer->contents().size() ==
-             session_options.media_description_options.size());
+  auto answer = absl::make_unique<SessionDescription>();
+
+  // If the offer supports BUNDLE, and we want to use it too, create a BUNDLE
+  // group in the answer with the appropriate content names.
+  const ContentGroup* offer_bundle = offer->GetGroupByName(GROUP_TYPE_BUNDLE);
+  ContentGroup answer_bundle(GROUP_TYPE_BUNDLE);
+  // Transport info shared by the bundle group.
+  std::unique_ptr<TransportInfo> bundle_transport;
+
+  answer->set_extmap_allow_mixed(offer->extmap_allow_mixed());
+
   // Iterate through the media description options, matching with existing
   // media descriptions in |current_description|.
   size_t msection_index = 0;
@@ -1589,24 +1613,24 @@
   return audio_sendrecv_codecs_;
 }
 
-void MergeCodecsFromDescription(const SessionDescription* description,
-                                AudioCodecs* audio_codecs,
-                                VideoCodecs* video_codecs,
-                                DataCodecs* data_codecs,
-                                UsedPayloadTypes* used_pltypes) {
-  RTC_DCHECK(description);
-  for (const ContentInfo& content : description->contents()) {
-    if (IsMediaContentOfType(&content, MEDIA_TYPE_AUDIO)) {
+void MergeCodecsFromDescription(
+    const std::vector<const ContentInfo*>& current_active_contents,
+    AudioCodecs* audio_codecs,
+    VideoCodecs* video_codecs,
+    DataCodecs* data_codecs,
+    UsedPayloadTypes* used_pltypes) {
+  for (const ContentInfo* content : current_active_contents) {
+    if (IsMediaContentOfType(content, MEDIA_TYPE_AUDIO)) {
       const AudioContentDescription* audio =
-          content.media_description()->as_audio();
+          content->media_description()->as_audio();
       MergeCodecs<AudioCodec>(audio->codecs(), audio_codecs, used_pltypes);
-    } else if (IsMediaContentOfType(&content, MEDIA_TYPE_VIDEO)) {
+    } else if (IsMediaContentOfType(content, MEDIA_TYPE_VIDEO)) {
       const VideoContentDescription* video =
-          content.media_description()->as_video();
+          content->media_description()->as_video();
       MergeCodecs<VideoCodec>(video->codecs(), video_codecs, used_pltypes);
-    } else if (IsMediaContentOfType(&content, MEDIA_TYPE_DATA)) {
+    } else if (IsMediaContentOfType(content, MEDIA_TYPE_DATA)) {
       const DataContentDescription* data =
-          content.media_description()->as_data();
+          content->media_description()->as_data();
       MergeCodecs<DataCodec>(data->codecs(), data_codecs, used_pltypes);
     }
   }
@@ -1619,24 +1643,18 @@
 // 3. For each individual media description (m= section), filter codecs based
 //    on the directional attribute (happens in another method).
 void MediaSessionDescriptionFactory::GetCodecsForOffer(
-    const SessionDescription* current_description,
+    const std::vector<const ContentInfo*>& current_active_contents,
     AudioCodecs* audio_codecs,
     VideoCodecs* video_codecs,
     DataCodecs* data_codecs) const {
-  UsedPayloadTypes used_pltypes;
-  audio_codecs->clear();
-  video_codecs->clear();
-  data_codecs->clear();
-
   // First - get all codecs from the current description if the media type
   // is used. Add them to |used_pltypes| so the payload type is not reused if a
   // new media type is added.
-  if (current_description) {
-    MergeCodecsFromDescription(current_description, audio_codecs, video_codecs,
-                               data_codecs, &used_pltypes);
-  }
+  UsedPayloadTypes used_pltypes;
+  MergeCodecsFromDescription(current_active_contents, audio_codecs,
+                             video_codecs, data_codecs, &used_pltypes);
 
-  // Add our codecs that are not in |current_description|.
+  // Add our codecs that are not in the current description.
   MergeCodecs<AudioCodec>(all_audio_codecs_, audio_codecs, &used_pltypes);
   MergeCodecs<VideoCodec>(video_codecs_, video_codecs, &used_pltypes);
   MergeCodecs<DataCodec>(data_codecs_, data_codecs, &used_pltypes);
@@ -1650,29 +1668,23 @@
 // 4. For each individual media description (m= section), filter codecs based
 //    on the directional attribute (happens in another method).
 void MediaSessionDescriptionFactory::GetCodecsForAnswer(
-    const SessionDescription* current_description,
-    const SessionDescription* remote_offer,
+    const std::vector<const ContentInfo*>& current_active_contents,
+    const SessionDescription& remote_offer,
     AudioCodecs* audio_codecs,
     VideoCodecs* video_codecs,
     DataCodecs* data_codecs) const {
-  UsedPayloadTypes used_pltypes;
-  audio_codecs->clear();
-  video_codecs->clear();
-  data_codecs->clear();
-
   // First - get all codecs from the current description if the media type
   // is used. Add them to |used_pltypes| so the payload type is not reused if a
   // new media type is added.
-  if (current_description) {
-    MergeCodecsFromDescription(current_description, audio_codecs, video_codecs,
-                               data_codecs, &used_pltypes);
-  }
+  UsedPayloadTypes used_pltypes;
+  MergeCodecsFromDescription(current_active_contents, audio_codecs,
+                             video_codecs, data_codecs, &used_pltypes);
 
   // Second - filter out codecs that we don't support at all and should ignore.
   AudioCodecs filtered_offered_audio_codecs;
   VideoCodecs filtered_offered_video_codecs;
   DataCodecs filtered_offered_data_codecs;
-  for (const ContentInfo& content : remote_offer->contents()) {
+  for (const ContentInfo& content : remote_offer.contents()) {
     if (IsMediaContentOfType(&content, MEDIA_TYPE_AUDIO)) {
       const AudioContentDescription* audio =
           content.media_description()->as_audio();
@@ -1712,7 +1724,7 @@
     }
   }
 
-  // Add codecs that are not in |current_description| but were in
+  // Add codecs that are not in the current description but were in
   // |remote_offer|.
   MergeCodecs<AudioCodec>(filtered_offered_audio_codecs, audio_codecs,
                           &used_pltypes);
@@ -1722,9 +1734,10 @@
                          &used_pltypes);
 }
 
+// TODO(steveanton): Replace |is_unified_plan| flag with a member variable.
 void MediaSessionDescriptionFactory::GetRtpHdrExtsToOffer(
-    const MediaSessionOptions& session_options,
-    const SessionDescription* current_description,
+    const std::vector<const ContentInfo*>& current_active_contents,
+    bool is_unified_plan,
     RtpHeaderExtensions* offer_audio_extensions,
     RtpHeaderExtensions* offer_video_extensions) const {
   // All header extensions allocated from the same range to avoid potential
@@ -1732,43 +1745,40 @@
   UsedRtpHeaderExtensionIds used_ids;
   RtpHeaderExtensions all_regular_extensions;
   RtpHeaderExtensions all_encrypted_extensions;
-  offer_audio_extensions->clear();
-  offer_video_extensions->clear();
 
   // First - get all extensions from the current description if the media type
   // is used.
   // Add them to |used_ids| so the local ids are not reused if a new media
   // type is added.
-  if (current_description) {
-    for (const ContentInfo& content : current_description->contents()) {
-      if (IsMediaContentOfType(&content, MEDIA_TYPE_AUDIO)) {
-        const AudioContentDescription* audio =
-            content.media_description()->as_audio();
-        MergeRtpHdrExts(audio->rtp_header_extensions(), offer_audio_extensions,
-                        &all_regular_extensions, &all_encrypted_extensions,
-                        &used_ids);
-      } else if (IsMediaContentOfType(&content, MEDIA_TYPE_VIDEO)) {
-        const VideoContentDescription* video =
-            content.media_description()->as_video();
-        MergeRtpHdrExts(video->rtp_header_extensions(), offer_video_extensions,
-                        &all_regular_extensions, &all_encrypted_extensions,
-                        &used_ids);
-      }
+  for (const ContentInfo* content : current_active_contents) {
+    if (IsMediaContentOfType(content, MEDIA_TYPE_AUDIO)) {
+      const AudioContentDescription* audio =
+          content->media_description()->as_audio();
+      MergeRtpHdrExts(audio->rtp_header_extensions(), offer_audio_extensions,
+                      &all_regular_extensions, &all_encrypted_extensions,
+                      &used_ids);
+    } else if (IsMediaContentOfType(content, MEDIA_TYPE_VIDEO)) {
+      const VideoContentDescription* video =
+          content->media_description()->as_video();
+      MergeRtpHdrExts(video->rtp_header_extensions(), offer_video_extensions,
+                      &all_regular_extensions, &all_encrypted_extensions,
+                      &used_ids);
     }
   }
 
-  // Add our default RTP header extensions that are not in
-  // |current_description|.
-  MergeRtpHdrExts(audio_rtp_header_extensions(session_options.is_unified_plan),
+  // Add our default RTP header extensions that are not in the current
+  // description.
+  MergeRtpHdrExts(audio_rtp_header_extensions(is_unified_plan),
                   offer_audio_extensions, &all_regular_extensions,
                   &all_encrypted_extensions, &used_ids);
-  MergeRtpHdrExts(video_rtp_header_extensions(session_options.is_unified_plan),
+  MergeRtpHdrExts(video_rtp_header_extensions(is_unified_plan),
                   offer_video_extensions, &all_regular_extensions,
                   &all_encrypted_extensions, &used_ids);
 
   // TODO(jbauch): Support adding encrypted header extensions to existing
   // sessions.
-  if (enable_encrypted_rtp_header_extensions_ && !current_description) {
+  if (enable_encrypted_rtp_header_extensions_ &&
+      current_active_contents.empty()) {
     AddEncryptedVersionsOfHdrExts(offer_audio_extensions,
                                   &all_encrypted_extensions, &used_ids);
     AddEncryptedVersionsOfHdrExts(offer_video_extensions,
@@ -1858,8 +1868,10 @@
       GetAudioCodecsForOffer(media_description_options.direction);
 
   AudioCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not being recycled.
-  if (current_content && !current_content->rejected) {
+  // Add the codecs from current content if it exists and is not rejected nor
+  // recycled.
+  if (current_content && !current_content->rejected &&
+      current_content->name == media_description_options.mid) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
     const AudioContentDescription* acd =
         current_content->media_description()->as_audio();
@@ -1934,8 +1946,10 @@
                                         &crypto_suites);
 
   VideoCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not being recycled.
-  if (current_content && !current_content->rejected) {
+  // Add the codecs from current content if it exists and is not rejected nor
+  // recycled.
+  if (current_content && !current_content->rejected &&
+      current_content->name == media_description_options.mid) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
     const VideoContentDescription* vcd =
         current_content->media_description()->as_video();
@@ -2097,8 +2111,10 @@
       GetAudioCodecsForAnswer(offer_rtd, answer_rtd);
 
   AudioCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not being recycled.
-  if (current_content && !current_content->rejected) {
+  // Add the codecs from current content if it exists and is not rejected nor
+  // recycled.
+  if (current_content && !current_content->rejected &&
+      current_content->name == media_description_options.mid) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
     const AudioContentDescription* acd =
         current_content->media_description()->as_audio();
@@ -2183,8 +2199,10 @@
   }
 
   VideoCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not being recycled.
-  if (current_content && !current_content->rejected) {
+  // Add the codecs from current content if it exists and is not rejected nor
+  // recycled.
+  if (current_content && !current_content->rejected &&
+      current_content->name == media_description_options.mid) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
     const VideoContentDescription* vcd =
         current_content->media_description()->as_video();
diff --git a/pc/mediasession.h b/pc/mediasession.h
index 5904605..9495bdc 100644
--- a/pc/mediasession.h
+++ b/pc/mediasession.h
@@ -173,19 +173,22 @@
   const AudioCodecs& GetAudioCodecsForAnswer(
       const webrtc::RtpTransceiverDirection& offer,
       const webrtc::RtpTransceiverDirection& answer) const;
-  void GetCodecsForOffer(const SessionDescription* current_description,
-                         AudioCodecs* audio_codecs,
-                         VideoCodecs* video_codecs,
-                         DataCodecs* data_codecs) const;
-  void GetCodecsForAnswer(const SessionDescription* current_description,
-                          const SessionDescription* remote_offer,
-                          AudioCodecs* audio_codecs,
-                          VideoCodecs* video_codecs,
-                          DataCodecs* data_codecs) const;
-  void GetRtpHdrExtsToOffer(const MediaSessionOptions& session_options,
-                            const SessionDescription* current_description,
-                            RtpHeaderExtensions* audio_extensions,
-                            RtpHeaderExtensions* video_extensions) const;
+  void GetCodecsForOffer(
+      const std::vector<const ContentInfo*>& current_active_contents,
+      AudioCodecs* audio_codecs,
+      VideoCodecs* video_codecs,
+      DataCodecs* data_codecs) const;
+  void GetCodecsForAnswer(
+      const std::vector<const ContentInfo*>& current_active_contents,
+      const SessionDescription& remote_offer,
+      AudioCodecs* audio_codecs,
+      VideoCodecs* video_codecs,
+      DataCodecs* data_codecs) const;
+  void GetRtpHdrExtsToOffer(
+      const std::vector<const ContentInfo*>& current_active_contents,
+      bool is_unified_plan,
+      RtpHeaderExtensions* audio_extensions,
+      RtpHeaderExtensions* video_extensions) const;
   bool AddTransportOffer(const std::string& content_name,
                          const TransportOptions& transport_options,
                          const SessionDescription* current_desc,
diff --git a/pc/mediasession_unittest.cc b/pc/mediasession_unittest.cc
index 076ad12..2098ec0 100644
--- a/pc/mediasession_unittest.cc
+++ b/pc/mediasession_unittest.cc
@@ -2020,6 +2020,110 @@
   EXPECT_THAT(updated_vcd->codecs(), ElementsAreArray(kUpdatedVideoCodecOffer));
 }
 
+// Test that a reoffer does not reuse audio codecs from a previous media section
+// that is being recycled.
+TEST_F(MediaSessionDescriptionFactoryTest,
+       ReOfferDoesNotReUseRecycledAudioCodecs) {
+  f1_.set_video_codecs({});
+  f2_.set_video_codecs({});
+
+  MediaSessionOptions opts;
+  AddMediaSection(MEDIA_TYPE_AUDIO, "a0", RtpTransceiverDirection::kSendRecv,
+                  kActive, &opts);
+  auto offer = absl::WrapUnique(f1_.CreateOffer(opts, nullptr));
+  auto answer = absl::WrapUnique(f2_.CreateAnswer(offer.get(), opts, nullptr));
+
+  // Recycle the media section by changing its mid.
+  opts.media_description_options[0].mid = "a1";
+  auto reoffer = absl::WrapUnique(f2_.CreateOffer(opts, answer.get()));
+
+  // Expect that the results of the first negotiation are ignored. If the m=
+  // section was not recycled the payload types would match the initial offerer.
+  const AudioContentDescription* acd =
+      GetFirstAudioContentDescription(reoffer.get());
+  EXPECT_THAT(acd->codecs(), ElementsAreArray(kAudioCodecs2));
+}
+
+// Test that a reoffer does not reuse video codecs from a previous media section
+// that is being recycled.
+TEST_F(MediaSessionDescriptionFactoryTest,
+       ReOfferDoesNotReUseRecycledVideoCodecs) {
+  f1_.set_audio_codecs({}, {});
+  f2_.set_audio_codecs({}, {});
+
+  MediaSessionOptions opts;
+  AddMediaSection(MEDIA_TYPE_VIDEO, "v0", RtpTransceiverDirection::kSendRecv,
+                  kActive, &opts);
+  auto offer = absl::WrapUnique(f1_.CreateOffer(opts, nullptr));
+  auto answer = absl::WrapUnique(f2_.CreateAnswer(offer.get(), opts, nullptr));
+
+  // Recycle the media section by changing its mid.
+  opts.media_description_options[0].mid = "v1";
+  auto reoffer = absl::WrapUnique(f2_.CreateOffer(opts, answer.get()));
+
+  // Expect that the results of the first negotiation are ignored. If the m=
+  // section was not recycled the payload types would match the initial offerer.
+  const VideoContentDescription* vcd =
+      GetFirstVideoContentDescription(reoffer.get());
+  EXPECT_THAT(vcd->codecs(), ElementsAreArray(kVideoCodecs2));
+}
+
+// Test that a reanswer does not reuse audio codecs from a previous media
+// section that is being recycled.
+TEST_F(MediaSessionDescriptionFactoryTest,
+       ReAnswerDoesNotReUseRecycledAudioCodecs) {
+  f1_.set_video_codecs({});
+  f2_.set_video_codecs({});
+
+  // Perform initial offer/answer in reverse (|f2_| as offerer) so that the
+  // second offer/answer is forward (|f1_| as offerer).
+  MediaSessionOptions opts;
+  AddMediaSection(MEDIA_TYPE_AUDIO, "a0", RtpTransceiverDirection::kSendRecv,
+                  kActive, &opts);
+  auto offer = absl::WrapUnique(f2_.CreateOffer(opts, nullptr));
+  auto answer = absl::WrapUnique(f1_.CreateAnswer(offer.get(), opts, nullptr));
+
+  // Recycle the media section by changing its mid.
+  opts.media_description_options[0].mid = "a1";
+  auto reoffer = absl::WrapUnique(f1_.CreateOffer(opts, answer.get()));
+  auto reanswer =
+      absl::WrapUnique(f2_.CreateAnswer(reoffer.get(), opts, offer.get()));
+
+  // Expect that the results of the first negotiation are ignored. If the m=
+  // section was not recycled the payload types would match the initial offerer.
+  const AudioContentDescription* acd =
+      GetFirstAudioContentDescription(reanswer.get());
+  EXPECT_THAT(acd->codecs(), ElementsAreArray(kAudioCodecsAnswer));
+}
+
+// Test that a reanswer does not reuse video codecs from a previous media
+// section that is being recycled.
+TEST_F(MediaSessionDescriptionFactoryTest,
+       ReAnswerDoesNotReUseRecycledVideoCodecs) {
+  f1_.set_audio_codecs({}, {});
+  f2_.set_audio_codecs({}, {});
+
+  // Perform initial offer/answer in reverse (|f2_| as offerer) so that the
+  // second offer/answer is forward (|f1_| as offerer).
+  MediaSessionOptions opts;
+  AddMediaSection(MEDIA_TYPE_VIDEO, "v0", RtpTransceiverDirection::kSendRecv,
+                  kActive, &opts);
+  auto offer = absl::WrapUnique(f2_.CreateOffer(opts, nullptr));
+  auto answer = absl::WrapUnique(f1_.CreateAnswer(offer.get(), opts, nullptr));
+
+  // Recycle the media section by changing its mid.
+  opts.media_description_options[0].mid = "v1";
+  auto reoffer = absl::WrapUnique(f1_.CreateOffer(opts, answer.get()));
+  auto reanswer =
+      absl::WrapUnique(f2_.CreateAnswer(reoffer.get(), opts, offer.get()));
+
+  // Expect that the results of the first negotiation are ignored. If the m=
+  // section was not recycled the payload types would match the initial offerer.
+  const VideoContentDescription* vcd =
+      GetFirstVideoContentDescription(reanswer.get());
+  EXPECT_THAT(vcd->codecs(), ElementsAreArray(kVideoCodecsAnswer));
+}
+
 // Create an updated offer after creating an answer to the original offer and
 // verify that the codecs that were part of the original answer are not changed
 // in the updated offer. In this test Rtx is enabled.
diff --git a/pc/peerconnection.cc b/pc/peerconnection.cc
index e166d1e..c341888 100644
--- a/pc/peerconnection.cc
+++ b/pc/peerconnection.cc
@@ -3947,6 +3947,17 @@
   return media_description_options;
 }
 
+// Returns the ContentInfo at mline index |i|, or null if none exists.
+static const ContentInfo* GetContentByIndex(
+    const SessionDescriptionInterface* sdesc,
+    size_t i) {
+  if (!sdesc) {
+    return nullptr;
+  }
+  const ContentInfos& contents = sdesc->description()->contents();
+  return (i < contents.size() ? &contents[i] : nullptr);
+}
+
 void PeerConnection::GetOptionsForUnifiedPlanOffer(
     const RTCOfferAnswerOptions& offer_answer_options,
     cricket::MediaSessionOptions* session_options) {
@@ -3974,10 +3985,15 @@
     // Either |local_content| or |remote_content| is non-null.
     const ContentInfo* local_content =
         (i < local_contents.size() ? &local_contents[i] : nullptr);
+    const ContentInfo* current_local_content =
+        GetContentByIndex(current_local_description(), i);
     const ContentInfo* remote_content =
         (i < remote_contents.size() ? &remote_contents[i] : nullptr);
-    bool had_been_rejected = (local_content && local_content->rejected) ||
-                             (remote_content && remote_content->rejected);
+    const ContentInfo* current_remote_content =
+        GetContentByIndex(current_remote_description(), i);
+    bool had_been_rejected =
+        (current_local_content && current_local_content->rejected) ||
+        (current_remote_content && current_remote_content->rejected);
     const std::string& mid =
         (local_content ? local_content->name : remote_content->name);
     cricket::MediaType media_type =
@@ -3988,7 +4004,7 @@
       auto transceiver = GetAssociatedTransceiver(mid);
       RTC_CHECK(transceiver);
       // A media section is considered eligible for recycling if it is marked as
-      // rejected in either the local or remote description.
+      // rejected in either the current local or current remote description.
       if (had_been_rejected && transceiver->stopped()) {
         session_options->media_description_options.push_back(
             cricket::MediaDescriptionOptions(transceiver->media_type(), mid,
@@ -4180,12 +4196,13 @@
         session_options->media_description_options.push_back(
             cricket::MediaDescriptionOptions(
                 cricket::MEDIA_TYPE_AUDIO, content.name,
-                RtpTransceiverDirection::kInactive, true));
+                RtpTransceiverDirection::kInactive, /*stopped=*/true));
       } else {
+        bool stopped = (audio_direction == RtpTransceiverDirection::kInactive);
         session_options->media_description_options.push_back(
-            cricket::MediaDescriptionOptions(
-                cricket::MEDIA_TYPE_AUDIO, content.name, audio_direction,
-                audio_direction == RtpTransceiverDirection::kInactive));
+            cricket::MediaDescriptionOptions(cricket::MEDIA_TYPE_AUDIO,
+                                             content.name, audio_direction,
+                                             stopped));
         *audio_index = session_options->media_description_options.size() - 1;
       }
     } else if (IsVideoContent(&content)) {
@@ -4194,12 +4211,13 @@
         session_options->media_description_options.push_back(
             cricket::MediaDescriptionOptions(
                 cricket::MEDIA_TYPE_VIDEO, content.name,
-                RtpTransceiverDirection::kInactive, true));
+                RtpTransceiverDirection::kInactive, /*stopped=*/true));
       } else {
+        bool stopped = (video_direction == RtpTransceiverDirection::kInactive);
         session_options->media_description_options.push_back(
-            cricket::MediaDescriptionOptions(
-                cricket::MEDIA_TYPE_VIDEO, content.name, video_direction,
-                video_direction == RtpTransceiverDirection::kInactive));
+            cricket::MediaDescriptionOptions(cricket::MEDIA_TYPE_VIDEO,
+                                             content.name, video_direction,
+                                             stopped));
         *video_index = session_options->media_description_options.size() - 1;
       }
     } else {
diff --git a/pc/peerconnection_jsep_unittest.cc b/pc/peerconnection_jsep_unittest.cc
index fe878b0..f0c9111 100644
--- a/pc/peerconnection_jsep_unittest.cc
+++ b/pc/peerconnection_jsep_unittest.cc
@@ -700,6 +700,12 @@
 // Test that the offer/answer and transceivers for both the caller and callee
 // side are generated/updated correctly when recycling an audio/video media
 // section as a media section of either the same or opposite type.
+// Correct recycling works as follows:
+// - The m= section is re-offered with a new MID value and the new media type.
+// - The previously-associated transceiver is dissociated when the new offer is
+//   set as a local description on the offerer or as a remote description on
+//   the answerer.
+// - The new transceiver is associated with the new MID value.
 class RecycleMediaSectionTest
     : public PeerConnectionJsepTest,
       public testing::WithParamInterface<
@@ -714,7 +720,9 @@
   cricket::MediaType second_type_;
 };
 
-TEST_P(RecycleMediaSectionTest, VerifyOfferAnswerAndTransceivers) {
+// Test that recycling works properly when a new transceiver recycles an m=
+// section that was rejected in both the current local and remote descriptions.
+TEST_P(RecycleMediaSectionTest, CurrentLocalAndCurrentRemoteRejected) {
   auto caller = CreatePeerConnection();
   auto first_transceiver = caller->AddTransceiver(first_type_);
   auto callee = CreatePeerConnection();
@@ -774,6 +782,285 @@
   ASSERT_EQ(2u, callee->pc()->GetTransceivers().size());
 }
 
+// Test that recycling works properly when a new transceiver recycles an m=
+// section that was rejected in only the current remote description.
+TEST_P(RecycleMediaSectionTest, CurrentRemoteOnlyRejected) {
+  auto caller = CreatePeerConnection();
+  auto caller_first_transceiver = caller->AddTransceiver(first_type_);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  std::string first_mid = *caller_first_transceiver->mid();
+  ASSERT_EQ(1u, callee->pc()->GetTransceivers().size());
+  auto callee_first_transceiver = callee->pc()->GetTransceivers()[0];
+  callee_first_transceiver->Stop();
+
+  // The answer will have a rejected m= section.
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  // The offer should reuse the previous media section but allocate a new MID
+  // and change the media type.
+  auto caller_second_transceiver = caller->AddTransceiver(second_type_);
+  auto offer = caller->CreateOffer();
+  const auto& offer_contents = offer->description()->contents();
+  ASSERT_EQ(1u, offer_contents.size());
+  EXPECT_FALSE(offer_contents[0].rejected);
+  EXPECT_EQ(second_type_, offer_contents[0].media_description()->type());
+  std::string second_mid = offer_contents[0].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  // Setting the local offer will dissociate the previous transceiver and set
+  // the MID for the new transceiver.
+  ASSERT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+  EXPECT_EQ(absl::nullopt, caller_first_transceiver->mid());
+  EXPECT_EQ(second_mid, caller_second_transceiver->mid());
+
+  // Setting the remote offer will dissociate the previous transceiver and
+  // create a new transceiver for the media section.
+  ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+  auto callee_transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, callee_transceivers.size());
+  EXPECT_EQ(absl::nullopt, callee_transceivers[0]->mid());
+  EXPECT_EQ(first_type_, callee_transceivers[0]->media_type());
+  EXPECT_EQ(second_mid, callee_transceivers[1]->mid());
+  EXPECT_EQ(second_type_, callee_transceivers[1]->media_type());
+
+  // The answer should have only one media section for the new transceiver.
+  auto answer = callee->CreateAnswer();
+  auto answer_contents = answer->description()->contents();
+  ASSERT_EQ(1u, answer_contents.size());
+  EXPECT_FALSE(answer_contents[0].rejected);
+  EXPECT_EQ(second_mid, answer_contents[0].name);
+  EXPECT_EQ(second_type_, answer_contents[0].media_description()->type());
+
+  // Setting the local answer should succeed.
+  ASSERT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+
+  // Setting the remote answer should succeed and not create any new
+  // transceivers.
+  ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+  ASSERT_EQ(2u, caller->pc()->GetTransceivers().size());
+  ASSERT_EQ(2u, callee->pc()->GetTransceivers().size());
+}
+
+// Test that recycling works properly when a new transceiver recycles an m=
+// section that was rejected only in the current local description.
+TEST_P(RecycleMediaSectionTest, CurrentLocalOnlyRejected) {
+  auto caller = CreatePeerConnection();
+  auto caller_first_transceiver = caller->AddTransceiver(first_type_);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  std::string first_mid = *caller_first_transceiver->mid();
+  ASSERT_EQ(1u, callee->pc()->GetTransceivers().size());
+  auto callee_first_transceiver = callee->pc()->GetTransceivers()[0];
+  callee_first_transceiver->Stop();
+
+  // The answer will have a rejected m= section.
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  // The offer should reuse the previous media section but allocate a new MID
+  // and change the media type.
+  auto callee_second_transceiver = callee->AddTransceiver(second_type_);
+  auto offer = callee->CreateOffer();
+  const auto& offer_contents = offer->description()->contents();
+  ASSERT_EQ(1u, offer_contents.size());
+  EXPECT_FALSE(offer_contents[0].rejected);
+  EXPECT_EQ(second_type_, offer_contents[0].media_description()->type());
+  std::string second_mid = offer_contents[0].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  // Setting the local offer will dissociate the previous transceiver and set
+  // the MID for the new transceiver.
+  ASSERT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(offer.get())));
+  EXPECT_EQ(absl::nullopt, callee_first_transceiver->mid());
+  EXPECT_EQ(second_mid, callee_second_transceiver->mid());
+
+  // Setting the remote offer will dissociate the previous transceiver and
+  // create a new transceiver for the media section.
+  ASSERT_TRUE(caller->SetRemoteDescription(std::move(offer)));
+  auto caller_transceivers = caller->pc()->GetTransceivers();
+  ASSERT_EQ(2u, caller_transceivers.size());
+  EXPECT_EQ(absl::nullopt, caller_transceivers[0]->mid());
+  EXPECT_EQ(first_type_, caller_transceivers[0]->media_type());
+  EXPECT_EQ(second_mid, caller_transceivers[1]->mid());
+  EXPECT_EQ(second_type_, caller_transceivers[1]->media_type());
+
+  // The answer should have only one media section for the new transceiver.
+  auto answer = caller->CreateAnswer();
+  auto answer_contents = answer->description()->contents();
+  ASSERT_EQ(1u, answer_contents.size());
+  EXPECT_FALSE(answer_contents[0].rejected);
+  EXPECT_EQ(second_mid, answer_contents[0].name);
+  EXPECT_EQ(second_type_, answer_contents[0].media_description()->type());
+
+  // Setting the local answer should succeed.
+  ASSERT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(answer.get())));
+
+  // Setting the remote answer should succeed and not create any new
+  // transceivers.
+  ASSERT_TRUE(callee->SetRemoteDescription(std::move(answer)));
+  ASSERT_EQ(2u, callee->pc()->GetTransceivers().size());
+  ASSERT_EQ(2u, caller->pc()->GetTransceivers().size());
+}
+
+// Test that a m= section is *not* recycled if the media section is only
+// rejected in the pending local description and there is no current remote
+// description.
+TEST_P(RecycleMediaSectionTest, PendingLocalRejectedAndNoRemote) {
+  auto caller = CreatePeerConnection();
+  auto caller_first_transceiver = caller->AddTransceiver(first_type_);
+
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+  std::string first_mid = *caller_first_transceiver->mid();
+  caller_first_transceiver->Stop();
+
+  // The reoffer will have a rejected m= section.
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+  auto caller_second_transceiver = caller->AddTransceiver(second_type_);
+
+  // The reoffer should not recycle the existing m= section since it is not
+  // rejected in either the *current* local or *current* remote description.
+  auto reoffer = caller->CreateOffer();
+  auto reoffer_contents = reoffer->description()->contents();
+  ASSERT_EQ(2u, reoffer_contents.size());
+  EXPECT_TRUE(reoffer_contents[0].rejected);
+  EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type());
+  EXPECT_EQ(first_mid, reoffer_contents[0].name);
+  EXPECT_FALSE(reoffer_contents[1].rejected);
+  EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type());
+  std::string second_mid = reoffer_contents[1].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  ASSERT_TRUE(caller->SetLocalDescription(std::move(reoffer)));
+
+  // Both RtpTransceivers are associated.
+  EXPECT_EQ(first_mid, caller_first_transceiver->mid());
+  EXPECT_EQ(second_mid, caller_second_transceiver->mid());
+}
+
+// Test that a m= section is *not* recycled if the media section is only
+// rejected in the pending local description and not rejected in the current
+// remote description.
+TEST_P(RecycleMediaSectionTest, PendingLocalRejectedAndNotRejectedRemote) {
+  auto caller = CreatePeerConnection();
+  auto caller_first_transceiver = caller->AddTransceiver(first_type_);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+  std::string first_mid = *caller_first_transceiver->mid();
+  caller_first_transceiver->Stop();
+
+  // The reoffer will have a rejected m= section.
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+  auto caller_second_transceiver = caller->AddTransceiver(second_type_);
+
+  // The reoffer should not recycle the existing m= section since it is not
+  // rejected in either the *current* local or *current* remote description.
+  auto reoffer = caller->CreateOffer();
+  auto reoffer_contents = reoffer->description()->contents();
+  ASSERT_EQ(2u, reoffer_contents.size());
+  EXPECT_TRUE(reoffer_contents[0].rejected);
+  EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type());
+  EXPECT_EQ(first_mid, reoffer_contents[0].name);
+  EXPECT_FALSE(reoffer_contents[1].rejected);
+  EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type());
+  std::string second_mid = reoffer_contents[1].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  ASSERT_TRUE(caller->SetLocalDescription(std::move(reoffer)));
+
+  // Both RtpTransceivers are associated.
+  EXPECT_EQ(first_mid, caller_first_transceiver->mid());
+  EXPECT_EQ(second_mid, caller_second_transceiver->mid());
+}
+
+// Test that an m= section is *not* recycled if the media section is only
+// rejected in the pending remote description and there is no current local
+// description.
+TEST_P(RecycleMediaSectionTest, PendingRemoteRejectedAndNoLocal) {
+  auto caller = CreatePeerConnection();
+  auto caller_first_transceiver = caller->AddTransceiver(first_type_);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  ASSERT_EQ(1u, callee->pc()->GetTransceivers().size());
+  auto callee_first_transceiver = callee->pc()->GetTransceivers()[0];
+  std::string first_mid = *callee_first_transceiver->mid();
+  caller_first_transceiver->Stop();
+
+  // The reoffer will have a rejected m= section.
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto callee_second_transceiver = callee->AddTransceiver(second_type_);
+
+  // The reoffer should not recycle the existing m= section since it is not
+  // rejected in either the *current* local or *current* remote description.
+  auto reoffer = callee->CreateOffer();
+  auto reoffer_contents = reoffer->description()->contents();
+  ASSERT_EQ(2u, reoffer_contents.size());
+  EXPECT_TRUE(reoffer_contents[0].rejected);
+  EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type());
+  EXPECT_EQ(first_mid, reoffer_contents[0].name);
+  EXPECT_FALSE(reoffer_contents[1].rejected);
+  EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type());
+  std::string second_mid = reoffer_contents[1].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  // Note: Cannot actually set the reoffer since the callee is in the signaling
+  // state 'have-remote-offer'.
+}
+
+// Test that an m= section is *not* recycled if the media section is only
+// rejected in the pending remote description and not rejected in the current
+// local description.
+TEST_P(RecycleMediaSectionTest, PendingRemoteRejectedAndNotRejectedLocal) {
+  auto caller = CreatePeerConnection();
+  auto caller_first_transceiver = caller->AddTransceiver(first_type_);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+  ASSERT_EQ(1u, callee->pc()->GetTransceivers().size());
+  auto callee_first_transceiver = callee->pc()->GetTransceivers()[0];
+  std::string first_mid = *callee_first_transceiver->mid();
+  caller_first_transceiver->Stop();
+
+  // The reoffer will have a rejected m= section.
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto callee_second_transceiver = callee->AddTransceiver(second_type_);
+
+  // The reoffer should not recycle the existing m= section since it is not
+  // rejected in either the *current* local or *current* remote description.
+  auto reoffer = callee->CreateOffer();
+  auto reoffer_contents = reoffer->description()->contents();
+  ASSERT_EQ(2u, reoffer_contents.size());
+  EXPECT_TRUE(reoffer_contents[0].rejected);
+  EXPECT_EQ(first_type_, reoffer_contents[0].media_description()->type());
+  EXPECT_EQ(first_mid, reoffer_contents[0].name);
+  EXPECT_FALSE(reoffer_contents[1].rejected);
+  EXPECT_EQ(second_type_, reoffer_contents[1].media_description()->type());
+  std::string second_mid = reoffer_contents[1].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  // Note: Cannot actually set the reoffer since the callee is in the signaling
+  // state 'have-remote-offer'.
+}
+
 // Test all combinations of audio and video as the first and second media type
 // for the media section. This is needed for full test coverage because
 // MediaSession has separate functions for processing audio and video media