Reland "Fix bug where we assume new m= sections will always be bundled."

This is a reland of d2b885fd91909f1b17fb11292a8c989d5d883b22, after
making sure transports that are just being kept alive in case of
rollback don't contribute to connection state, which broke a WPT.

Original change's description:
> Fix bug where we assume new m= sections will always be bundled.
>
> A recent change [1] assumes that all new m= sections will share the
> first BUNDLE group (if one already exists), which avoids generating
> ICE candidates that are ultimately unnecessary. This is fine for JSEP
> endpoints, but it breaks the following scenarios for non-JSEP endpoints:
>
> * Remote offer adding a new m= section that's not part of any BUNDLE
>   group.
> * Remote offer adding an m= section to the second BUNDLE group.
>
> The latter is specifically problematic for any application that wants
> to bundle all audio streams in one group and all video streams in
> another group when using Unified Plan SDP, to replicate the behavior of
> using Plan B without bundling. It may try to add a video stream only
> for WebRTC to bundle it with audio.
>
> This is fixed by doing some minor re-factoring, having BundleManager
> update the bundle groups at offer time.
>
> Also:
> * Added some additional validation for multiple bundle groups in a
>   subsequent offer, since that now becomes relevant.
> * Improved rollback support, because now rolling back an offer may need
>   to not only remove mid->transport mappings but alter them.
>
> [1]: https://webrtc-review.googlesource.com/c/src/+/221601
>
> Bug: webrtc:12906, webrtc:12999
> Change-Id: I4c6e7020c0be33a782d3608dee88e4e2fceb1be1
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/225642
> Reviewed-by: Harald Alvestrand <hta@webrtc.org>
> Reviewed-by: Henrik Boström <hbos@webrtc.org>
> Commit-Queue: Taylor Brandstetter <deadbeef@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#34544}

Bug: webrtc:12906, webrtc:12999
Change-Id: I68bf988b1918dd2d51de76e53e4fd696fea5a09b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/227120
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Taylor Brandstetter <deadbeef@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#34596}
diff --git a/pc/jsep_transport_collection.cc b/pc/jsep_transport_collection.cc
index ce068d9..c7364b0 100644
--- a/pc/jsep_transport_collection.cc
+++ b/pc/jsep_transport_collection.cc
@@ -12,6 +12,7 @@
 
 #include <algorithm>
 #include <map>
+#include <set>
 #include <type_traits>
 #include <utility>
 
@@ -20,21 +21,53 @@
 
 namespace webrtc {
 
-void BundleManager::Update(const cricket::SessionDescription* description) {
+void BundleManager::Update(const cricket::SessionDescription* description,
+                           SdpType type) {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
-  bundle_groups_.clear();
-  for (const cricket::ContentGroup* new_bundle_group :
-       description->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE)) {
-    bundle_groups_.push_back(
-        std::make_unique<cricket::ContentGroup>(*new_bundle_group));
-    RTC_DLOG(LS_VERBOSE) << "Establishing bundle group "
-                         << new_bundle_group->ToString();
-  }
-  established_bundle_groups_by_mid_.clear();
-  for (const auto& bundle_group : bundle_groups_) {
-    for (const std::string& content_name : bundle_group->content_names()) {
-      established_bundle_groups_by_mid_[content_name] = bundle_group.get();
+  // Rollbacks should call Rollback, not Update.
+  RTC_DCHECK(type != SdpType::kRollback);
+  bool bundle_groups_changed = false;
+  // TODO(bugs.webrtc.org/3349): Do this for kPrAnswer as well. To make this
+  // work, we also need to make sure PRANSWERs don't call
+  // MaybeDestroyJsepTransport, because the final answer may need the destroyed
+  // transport if it changes the BUNDLE group.
+  if (bundle_policy_ == PeerConnectionInterface::kBundlePolicyMaxBundle ||
+      type == SdpType::kAnswer) {
+    // If our policy is "max-bundle" or this is an answer, update all bundle
+    // groups.
+    bundle_groups_changed = true;
+    bundle_groups_.clear();
+    for (const cricket::ContentGroup* new_bundle_group :
+         description->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE)) {
+      bundle_groups_.push_back(
+          std::make_unique<cricket::ContentGroup>(*new_bundle_group));
+      RTC_DLOG(LS_VERBOSE) << "Establishing bundle group "
+                           << new_bundle_group->ToString();
     }
+  } else if (type == SdpType::kOffer) {
+    // If this is an offer, update existing bundle groups.
+    // We do this because as per RFC 8843, section 7.3.2, the answerer cannot
+    // remove an m= section from an existing BUNDLE group without rejecting it.
+    // Thus any m= sections added to a BUNDLE group in this offer can
+    // preemptively start using the bundled transport, as there is no possible
+    // non-bundled fallback.
+    for (const cricket::ContentGroup* new_bundle_group :
+         description->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE)) {
+      // Attempt to find a matching existing group.
+      for (const std::string& mid : new_bundle_group->content_names()) {
+        auto it = established_bundle_groups_by_mid_.find(mid);
+        if (it != established_bundle_groups_by_mid_.end()) {
+          *it->second = *new_bundle_group;
+          bundle_groups_changed = true;
+          RTC_DLOG(LS_VERBOSE)
+              << "Establishing bundle group " << new_bundle_group->ToString();
+          break;
+        }
+      }
+    }
+  }
+  if (bundle_groups_changed) {
+    RefreshEstablishedBundleGroupsByMid();
   }
 }
 
@@ -92,6 +125,34 @@
   bundle_groups_.erase(bundle_group_it);
 }
 
