Keep transceiver MID in sync with its channel

When applying a local description, a transceiver could be matched to an
m= section by its index even if it was already associated with a
different MID. The channel of the transceiver keeps using the MID it was
created for, so the two could get out of sync.

- Only match transceivers that are not yet associated by m= section
  index.
- Route transport changes to a transceiver based on the MID of its
  channel.

Bug: chromium:567160162
Change-Id: Ic9f29b66a8f9f181fd414009f5c1e5cebfa9ca54
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/506421
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48768}
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index 035f6b7..15a0e4e 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -3130,7 +3130,10 @@
     for (const auto& transceiver :
          rtp_manager()->transceivers()->UnsafeList()) {
       auto internal = transceiver->internal();
-      if (internal->mid() == mid) {
+      // Match on the MID that the channel was created for rather than on the
+      // transceiver's current MID. The channel is bound to the transport of
+      // that MID and must be notified when that transport changes.
+      if (internal->channel_mid() == mid) {
         ret = internal->SetRtpTransport(rtp_transport);
       }
     }
diff --git a/pc/rtp_transceiver.cc b/pc/rtp_transceiver.cc
index b951403..6d5c39e 100644
--- a/pc/rtp_transceiver.cc
+++ b/pc/rtp_transceiver.cc
@@ -1579,6 +1579,11 @@
   return channel_->transport_name();
 }
 