+void BundleManager::Rollback() {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  bundle_groups_.clear();
+  for (const auto& bundle_group : stable_bundle_groups_) {
+    bundle_groups_.push_back(
+        std::make_unique<cricket::ContentGroup>(*bundle_group));
+  }
+  RefreshEstablishedBundleGroupsByMid();
+}
+
+void BundleManager::Commit() {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  stable_bundle_groups_.clear();
+  for (const auto& bundle_group : bundle_groups_) {
+    stable_bundle_groups_.push_back(
+        std::make_unique<cricket::ContentGroup>(*bundle_group));
+  }
+}
+
+void BundleManager::RefreshEstablishedBundleGroupsByMid() {
+  established_bundle_groups_by_mid_.clear();
+  for (const auto& bundle_group : bundle_groups_) {
+    for (const std::string& content_name : bundle_group->content_names()) {
+      established_bundle_groups_by_mid_[content_name] = bundle_group.get();
+    }
+  }
+}
+
 void JsepTransportCollection::RegisterTransport(
     const std::string& mid,
     std::unique_ptr<cricket::JsepTransport> transport) {
@@ -110,6 +171,17 @@
   return result;
 }
 
+std::vector<cricket::JsepTransport*>
+JsepTransportCollection::ActiveTransports() {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  std::set<cricket::JsepTransport*> transports;
+  for (const auto& kv : mid_to_transport_) {
+    transports.insert(kv.second);
+  }
+  return std::vector<cricket::JsepTransport*>(transports.begin(),
+                                              transports.end());
+}
+
 void JsepTransportCollection::DestroyAllTransports() {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
   for (const auto& jsep_transport : jsep_transports_by_name_) {
@@ -157,8 +229,6 @@
   if (it != mid_to_transport_.end() && it->second == jsep_transport)
     return true;
 
-  pending_mids_.push_back(mid);
-
   // The map_change_callback must be called before destroying the
   // transport, because it removes references to the transport
   // in the RTP demuxer.
@@ -191,17 +261,36 @@
   RTC_DCHECK(IsConsistent());
 }
 
-void JsepTransportCollection::RollbackTransports() {
+bool JsepTransportCollection::RollbackTransports() {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
-  for (auto&& mid : pending_mids_) {
-    RemoveTransportForMid(mid);
+  bool ret = true;
+  // First, remove any new mid->transport mappings.
+  for (const auto& kv : mid_to_transport_) {
+    if (stable_mid_to_transport_.count(kv.first) == 0) {
+      ret = ret && map_change_callback_(kv.first, nullptr);
+    }
   }
-  pending_mids_.clear();
+  // Next, restore old mappings.
+  for (const auto& kv : stable_mid_to_transport_) {
+    auto it = mid_to_transport_.find(kv.first);
+    if (it == mid_to_transport_.end() || it->second != kv.second) {
+      ret = ret && map_change_callback_(kv.first, kv.second);
+    }
+  }
+  mid_to_transport_ = stable_mid_to_transport_;
+  // Moving a transport back to mid_to_transport_ means it's now included in
+  // the aggregate state if it wasn't previously.
+  state_change_callback_();
+  DestroyUnusedTransports();
+  RTC_DCHECK(IsConsistent());
+  return ret;
 }
 
 void JsepTransportCollection::CommitTransports() {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
-  pending_mids_.clear();
+  stable_mid_to_transport_ = mid_to_transport_;
+  DestroyUnusedTransports();
+  RTC_DCHECK(IsConsistent());
 }
 
 bool JsepTransportCollection::TransportInUse(
@@ -215,14 +304,32 @@
   return false;
 }
 
+bool JsepTransportCollection::TransportNeededForRollback(
+    cricket::JsepTransport* jsep_transport) const {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  for (const auto& kv : stable_mid_to_transport_) {
+    if (kv.second == jsep_transport) {
+      return true;
+    }
+  }
+  return false;
+}
+
 void JsepTransportCollection::MaybeDestroyJsepTransport(
     cricket::JsepTransport* transport) {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
   // Don't destroy the JsepTransport if there are still media sections referring
-  // to it.
+  // to it, or if it will be needed in case of rollback.
   if (TransportInUse(transport)) {
     return;
   }
+  // If this transport is needed for rollback, don't destroy it yet, but make
+  // sure the aggregate state is updated since this transport is no longer
+  // included in it.
+  if (TransportNeededForRollback(transport)) {
+    state_change_callback_();
+    return;
+  }
   for (const auto& it : jsep_transports_by_name_) {
     if (it.second.get() == transport) {
       jsep_transports_by_name_.erase(it.first);
@@ -233,21 +340,33 @@
   RTC_DCHECK(IsConsistent());
 }
 
+void JsepTransportCollection::DestroyUnusedTransports() {
+  RTC_DCHECK_RUN_ON(&sequence_checker_);
+  bool need_state_change_callback = false;
+  auto it = jsep_transports_by_name_.begin();
+  while (it != jsep_transports_by_name_.end()) {
+    if (TransportInUse(it->second.get()) ||
+        TransportNeededForRollback(it->second.get())) {
+      ++it;
+    } else {
+      it = jsep_transports_by_name_.erase(it);
+      need_state_change_callback = true;
+    }
+  }
+  if (need_state_change_callback) {
+    state_change_callback_();
+  }
+}
+
 bool JsepTransportCollection::IsConsistent() {
   RTC_DCHECK_RUN_ON(&sequence_checker_);
   for (const auto& it : jsep_transports_by_name_) {
-    if (!TransportInUse(it.second.get())) {
+    if (!TransportInUse(it.second.get()) &&
+        !TransportNeededForRollback(it.second.get())) {
       RTC_LOG(LS_ERROR) << "Transport registered with mid " << it.first
                         << " is not in use, transport " << it.second.get();
       return false;
     }
-    const auto& lookup = mid_to_transport_.find(it.first);
-    if (lookup->second != it.second.get()) {
-      // Not an error, but unusual.
-      RTC_DLOG(LS_INFO) << "Note: Mid " << it.first << " was registered to "
-                        << it.second.get() << " but currently maps to "
-                        << lookup->second;
-    }
   }
   return true;
 }
diff --git a/pc/jsep_transport_collection.h b/pc/jsep_transport_collection.h
index 0dd528d..aa52934 100644
--- a/pc/jsep_transport_collection.h
+++ b/pc/jsep_transport_collection.h
@@ -18,6 +18,7 @@
 #include <utility>
 #include <vector>
 
+#include "api/peer_connection_interface.h"
 #include "api/sequence_checker.h"
 #include "pc/jsep_transport.h"
 #include "pc/session_description.h"
@@ -41,7 +42,8 @@
 // 6) Change the logic to do what's right.
 class BundleManager {
  public:
-  BundleManager() {
+  explicit BundleManager(PeerConnectionInterface::BundlePolicy bundle_policy)
+      : bundle_policy_(bundle_policy) {
     // Allow constructor to be called on a different thread.
     sequence_checker_.Detach();
   }
@@ -58,17 +60,27 @@
   bool IsFirstMidInGroup(const std::string& mid) const;
   // Update the groups description. This completely replaces the group
   // description with the one from the SessionDescription.
-  void Update(const cricket::SessionDescription* description);
+  void Update(const cricket::SessionDescription* description, SdpType type);
   // Delete a MID from the group that contains it.
   void DeleteMid(const cricket::ContentGroup* bundle_group,
                  const std::string& mid);
   // Delete a group.
   void DeleteGroup(const cricket::ContentGroup* bundle_group);
+  // Roll back to previous stable state.
+  void Rollback();
+  // Commit current bundle groups.
+  void Commit();
 
  private:
+  // Recalculate established_bundle_groups_by_mid_ from bundle_groups_.
+  void RefreshEstablishedBundleGroupsByMid() RTC_RUN_ON(sequence_checker_);
+
   RTC_NO_UNIQUE_ADDRESS SequenceChecker sequence_checker_;
+  PeerConnectionInterface::BundlePolicy bundle_policy_;
   std::vector<std::unique_ptr<cricket::ContentGroup>> bundle_groups_
       RTC_GUARDED_BY(sequence_checker_);
+  std::vector<std::unique_ptr<cricket::ContentGroup>> stable_bundle_groups_
+      RTC_GUARDED_BY(sequence_checker_);
   std::map<std::string, cricket::ContentGroup*>
       established_bundle_groups_by_mid_;
 };
@@ -91,7 +103,11 @@
 
   void RegisterTransport(const std::string& mid,
                          std::unique_ptr<cricket::JsepTransport> transport);
+  // Returns all transports, including those not currently mapped to any MID
+  // because they're being kept alive in case of rollback.
   std::vector<cricket::JsepTransport*> Transports();
+  // Only returns transports currently mapped to a MID.
+  std::vector<cricket::JsepTransport*> ActiveTransports();
   void DestroyAllTransports();
   // Lookup a JsepTransport by the MID that was used to register it.
   cricket::JsepTransport* GetTransportByName(const std::string& mid);
@@ -108,17 +124,28 @@
   // Remove a transport for a MID. This may destroy a transport if it is
   // no longer in use.
   void RemoveTransportForMid(const std::string& mid);
-  // Roll back pending mid-to-transport mappings.
-  void RollbackTransports();
-  // Commit pending mid-transport mappings (rollback is no longer possible).
+  // Roll back to previous stable mid-to-transport mappings.
+  bool RollbackTransports();
+  // Commit pending mid-transport mappings (rollback is no longer possible),
+  // and destroy unused transports because we know now we'll never need them
+  // again.
   void CommitTransports();
+
+ private:
   // Returns true if any mid currently maps to this transport.
   bool TransportInUse(cricket::JsepTransport* jsep_transport) const;
 
- private:
-  // Destroy a transport if it's no longer in use.
+  // Returns true if any mid in the last stable mapping maps to this transport,
+  // meaning it should be kept alive in case of rollback.
+  bool TransportNeededForRollback(cricket::JsepTransport* jsep_transport) const;
+
+  // Destroy a transport if it's no longer in use. This includes whether it
+  // will be needed in case of rollback.
   void MaybeDestroyJsepTransport(cricket::JsepTransport* transport);
 
+  // Destroys all transports that are no longer in use.
+  void DestroyUnusedTransports();
+
   bool IsConsistent();  // For testing only: Verify internal structure.
 
   RTC_NO_UNIQUE_ADDRESS SequenceChecker sequence_checker_;
@@ -130,8 +157,10 @@
   // (BaseChannel/SctpTransport) and the JsepTransport underneath.
   std::map<std::string, cricket::JsepTransport*> mid_to_transport_
       RTC_GUARDED_BY(sequence_checker_);
-  // Keep track of mids that have been mapped to transports. Used for rollback.
-  std::vector<std::string> pending_mids_ RTC_GUARDED_BY(sequence_checker_);
+  // A snapshot of mid_to_transport_ at the last stable state. Used for
+  // rollback.
+  std::map<std::string, cricket::JsepTransport*> stable_mid_to_transport_
+      RTC_GUARDED_BY(sequence_checker_);
   // Callback used to inform subscribers of altered transports.
   const std::function<bool(const std::string& mid,
                            cricket::JsepTransport* transport)>
diff --git a/pc/jsep_transport_controller.cc b/pc/jsep_transport_controller.cc
index 47fddd4..1b554eb 100644
--- a/pc/jsep_transport_controller.cc
+++ b/pc/jsep_transport_controller.cc
@@ -55,7 +55,8 @@
             UpdateAggregateStates_n();
           }),
       config_(config),
-      active_reset_srtp_params_(config.active_reset_srtp_params) {
+      active_reset_srtp_params_(config.active_reset_srtp_params),
+      bundles_(config.bundle_policy) {
   // The |transport_observer| is assumed to be non-null.
   RTC_DCHECK(config_.transport_observer);
   RTC_DCHECK(config_.rtcp_handler);
@@ -374,13 +375,18 @@
   }
 }
 
-void JsepTransportController::RollbackTransports() {
+RTCError JsepTransportController::RollbackTransports() {
   if (!network_thread_->IsCurrent()) {
-    network_thread_->Invoke<void>(RTC_FROM_HERE, [=] { RollbackTransports(); });
-    return;
+    return network_thread_->Invoke<RTCError>(
+        RTC_FROM_HERE, [=] { return RollbackTransports(); });
   }
   RTC_DCHECK_RUN_ON(network_thread_);
-  transports_.RollbackTransports();
+  bundles_.Rollback();
+  if (!transports_.RollbackTransports()) {
+    LOG_AND_RETURN_ERROR(RTCErrorType::INTERNAL_ERROR,
+                         "Failed to roll back transport state.");
+  }
+  return RTCError::OK();
 }
 
 rtc::scoped_refptr<webrtc::IceTransportInterface>
@@ -525,6 +531,23 @@
   return dtls_transports;
 }
 
+std::vector<cricket::DtlsTransportInternal*>
+JsepTransportController::GetActiveDtlsTransports() {
+  RTC_DCHECK_RUN_ON(network_thread_);
+  std::vector<cricket::DtlsTransportInternal*> dtls_transports;
+  for (auto jsep_transport : transports_.ActiveTransports()) {
+    RTC_DCHECK(jsep_transport);
+    if (jsep_transport->rtp_dtls_transport()) {
+      dtls_transports.push_back(jsep_transport->rtp_dtls_transport());
+    }
+
+    if (jsep_transport->rtcp_dtls_transport()) {
+      dtls_transports.push_back(jsep_transport->rtcp_dtls_transport());
+    }
+  }
+  return dtls_transports;
+}
+
 RTCError JsepTransportController::ApplyDescription_n(
     bool local,
     SdpType type,
@@ -551,17 +574,6 @@
         MergeEncryptedHeaderExtensionIdsForBundles(description);
   }
 
-  // Because the creation of transports depends on whether
-  // certain mids are present, we have to process rejection
-  // before we try to create transports.
-  for (size_t i = 0; i < description->contents().size(); ++i) {
-    const cricket::ContentInfo& content_info = description->contents()[i];
-    if (content_info.rejected) {
-      // This may cause groups to be removed from |bundles_.bundle_groups()|.
-      HandleRejectedContent(content_info);
-    }
-  }
-
   for (const cricket::ContentInfo& content_info : description->contents()) {
     // Don't create transports for rejected m-lines and bundled m-lines.
     if (content_info.rejected ||
@@ -582,6 +594,8 @@
         description->transport_infos()[i];
 
     if (content_info.rejected) {
+      // This may cause groups to be removed from |bundles_.bundle_groups()|.
+      HandleRejectedContent(content_info);
       continue;
     }
 
@@ -647,6 +661,7 @@
   }
   if (type == SdpType::kAnswer) {
     transports_.CommitTransports();
+    bundles_.Commit();
   }
   return RTCError::OK();
 }
@@ -682,7 +697,44 @@
     }
   }
 
-  if (type == SdpType::kAnswer) {
+  if (type == SdpType::kOffer) {
+    // For an offer, we need to verify that there is not a conflicting mapping
+    // between existing and new bundle groups. For example, if the existing
+    // groups are [[1,2],[3,4]] and new are [[1,3],[2,4]] or [[1,2,3,4]], or
+    // vice versa. Switching things around like this requires a separate offer
+    // that removes the relevant sections from their group, as per RFC 8843,
+    // section 7.5.2.
+    std::map<const cricket::ContentGroup*, const cricket::ContentGroup*>
+        new_bundle_groups_by_existing_bundle_groups;
+    std::map<const cricket::ContentGroup*, const cricket::ContentGroup*>
+        existing_bundle_groups_by_new_bundle_groups;
+    for (const cricket::ContentGroup* new_bundle_group : new_bundle_groups) {
+      for (const std::string& mid : new_bundle_group->content_names()) {
+        cricket::ContentGroup* existing_bundle_group =
+            bundles_.LookupGroupByMid(mid);
+        if (!existing_bundle_group) {
+          continue;
+        }
+        auto it = new_bundle_groups_by_existing_bundle_groups.find(
+            existing_bundle_group);
+        if (it != new_bundle_groups_by_existing_bundle_groups.end() &&
+            it->second != new_bundle_group) {
+          return RTCError(RTCErrorType::INVALID_PARAMETER,
+                          "MID " + mid + " in the offer has changed group.");
+        }
+        new_bundle_groups_by_existing_bundle_groups.insert(
+            std::make_pair(existing_bundle_group, new_bundle_group));
+        it = existing_bundle_groups_by_new_bundle_groups.find(new_bundle_group);
+        if (it != existing_bundle_groups_by_new_bundle_groups.end() &&
+            it->second != existing_bundle_group) {
+          return RTCError(RTCErrorType::INVALID_PARAMETER,
+                          "MID " + mid + " in the offer has changed group.");
+        }
+        existing_bundle_groups_by_new_bundle_groups.insert(
+            std::make_pair(new_bundle_group, existing_bundle_group));
+      }
+    }
+  } else if (type == SdpType::kAnswer) {
     std::vector<const cricket::ContentGroup*> offered_bundle_groups =
         local ? remote_desc_->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE)
               : local_desc_->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE);
@@ -762,9 +814,7 @@
                     "max-bundle is used but no bundle group found.");
   }
 
-  if (ShouldUpdateBundleGroup(type, description)) {
-    bundles_.Update(description);
-  }
+  bundles_.Update(description, type);
 
   for (const auto& bundle_group : bundles_.bundle_groups()) {
     if (!bundle_group->FirstContentName())
@@ -872,26 +922,6 @@
       rtp_abs_sendtime_extn_id, transport_info.description);
 }
 
-bool JsepTransportController::ShouldUpdateBundleGroup(
-    SdpType type,
-    const cricket::SessionDescription* description) {
-  if (config_.bundle_policy ==
-      PeerConnectionInterface::kBundlePolicyMaxBundle) {
-    return true;
-  }
-
-  if (type != SdpType::kAnswer) {
-    return false;
-  }
-
-  RTC_DCHECK(local_desc_ && remote_desc_);
-  std::vector<const cricket::ContentGroup*> local_bundles =
-      local_desc_->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE);
-  std::vector<const cricket::ContentGroup*> remote_bundles =
-      remote_desc_->GetGroupsByName(cricket::GROUP_TYPE_BUNDLE);
-  return !local_bundles.empty() && !remote_bundles.empty();
-}
-
 std::vector<int> JsepTransportController::GetEncryptedHeaderExtensionIds(
     const cricket::ContentInfo& content_info) {
   const cricket::MediaContentDescription* content_desc =
@@ -987,21 +1017,6 @@
   if (transport) {
     return RTCError::OK();
   }
-  // If we have agreed to a bundle, the new mid will be added to the bundle
-  // according to JSEP, and the responder can't move it out of the group
-  // according to BUNDLE. So don't create a transport.
-  // The MID will be added to the bundle elsewhere in the code.
-  if (bundles_.bundle_groups().size() > 0) {
-    const auto& default_bundle_group = bundles_.bundle_groups()[0];
-    if (default_bundle_group->content_names().size() > 0) {
-      auto bundle_transport =
-          GetJsepTransportByName(default_bundle_group->content_names()[0]);
-      if (bundle_transport) {
-        transports_.SetTransportForMid(content_info.name, bundle_transport);
-        return RTCError::OK();
-      }
-    }
-  }
   const cricket::MediaContentDescription* content_desc =
       content_info.media_description();
   if (certificate_ && !content_desc->cryptos().empty()) {
@@ -1201,7 +1216,7 @@
 
 void JsepTransportController::UpdateAggregateStates_n() {
   TRACE_EVENT0("webrtc", "JsepTransportController::UpdateAggregateStates_n");
-  auto dtls_transports = GetDtlsTransports();
+  auto dtls_transports = GetActiveDtlsTransports();
   cricket::IceConnectionState new_connection_state =
       cricket::kIceConnectionConnecting;
   PeerConnectionInterface::IceConnectionState new_ice_connection_state =
diff --git a/pc/jsep_transport_controller.h b/pc/jsep_transport_controller.h
index 71b01bf..4161f6a 100644
--- a/pc/jsep_transport_controller.h
+++ b/pc/jsep_transport_controller.h
@@ -224,9 +224,7 @@
 
   void SetActiveResetSrtpParams(bool active_reset_srtp_params);
 
-  // For now the rollback only removes mid to transport mappings
-  // and deletes unused transports, but doesn't consider anything more complex.
-  void RollbackTransports();
+  RTCError RollbackTransports();
 
   // F: void(const std::string&, const std::vector<cricket::Candidate>&)
   template <typename F>
@@ -342,9 +340,6 @@
       const std::vector<int>& encrypted_extension_ids,
       int rtp_abs_sendtime_extn_id);
 
-  bool ShouldUpdateBundleGroup(SdpType type,
-                               const cricket::SessionDescription* description);
-
   std::map<const cricket::ContentGroup*, std::vector<int>>
   MergeEncryptedHeaderExtensionIdsForBundles(
       const cricket::SessionDescription* description);
@@ -411,9 +406,13 @@
       cricket::DtlsTransportInternal* rtcp_dtls_transport);
 
   // Collect all the DtlsTransports, including RTP and RTCP, from the
-  // JsepTransports. JsepTransportController can iterate all the DtlsTransports
-  // and update the aggregate states.
+  // JsepTransports, including those not mapped to a MID because they are being
+  // kept alive in case of rollback.
   std::vector<cricket::DtlsTransportInternal*> GetDtlsTransports();
+  // Same as the above, but doesn't include rollback transports.
+  // JsepTransportController can iterate all the DtlsTransports and update the
+  // aggregate states.
+  std::vector<cricket::DtlsTransportInternal*> GetActiveDtlsTransports();
 
   // Handlers for signals from Transport.
   void OnTransportWritableState_n(rtc::PacketTransportInternal* transport)
diff --git a/pc/jsep_transport_controller_unittest.cc b/pc/jsep_transport_controller_unittest.cc
index a06f580..88fda75 100644
--- a/pc/jsep_transport_controller_unittest.cc
+++ b/pc/jsep_transport_controller_unittest.cc
@@ -861,6 +861,67 @@
   EXPECT_EQ(2, gathering_state_signal_count_);
 }
 