+absl::string_view RtpTransceiver::channel_mid() const {
+  RTC_DCHECK_RUN_ON(context()->network_thread());
+  return channel_ ? absl::string_view(channel_->mid()) : absl::string_view();
+}
+
 MediaSendChannelInterface* RtpTransceiver::media_send_channel() {
   RTC_DCHECK_RUN_ON(thread_);
   return channel_ ? channel_->media_send_channel() : nullptr;
diff --git a/pc/rtp_transceiver.h b/pc/rtp_transceiver.h
index 3dca197..a215bc9 100644
--- a/pc/rtp_transceiver.h
+++ b/pc/rtp_transceiver.h
@@ -428,6 +428,10 @@
   const std::vector<StreamParams>& channel_local_streams() const;
   const std::vector<StreamParams>& channel_remote_streams() const;
   absl::string_view channel_transport_name() const;
+  // Returns the MID that the channel was created for, or an empty string if
+  // there is no channel. The channel stays bound to the transport of this MID
+  // for its lifetime. Must be called on the network thread.
+  absl::string_view channel_mid() const;
 
   // Accessors for media channels. These return null if there is no channel.
   MediaSendChannelInterface* media_send_channel();
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index 5cf98d1..d624d81 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -4787,7 +4787,10 @@
     // mapping between transceivers and m= section indices established when
     // creating the offer.
     if (!transceiver) {
-      transceiver = transceivers()->FindByMLineIndex(mline_index);
+      // Only consider transceivers that are not yet associated. The channel of
+      // an associated transceiver stays bound to the MID it was created for
+      // (and to the transport of that MID), so the MID must not change.
+      transceiver = transceivers()->FindUnassociatedByMLineIndex(mline_index);
     }
     if (!transceiver) {
       // This may happen normally when media sections are rejected.
diff --git a/pc/sdp_offer_answer_unittest.cc b/pc/sdp_offer_answer_unittest.cc
index 82bda92..cb5ad60 100644
--- a/pc/sdp_offer_answer_unittest.cc
+++ b/pc/sdp_offer_answer_unittest.cc
@@ -2388,6 +2388,62 @@
   EXPECT_FALSE(has_channel);
 }
 
+// Verifies that a munged local re-offer can not change the MID of a
+// transceiver that is already associated with an m= section.
+TEST_F(SdpOfferAnswerTest, RejectsMidChangeOfAssociatedTransceiver) {
+  auto caller = CreatePeerConnection();
+  auto callee = CreatePeerConnection();
+
+  // Negotiate without BUNDLE so that each m= section gets its own transport.
+  auto negotiate = [&] {
+    auto offer = caller->CreateOffer();
+    ASSERT_THAT(offer, NotNull());
+    offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+    ASSERT_TRUE(caller->SetLocalDescription(offer->Clone()));
+    ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+    auto answer = callee->CreateAnswerAndSetAsLocal();
+    ASSERT_THAT(answer, NotNull());
+    ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+  };
+
+  auto set_mid = [](SessionDescriptionInterface* sdesc, size_t index,
+                    absl::string_view mid) {
+    SessionDescription* desc = sdesc->description();
+    desc->contents()[index].set_mid(mid);
+    desc->transport_infos()[index].content_name = std::string(mid);
+  };
+
+  caller->AddTransceiver(MediaType::AUDIO);
+  scoped_refptr<RtpTransceiverInterface> stopped_transceiver =
+      caller->AddTransceiver(MediaType::AUDIO);
+  ASSERT_NO_FATAL_FAILURE(negotiate());
+
+  // Reject the second m= section in both the local and remote descriptions.
+  ASSERT_TRUE(stopped_transceiver->StopStandard().ok());
+  ASSERT_NO_FATAL_FAILURE(negotiate());
+  ASSERT_THAT(caller->pc()->GetTransceivers(), SizeIs(1));
+
+  // A new transceiver recycles the rejected m= section and gets associated
+  // with it (and gets a channel) by a local offer.
+  scoped_refptr<RtpTransceiverInterface> recycled_transceiver =
+      caller->AddTransceiver(MediaType::AUDIO);
+  auto offer = caller->CreateOffer();
+  ASSERT_THAT(offer, NotNull());
+  ASSERT_THAT(offer->description()->contents(), SizeIs(2));
+  offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+  ASSERT_TRUE(caller->SetLocalDescription(std::move(offer)));
+  const std::optional<std::string> recycled_mid = recycled_transceiver->mid();
+  ASSERT_TRUE(recycled_mid.has_value());
+
+  // Munged re-offer that changes the MID of the recycled m= section.
+  auto renamed_offer = caller->CreateOffer();
+  ASSERT_THAT(renamed_offer, NotNull());
+  renamed_offer->description()->RemoveGroupByName(GROUP_TYPE_BUNDLE);
+  set_mid(renamed_offer.get(), 1, "renamed");
+  EXPECT_FALSE(caller->SetLocalDescription(std::move(renamed_offer)));
+  EXPECT_EQ(recycled_transceiver->mid(), recycled_mid);
+}
+
 TEST_F(SdpOfferAnswerTest, SubsequentOfferDoesNotAddSctpInit) {
   auto pc1 = CreatePeerConnection("WebRTC-Sctp-Snap/Enabled/");
   auto pc2 = CreatePeerConnection("WebRTC-Sctp-Snap/Enabled/");
diff --git a/pc/transceiver_list.cc b/pc/transceiver_list.cc
index f2abe48..49654de 100644
--- a/pc/transceiver_list.cc
+++ b/pc/transceiver_list.cc
@@ -85,11 +85,12 @@
   return nullptr;
 }
 
-RtpTransceiverProxyRefPtr TransceiverList::FindByMLineIndex(
+RtpTransceiverProxyRefPtr TransceiverList::FindUnassociatedByMLineIndex(
     size_t mline_index) const {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
   for (const auto& transceiver : transceivers_) {
-    if (transceiver->internal()->mline_index() == mline_index) {
+    RtpTransceiver* internal = transceiver->internal();
+    if (!internal->mid() && internal->mline_index() == mline_index) {
       return transceiver;
     }
   }
diff --git a/pc/transceiver_list.h b/pc/transceiver_list.h
index 5136449..a30ed99 100644
--- a/pc/transceiver_list.h
+++ b/pc/transceiver_list.h
@@ -127,7 +127,10 @@
   RtpTransceiverProxyRefPtr FindBySender(
       scoped_refptr<RtpSenderInterface> sender) const;
   RtpTransceiverProxyRefPtr FindByMid(absl::string_view mid) const;
-  RtpTransceiverProxyRefPtr FindByMLineIndex(size_t mline_index) const;
+  // Returns the transceiver that is not yet associated with an m= section
+  // (i.e. has no MID) and has been assigned `mline_index` by CreateOffer.
+  RtpTransceiverProxyRefPtr FindUnassociatedByMLineIndex(
+      size_t mline_index) const;
 
   // Find or create the stable state for a transceiver.
   TransceiverStableState* StableState(RtpTransceiverProxyRefPtr transceiver) {