+// Test that states immediately return to "new" if all transports are
+// discarded. This should happen at offer time, even though the transport
+// controller may keep the transport alive in case of rollback.
+TEST_F(JsepTransportControllerTest,
+       IceStatesReturnToNewWhenTransportsDiscarded) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, description.get())
+                  .ok());
+
+  // Trigger and verify initial non-new states.
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  fake_audio_dtls->fake_ice_transport()->MaybeStartGathering();
+  fake_audio_dtls->fake_ice_transport()->SetTransportState(
+      webrtc::IceTransportState::kChecking,
+      cricket::IceTransportState::STATE_CONNECTING);
+  EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking,
+                 ice_connection_state_, kTimeout);
+  EXPECT_EQ(1, ice_connection_state_signal_count_);
+  EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting,
+                 combined_connection_state_, kTimeout);
+  EXPECT_EQ(1, combined_connection_state_signal_count_);
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+
+  // Reject m= section which should disconnect the transport and return states
+  // to "new".
+  description->contents()[0].rejected = true;
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kOffer, description.get())
+                  .ok());
+  EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionNew,
+                 ice_connection_state_, kTimeout);
+  EXPECT_EQ(2, ice_connection_state_signal_count_);
+  EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kNew,
+                 combined_connection_state_, kTimeout);
+  EXPECT_EQ(2, combined_connection_state_signal_count_);
+  EXPECT_EQ_WAIT(cricket::kIceGatheringNew, gathering_state_, kTimeout);
+  EXPECT_EQ(2, gathering_state_signal_count_);
+
+  // For good measure, rollback the offer and verify that states return to
+  // their previous values.
+  EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+  EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking,
+                 ice_connection_state_, kTimeout);
+  EXPECT_EQ(3, ice_connection_state_signal_count_);
+  EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting,
+                 combined_connection_state_, kTimeout);
+  EXPECT_EQ(3, combined_connection_state_signal_count_);
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(3, gathering_state_signal_count_);
+}
+
 TEST_F(JsepTransportControllerTest, SignalCandidatesGathered) {
   CreateJsepTransportController(JsepTransportController::Config());
   auto description = CreateSessionDescriptionWithBundleGroup();
@@ -1608,6 +1669,356 @@
   EXPECT_EQ(mid5_transport, mid6_transport);
 }
 
+TEST_F(JsepTransportControllerTest,
+       MultipleBundleGroupsSectionsAddedInSubsequentOffer) {
+  static const char kMid1Audio[] = "1_audio";
+  static const char kMid2Audio[] = "2_audio";
+  static const char kMid3Audio[] = "3_audio";
+  static const char kMid4Video[] = "4_video";
+  static const char kMid5Video[] = "5_video";
+  static const char kMid6Video[] = "6_video";
+
+  CreateJsepTransportController(JsepTransportController::Config());
+  // Start by grouping (kMid1Audio,kMid2Audio) and (kMid4Video,kMid4f5Video).
+  cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group1.AddContentName(kMid1Audio);
+  bundle_group1.AddContentName(kMid2Audio);
+  cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group2.AddContentName(kMid4Video);
+  bundle_group2.AddContentName(kMid5Video);
+
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_offer->AddGroup(bundle_group1);
+  local_offer->AddGroup(bundle_group2);
+
+  auto remote_answer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  remote_answer->AddGroup(bundle_group1);
+  remote_answer->AddGroup(bundle_group2);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Add kMid3Audio and kMid6Video to the respective audio/video bundle groups.
+  cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group1.AddContentName(kMid3Audio);
+  cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group2.AddContentName(kMid6Video);
+
+  auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(subsequent_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  subsequent_offer->AddGroup(bundle_group1);
+  subsequent_offer->AddGroup(bundle_group2);
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+                  .ok());
+  auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+  auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+  auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video);
+  auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video);
+  auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video);
+  EXPECT_NE(mid1_transport, mid4_transport);
+  EXPECT_EQ(mid1_transport, mid2_transport);
+  EXPECT_EQ(mid2_transport, mid3_transport);
+  EXPECT_EQ(mid4_transport, mid5_transport);
+  EXPECT_EQ(mid5_transport, mid6_transport);
+}
+
+TEST_F(JsepTransportControllerTest,
+       MultipleBundleGroupsCombinedInSubsequentOffer) {
+  static const char kMid1Audio[] = "1_audio";
+  static const char kMid2Audio[] = "2_audio";
+  static const char kMid3Video[] = "3_video";
+  static const char kMid4Video[] = "4_video";
+
+  CreateJsepTransportController(JsepTransportController::Config());
+  // Start by grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video).
+  cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group1.AddContentName(kMid1Audio);
+  bundle_group1.AddContentName(kMid2Audio);
+  cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group2.AddContentName(kMid3Video);
+  bundle_group2.AddContentName(kMid4Video);
+
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_offer->AddGroup(bundle_group1);
+  local_offer->AddGroup(bundle_group2);
+
+  auto remote_answer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  remote_answer->AddGroup(bundle_group1);
+  remote_answer->AddGroup(bundle_group2);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Switch to grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video).
+  // This is a illegal without first removing m= sections from their groups.
+  cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  new_bundle_group.AddContentName(kMid1Audio);
+  new_bundle_group.AddContentName(kMid2Audio);
+  new_bundle_group.AddContentName(kMid3Video);
+  new_bundle_group.AddContentName(kMid4Video);
+
+  auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  subsequent_offer->AddGroup(new_bundle_group);
+  EXPECT_FALSE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+          .ok());
+}
+
+TEST_F(JsepTransportControllerTest,
+       MultipleBundleGroupsSplitInSubsequentOffer) {
+  static const char kMid1Audio[] = "1_audio";
+  static const char kMid2Audio[] = "2_audio";
+  static const char kMid3Video[] = "3_video";
+  static const char kMid4Video[] = "4_video";
+
+  CreateJsepTransportController(JsepTransportController::Config());
+  // Start by grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video).
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kMid1Audio);
+  bundle_group.AddContentName(kMid2Audio);
+  bundle_group.AddContentName(kMid3Video);
+  bundle_group.AddContentName(kMid4Video);
+
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_offer->AddGroup(bundle_group);
+
+  auto remote_answer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  remote_answer->AddGroup(bundle_group);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Switch to grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video).
+  // This is a illegal without first removing m= sections from their groups.
+  cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  new_bundle_group1.AddContentName(kMid1Audio);
+  new_bundle_group1.AddContentName(kMid2Audio);
+  cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  new_bundle_group2.AddContentName(kMid3Video);
+  new_bundle_group2.AddContentName(kMid4Video);
+
+  auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  subsequent_offer->AddGroup(new_bundle_group1);
+  subsequent_offer->AddGroup(new_bundle_group2);
+  EXPECT_FALSE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+          .ok());
+}
+
+TEST_F(JsepTransportControllerTest,
+       MultipleBundleGroupsShuffledInSubsequentOffer) {
+  static const char kMid1Audio[] = "1_audio";
+  static const char kMid2Audio[] = "2_audio";
+  static const char kMid3Video[] = "3_video";
+  static const char kMid4Video[] = "4_video";
+
+  CreateJsepTransportController(JsepTransportController::Config());
+  // Start by grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video).
+  cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group1.AddContentName(kMid1Audio);
+  bundle_group1.AddContentName(kMid2Audio);
+  cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group2.AddContentName(kMid3Video);
+  bundle_group2.AddContentName(kMid4Video);
+
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_offer->AddGroup(bundle_group1);
+  local_offer->AddGroup(bundle_group2);
+
+  auto remote_answer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  remote_answer->AddGroup(bundle_group1);
+  remote_answer->AddGroup(bundle_group2);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Switch to grouping (kMid1Audio,kMid3Video) and (kMid2Audio,kMid3Video).
+  // This is a illegal without first removing m= sections from their groups.
+  cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  new_bundle_group1.AddContentName(kMid1Audio);
+  new_bundle_group1.AddContentName(kMid3Video);
+  cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  new_bundle_group2.AddContentName(kMid2Audio);
+  new_bundle_group2.AddContentName(kMid4Video);
+
+  auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  subsequent_offer->AddGroup(new_bundle_group1);
+  subsequent_offer->AddGroup(new_bundle_group2);
+  EXPECT_FALSE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+          .ok());
+}
+
 // Tests that only a subset of all the m= sections are bundled.
 TEST_F(JsepTransportControllerTest, BundleSubsetOfMediaSections) {
   CreateJsepTransportController(JsepTransportController::Config());
@@ -2059,4 +2470,220 @@
           .ok());
 }
 
+TEST_F(JsepTransportControllerTest, RollbackRestoresRejectedTransport) {
+  static const char kMid1Audio[] = "1_audio";
+
+  // Perform initial offer/answer.
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  std::unique_ptr<cricket::SessionDescription> remote_answer(
+      local_offer->Clone());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+
+  // Apply a reoffer which rejects the m= section, causing the transport to be
+  // set to null.
+  auto local_reoffer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_reoffer->contents()[0].rejected = true;
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_reoffer.get())
+                  .ok());
+  auto old_mid1_transport = mid1_transport;
+  mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  EXPECT_EQ(nullptr, mid1_transport);
+
+  // Rolling back shouldn't just create a new transport for MID 1, it should
+  // restore the old transport.
+  EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+  mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  EXPECT_EQ(old_mid1_transport, mid1_transport);
+}
+
+// If an offer with a modified BUNDLE group causes a MID->transport mapping to
+// change, rollback should restore the previous mapping.
+TEST_F(JsepTransportControllerTest, RollbackRestoresPreviousTransportMapping) {
+  static const char kMid1Audio[] = "1_audio";
+  static const char kMid2Audio[] = "2_audio";
+  static const char kMid3Audio[] = "3_audio";
+
+  // Perform an initial offer/answer to establish a (kMid1Audio,kMid2Audio)
+  // group.
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kMid1Audio);
+  bundle_group.AddContentName(kMid2Audio);
+
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_offer->AddGroup(bundle_group);
+
+  std::unique_ptr<cricket::SessionDescription> remote_answer(
+      local_offer->Clone());
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+  auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+  EXPECT_EQ(mid1_transport, mid2_transport);
+  EXPECT_NE(mid1_transport, mid3_transport);
+
+  // Apply a reoffer adding kMid3Audio to the group; transport mapping should
+  // change, even without an answer, since this is an existing group.
+  bundle_group.AddContentName(kMid3Audio);
+  auto local_reoffer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_reoffer.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_reoffer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_reoffer->AddGroup(bundle_group);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_reoffer.get())
+                  .ok());
+
+  // Store the old transport pointer and verify that the offer actually changed
+  // transports.
+  auto old_mid3_transport = mid3_transport;
+  mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+  mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+  EXPECT_EQ(mid1_transport, mid2_transport);
+  EXPECT_EQ(mid1_transport, mid3_transport);
+
+  // Rolling back shouldn't just create a new transport for MID 3, it should
+  // restore the old transport.
+  EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+  mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+  EXPECT_EQ(old_mid3_transport, mid3_transport);
+}
+
+// Test that if an offer adds a MID to a specific BUNDLE group and is then
+// rolled back, it can be added to a different BUNDLE group in a new offer.
+// This is effectively testing that rollback resets the BundleManager state.
+TEST_F(JsepTransportControllerTest, RollbackAndAddToDifferentBundleGroup) {
+  static const char kMid1Audio[] = "1_audio";
+  static const char kMid2Audio[] = "2_audio";
+  static const char kMid3Audio[] = "3_audio";
+
+  // Perform an initial offer/answer to establish two bundle groups, each with
+  // one MID.
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group1.AddContentName(kMid1Audio);
+  cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group2.AddContentName(kMid2Audio);
+
+  auto local_offer = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  local_offer->AddGroup(bundle_group1);
+  local_offer->AddGroup(bundle_group2);
+
+  std::unique_ptr<cricket::SessionDescription> remote_answer(
+      local_offer->Clone());
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Apply an offer that adds kMid3Audio to the first BUNDLE group.,
+  cricket::ContentGroup modified_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+  modified_bundle_group1.AddContentName(kMid1Audio);
+  modified_bundle_group1.AddContentName(kMid3Audio);
+  auto subsequent_offer_1 = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(subsequent_offer_1.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer_1.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer_1.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  subsequent_offer_1->AddGroup(modified_bundle_group1);
+  subsequent_offer_1->AddGroup(bundle_group2);
+
+  EXPECT_TRUE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, subsequent_offer_1.get())
+          .ok());
+
+  auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+  auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+  EXPECT_NE(mid1_transport, mid2_transport);
+  EXPECT_EQ(mid1_transport, mid3_transport);
+
+  // Rollback and expect the transport to be reset.
+  EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+  EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kMid3Audio));
+
+  // Apply an offer that adds kMid3Audio to the second BUNDLE group.,
+  cricket::ContentGroup modified_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+  modified_bundle_group2.AddContentName(kMid2Audio);
+  modified_bundle_group2.AddContentName(kMid3Audio);
+  auto subsequent_offer_2 = std::make_unique<cricket::SessionDescription>();
+  AddAudioSection(subsequent_offer_2.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer_2.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(subsequent_offer_2.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  subsequent_offer_2->AddGroup(bundle_group1);
+  subsequent_offer_2->AddGroup(modified_bundle_group2);
+
+  EXPECT_TRUE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, subsequent_offer_2.get())
+          .ok());
+
+  mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+  mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+  mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+  EXPECT_NE(mid1_transport, mid2_transport);
+  EXPECT_EQ(mid2_transport, mid3_transport);
+}
+
 }  // namespace webrtc
diff --git a/pc/peer_connection_bundle_unittest.cc b/pc/peer_connection_bundle_unittest.cc
index 08754c6..be05af9 100644
--- a/pc/peer_connection_bundle_unittest.cc
+++ b/pc/peer_connection_bundle_unittest.cc
@@ -909,11 +909,11 @@
 
   EXPECT_TRUE(
       caller->SetLocalDescription(CloneSessionDescription(offer.get())));
-  callee->SetRemoteDescription(std::move(offer));
+  EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
   auto answer = callee->CreateAnswer();
   EXPECT_TRUE(
       callee->SetLocalDescription(CloneSessionDescription(answer.get())));
-  caller->SetRemoteDescription(std::move(answer));
+  EXPECT_TRUE(caller->SetRemoteDescription(std::move(answer)));
 
   // Verify bundling on sender side.
   auto senders = caller->pc()->GetSenders();
@@ -938,4 +938,59 @@
   EXPECT_NE(receiver0_transport, receiver2_transport);
 }
 
+// Test that, with the "max-compat" bundle policy, it's possible to add an m=
+// section that's not part of an existing bundle group.
+TEST_F(PeerConnectionBundleTestUnifiedPlan, AddNonBundledSection) {
+  RTCConfiguration config;
+  config.bundle_policy = PeerConnectionInterface::kBundlePolicyMaxCompat;
+  auto caller = CreatePeerConnection(config);
+  caller->AddAudioTrack("0_audio");
+  caller->AddAudioTrack("1_audio");
+  auto callee = CreatePeerConnection(config);
+
+  // Establish an existing BUNDLE group.
+  auto offer = caller->CreateOffer(RTCOfferAnswerOptions());
+  EXPECT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+  EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+  auto answer = callee->CreateAnswer();
+  EXPECT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+  EXPECT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+
+  // Add a track but munge SDP so it's not part of the bundle group.
+  caller->AddAudioTrack("3_audio");
+  offer = caller->CreateOffer(RTCOfferAnswerOptions());
+  offer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName("0");
+  bundle_group.AddContentName("1");
+  offer->description()->AddGroup(bundle_group);
+  EXPECT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+  EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+  answer = callee->CreateAnswer();
+  EXPECT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+  EXPECT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+
+  // Verify bundling on the sender side.
+  auto senders = caller->pc()->GetSenders();
+  ASSERT_EQ(senders.size(), 3u);
+  auto sender0_transport = senders[0]->dtls_transport();
+  auto sender1_transport = senders[1]->dtls_transport();
+  auto sender2_transport = senders[2]->dtls_transport();
+  EXPECT_EQ(sender0_transport, sender1_transport);
+  EXPECT_NE(sender0_transport, sender2_transport);
+
+  // Verify bundling on receiver side.
+  auto receivers = callee->pc()->GetReceivers();
+  ASSERT_EQ(receivers.size(), 3u);
+  auto receiver0_transport = receivers[0]->dtls_transport();
+  auto receiver1_transport = receivers[1]->dtls_transport();
+  auto receiver2_transport = receivers[2]->dtls_transport();
+  EXPECT_EQ(receiver0_transport, receiver1_transport);
+  EXPECT_NE(receiver0_transport, receiver2_transport);
+}
+
 }  // namespace webrtc
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index 533bd84..929736e 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -2737,7 +2737,10 @@
     transceiver->internal()->set_mid(state.mid());
     transceiver->internal()->set_mline_index(state.mline_index());
   }
-  transport_controller()->RollbackTransports();
+  RTCError e = transport_controller()->RollbackTransports();
+  if (!e.ok()) {
+    return e;
+  }
   transceivers()->DiscardStableStates();
   pending_local_description_.reset();
   pending_remote_description_.reset();
diff --git a/pc/test/mock_peer_connection_observers.h b/pc/test/mock_peer_connection_observers.h
index 413339d..8054e08 100644
--- a/pc/test/mock_peer_connection_observers.h
+++ b/pc/test/mock_peer_connection_observers.h
@@ -116,8 +116,6 @@
   }
   void OnIceCandidate(const IceCandidateInterface* candidate) override {
     RTC_DCHECK(pc_);
-    RTC_DCHECK(PeerConnectionInterface::kIceGatheringNew !=
-               pc_->ice_gathering_state());
     candidates_.push_back(std::make_unique<JsepIceCandidate>(
         candidate->sdp_mid(), candidate->sdp_mline_index(),
         candidate->candidate()));