Implement rollback for setRemoteDescription

Bug: chromium:980875
Change-Id: I4575e9ad1902a20937f9812f49edee2a2441f76d
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/153525
Commit-Queue: Eldar Rello <elrello@microsoft.com>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Steve Anton <steveanton@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#29422}
diff --git a/api/jsep.cc b/api/jsep.cc
index 01f5720..ddb39b6 100644
--- a/api/jsep.cc
+++ b/api/jsep.cc
@@ -41,6 +41,7 @@
 const char SessionDescriptionInterface::kOffer[] = "offer";
 const char SessionDescriptionInterface::kPrAnswer[] = "pranswer";
 const char SessionDescriptionInterface::kAnswer[] = "answer";
+const char SessionDescriptionInterface::kRollback[] = "rollback";
 
 const char* SdpTypeToString(SdpType type) {
   switch (type) {
@@ -50,6 +51,8 @@
       return SessionDescriptionInterface::kPrAnswer;
     case SdpType::kAnswer:
       return SessionDescriptionInterface::kAnswer;
+    case SdpType::kRollback:
+      return SessionDescriptionInterface::kRollback;
   }
   return "";
 }
@@ -61,6 +64,8 @@
     return SdpType::kPrAnswer;
   } else if (type_str == SessionDescriptionInterface::kAnswer) {
     return SdpType::kAnswer;
+  } else if (type_str == SessionDescriptionInterface::kRollback) {
+    return SdpType::kRollback;
   } else {
     return absl::nullopt;
   }
diff --git a/api/jsep.h b/api/jsep.h
index 6da7827..3f7f12a 100644
--- a/api/jsep.h
+++ b/api/jsep.h
@@ -103,8 +103,11 @@
   kOffer,     // Description must be treated as an SDP offer.
   kPrAnswer,  // Description must be treated as an SDP answer, but not a final
               // answer.
-  kAnswer  // Description must be treated as an SDP final answer, and the offer-
-           // answer exchange must be considered complete after receiving this.
+  kAnswer,    // Description must be treated as an SDP final answer, and the
+              // offer-answer exchange must be considered complete after
+              // receiving this.
+  kRollback   // Resets any pending offers and sets signaling state back to
+              // stable.
 };
 
 // Returns the string form of the given SDP type. String forms are defined in
@@ -128,6 +131,7 @@
   static const char kOffer[];
   static const char kPrAnswer[];
   static const char kAnswer[];
+  static const char kRollback[];
 
   virtual ~SessionDescriptionInterface() {}
 
diff --git a/api/peer_connection_interface.h b/api/peer_connection_interface.h
index a417641..f526c37 100644
--- a/api/peer_connection_interface.h
+++ b/api/peer_connection_interface.h
@@ -659,6 +659,9 @@
     // logs with TURN server logs. It will not be added if it's an empty string.
     std::string turn_logging_id;
 
+    // Added to be able to control rollout of this feature.
+    bool enable_implicit_rollback = false;
+
     //
     // Don't forget to update operator== if adding something.
     //
diff --git a/pc/jsep_session_description.cc b/pc/jsep_session_description.cc
index cc544dc..7f30b50 100644
--- a/pc/jsep_session_description.cc
+++ b/pc/jsep_session_description.cc
@@ -152,8 +152,10 @@
     const std::string& sdp,
     SdpParseError* error_out) {
   auto jsep_desc = std::make_unique<JsepSessionDescription>(type);
-  if (!SdpDeserialize(sdp, jsep_desc.get(), error_out)) {
-    return nullptr;
+  if (type != SdpType::kRollback) {
+    if (!SdpDeserialize(sdp, jsep_desc.get(), error_out)) {
+      return nullptr;
+    }
   }
   return std::move(jsep_desc);
 }
diff --git a/pc/jsep_transport_controller.cc b/pc/jsep_transport_controller.cc
index 52ae53c..d83b16e 100644
--- a/pc/jsep_transport_controller.cc
+++ b/pc/jsep_transport_controller.cc
@@ -471,6 +471,16 @@
       use_datagram_transport_for_data_channels_receive_only;
 }
 
+void JsepTransportController::RollbackTransportForMid(const std::string& mid) {
+  if (!network_thread_->IsCurrent()) {
+    network_thread_->Invoke<void>(RTC_FROM_HERE,
+                                  [=] { RollbackTransportForMid(mid); });
+    return;
+  }
+  RemoveTransportForMid(mid);
+  MaybeDestroyJsepTransport(mid);
+}
+
 std::unique_ptr<cricket::IceTransportInternal>
 JsepTransportController::CreateIceTransport(const std::string transport_name,
                                             bool rtcp) {
diff --git a/pc/jsep_transport_controller.h b/pc/jsep_transport_controller.h
index a46a71e..bcaeed5 100644
--- a/pc/jsep_transport_controller.h
+++ b/pc/jsep_transport_controller.h
@@ -239,6 +239,10 @@
       bool use_datagram_transport_for_data_channels,
       bool use_datagram_transport_for_data_channels_receive_only);
 
+  // TODO(elrello): For now the rollback only removes mid to transport mapping
+  // and deletes unused transport, but doesn't consider anything more complex.
+  void RollbackTransportForMid(const std::string& mid);
+
   // If media transport is present enabled and supported,
   // when this method is called, it creates a media transport and generates its
   // offer. The new offer is then returned, and the created media transport will
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index c2723e7f..c0e1831 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -782,6 +782,7 @@
     absl::optional<CryptoOptions> crypto_options;
     bool offer_extmap_allow_mixed;
     std::string turn_logging_id;
+    bool enable_implicit_rollback;
   };
   static_assert(sizeof(stuff_being_tested_for_equality) == sizeof(*this),
                 "Did you add something to RTCConfiguration and forget to "
@@ -847,7 +848,8 @@
              o.use_datagram_transport_for_data_channels_receive_only &&
          crypto_options == o.crypto_options &&
          offer_extmap_allow_mixed == o.offer_extmap_allow_mixed &&
-         turn_logging_id == o.turn_logging_id;
+         turn_logging_id == o.turn_logging_id &&
+         enable_implicit_rollback == o.enable_implicit_rollback;
 }
 
 bool PeerConnectionInterface::RTCConfiguration::operator!=(
@@ -2257,6 +2259,23 @@
     return;
   }
 
+  // For SLD we support only explicit rollback.
+  if (desc->GetType() == SdpType::kRollback) {
+    if (IsUnifiedPlan()) {
+      RTCError error = Rollback();
+      if (error.ok()) {
+        PostSetSessionDescriptionSuccess(observer);
+      } else {
+        PostSetSessionDescriptionFailure(observer, std::move(error));
+      }
+    } else {
+      PostSetSessionDescriptionFailure(
+          observer, RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
+                             "Rollback not supported in Plan B"));
+    }
+    return;
+  }
+
   RTCError error = ValidateSessionDescription(desc.get(), cricket::CS_LOCAL);
   if (!error.ok()) {
     std::string error_message = GetSetDescriptionErrorMessage(
@@ -2629,7 +2648,24 @@
         RTCError(RTCErrorType::INTERNAL_ERROR, std::move(error_message)));
     return;
   }
-
+  if (IsUnifiedPlan()) {
+    if (configuration_.enable_implicit_rollback) {
+      if (desc->GetType() == SdpType::kOffer &&
+          signaling_state() == kHaveLocalOffer) {
+        Rollback();
+      }
+    }
+    // Explicit rollback.
+    if (desc->GetType() == SdpType::kRollback) {
+      observer->OnSetRemoteDescriptionComplete(Rollback());
+      return;
+    }
+  } else if (desc->GetType() == SdpType::kRollback) {
+    observer->OnSetRemoteDescriptionComplete(
+        RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
+                 "Rollback not supported in Plan B"));
+    return;
+  }
   if (desc->GetType() == SdpType::kOffer) {
     // Report to UMA the format of the received offer.
     ReportSdpFormatReceived(*desc);
@@ -3382,8 +3418,12 @@
       transceiver = CreateAndAddTransceiver(sender, receiver);
       transceiver->internal()->set_direction(
           RtpTransceiverDirection::kRecvOnly);
+      if (type == SdpType::kOffer) {
+        transceiver_stable_states_by_transceivers_[transceiver] =
+            TransceiverStableState(RtpTransceiverDirection::kRecvOnly,
+                                   absl::nullopt, absl::nullopt, true);
+      }
     }
-
     // Check if the offer indicated simulcast but the answer rejected it.
     // This can happen when simulcast is not supported on the remote party.
     if (SimulcastIsRejected(old_local_content, *media_desc)) {
@@ -3416,6 +3456,20 @@
       return std::move(error);
     }
   }
+  if (type == SdpType::kOffer) {
+    // Make sure we don't overwrite existing stable states and that the
+    // state is really going to change when adding new record to the map.
+    auto it = transceiver_stable_states_by_transceivers_.find(transceiver);
+    if (it == transceiver_stable_states_by_transceivers_.end() &&
+        (transceiver->internal()->mid() != content.name ||
+         transceiver->internal()->mline_index() != mline_index)) {
+      transceiver_stable_states_by_transceivers_[transceiver] =
+          TransceiverStableState(transceiver->internal()->direction(),
+                                 transceiver->internal()->mid(),
+                                 transceiver->internal()->mline_index(), false);
+    }
+  }
+
   // Associate the found or created RtpTransceiver with the m= section by
   // setting the value of the RtpTransceiver's mid property to the MID of the m=
   // section, and establish a mapping between the transceiver and the index of
@@ -5837,6 +5891,7 @@
   } else {
     RTC_DCHECK(type == SdpType::kAnswer);
     ChangeSignalingState(PeerConnectionInterface::kStable);
+    transceiver_stable_states_by_transceivers_.clear();
   }
 
   // Update internal objects according to the session description's media
@@ -7550,4 +7605,51 @@
   return false;
 }
 
+RTCError PeerConnection::Rollback() {
+  auto state = signaling_state();
+  if (state != PeerConnectionInterface::kHaveLocalOffer &&
+      state != PeerConnectionInterface::kHaveRemoteOffer) {
+    return RTCError(RTCErrorType::INVALID_STATE,
+                    "Called in wrong signalingState: " +
+                        GetSignalingStateString(signaling_state()));
+  }
+  RTC_DCHECK_RUN_ON(signaling_thread());
+  RTC_DCHECK(IsUnifiedPlan());
+
+  for (auto&& transceivers_stable_state_pair :
+       transceiver_stable_states_by_transceivers_) {
+    auto transceiver = transceivers_stable_state_pair.first;
+    auto state = transceivers_stable_state_pair.second;
+    RTC_DCHECK(transceiver->internal()->mid().has_value());
+    std::string mid = transceiver->internal()->mid().value();
+    transport_controller_->RollbackTransportForMid(mid);
+    DestroyTransceiverChannel(transceiver);
+
+    if (state.newly_created()) {
+      // Remove added transceivers with no added track.
+      if (transceiver->internal()->sender()->track()) {
+        transceiver->internal()->set_created_by_addtrack(true);
+      } else {
+        int remaining_transceiver_count = 0;
+        for (auto&& t : transceivers_) {
+          if (t != transceiver) {
+            transceivers_[remaining_transceiver_count++] = t;
+          }
+        }
+        transceivers_.resize(remaining_transceiver_count);
+      }
+    }
+    transceiver->internal()->sender_internal()->set_transport(nullptr);
+    transceiver->internal()->receiver_internal()->set_transport(nullptr);
+    transceiver->internal()->set_direction(state.direction());
+    transceiver->internal()->set_mid(state.mid());
+    transceiver->internal()->set_mline_index(state.mline_index());
+  }
+  transceiver_stable_states_by_transceivers_.clear();
+  pending_local_description_.reset();
+  pending_remote_description_.reset();
+  ChangeSignalingState(PeerConnectionInterface::kStable);
+  return RTCError::OK();
+}
+
 }  // namespace webrtc
diff --git a/pc/peer_connection.h b/pc/peer_connection.h
index c783ae9..393a1dd 100644
--- a/pc/peer_connection.h
+++ b/pc/peer_connection.h
@@ -391,6 +391,34 @@
     FieldTrialFlag receive_only;
   };
 
+  // Captures partial state to be used for rollback. Applicable only in
+  // Unified Plan.
+  class TransceiverStableState {
+   public:
+    TransceiverStableState() {}
+    TransceiverStableState(RtpTransceiverDirection direction,
+                           absl::optional<std::string> mid,
+                           absl::optional<size_t> mline_index,
+                           bool newly_created)
+        : direction_(direction),
+          mid_(mid),
+          mline_index_(mline_index),
+          newly_created_(newly_created) {}
+    RtpTransceiverDirection direction() const { return direction_; }
+    absl::optional<std::string> mid() const { return mid_; }
+    absl::optional<size_t> mline_index() const { return mline_index_; }
+    bool newly_created() const { return newly_created_; }
+
+   private:
+    RtpTransceiverDirection direction_ = RtpTransceiverDirection::kRecvOnly;
+    absl::optional<std::string> mid_;
+    absl::optional<size_t> mline_index_;
+    // Indicates that the transceiver was created as part of applying a
+    // description to track potential need for removing transceiver during
+    // rollback.
+    bool newly_created_ = false;
+  };
+
   // Implements MessageHandler.
   void OnMessage(rtc::Message* msg) override;
 
@@ -1165,6 +1193,7 @@
 
   void UpdateNegotiationNeeded();
   bool CheckIfNegotiationIsNeeded();
+  RTCError Rollback();
 
   sigslot::signal1<DataChannel*> SignalDataChannelCreated_
       RTC_GUARDED_BY(signaling_thread());
@@ -1286,7 +1315,11 @@
       RTC_GUARDED_BY(signaling_thread());  // A pointer is passed to senders_
   rtc::scoped_refptr<RTCStatsCollector> stats_collector_
       RTC_GUARDED_BY(signaling_thread());
-
+  // Holds changes made to transceivers during applying descriptors for
+  // potential rollback. Gets cleared once signaling state goes to stable.
+  std::map<rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>,
+           TransceiverStableState>
+      transceiver_stable_states_by_transceivers_;
   std::vector<
       rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
       transceivers_;  // TODO(bugs.webrtc.org/9987): Accessed on both signaling
diff --git a/pc/peer_connection_integrationtest.cc b/pc/peer_connection_integrationtest.cc
index 3a0ef0f..b06091b 100644
--- a/pc/peer_connection_integrationtest.cc
+++ b/pc/peer_connection_integrationtest.cc
@@ -230,7 +230,7 @@
   // will set the whole offer/answer exchange in motion. Just need to wait for
   // the signaling state to reach "stable".
   void CreateAndSetAndSignalOffer() {
-    auto offer = CreateOffer();
+    auto offer = CreateOfferAndWait();
     ASSERT_NE(nullptr, offer);
     EXPECT_TRUE(SetLocalDescriptionAndSendSdpMessage(std::move(offer)));
   }
@@ -302,6 +302,13 @@
     return ice_candidate_pair_change_history_;
   }
 
+  // Every PeerConnection signaling state in order that has been seen by the
+  // observer.
+  std::vector<PeerConnectionInterface::SignalingState>
+  peer_connection_signaling_state_history() const {
+    return peer_connection_signaling_state_history_;
+  }
+
   void AddAudioVideoTracks() {
     AddAudioTrack();
     AddVideoTrack();
@@ -577,6 +584,14 @@
     network_manager()->set_mdns_responder(std::move(mdns_responder));
   }
 
+  // Returns null on failure.
+  std::unique_ptr<SessionDescriptionInterface> CreateOfferAndWait() {
+    rtc::scoped_refptr<MockCreateSessionDescriptionObserver> observer(
+        new rtc::RefCountedObject<MockCreateSessionDescriptionObserver>());
+    pc()->CreateOffer(observer, offer_answer_options_);
+    return WaitForDescriptionFromObserver(observer);
+  }
+
  private:
   explicit PeerConnectionWrapper(const std::string& debug_name)
       : debug_name_(debug_name) {}
@@ -732,14 +747,6 @@
   }
 
   // Returns null on failure.
-  std::unique_ptr<SessionDescriptionInterface> CreateOffer() {
-    rtc::scoped_refptr<MockCreateSessionDescriptionObserver> observer(
-        new rtc::RefCountedObject<MockCreateSessionDescriptionObserver>());
-    pc()->CreateOffer(observer, offer_answer_options_);
-    return WaitForDescriptionFromObserver(observer);
-  }
-
-  // Returns null on failure.
   std::unique_ptr<SessionDescriptionInterface> CreateAnswer() {
     rtc::scoped_refptr<MockCreateSessionDescriptionObserver> observer(
         new rtc::RefCountedObject<MockCreateSessionDescriptionObserver>());
@@ -894,6 +901,7 @@
   void OnSignalingChange(
       webrtc::PeerConnectionInterface::SignalingState new_state) override {
     EXPECT_EQ(pc()->signaling_state(), new_state);
+    peer_connection_signaling_state_history_.push_back(new_state);
   }
   void OnAddTrack(rtc::scoped_refptr<RtpReceiverInterface> receiver,
                   const std::vector<rtc::scoped_refptr<MediaStreamInterface>>&
@@ -1037,7 +1045,8 @@
       ice_gathering_state_history_;
   std::vector<cricket::CandidatePairChangeEvent>
       ice_candidate_pair_change_history_;
-
+  std::vector<PeerConnectionInterface::SignalingState>
+      peer_connection_signaling_state_history_;
   webrtc::FakeRtcEventLogFactory* event_log_factory_;
 
   rtc::AsyncInvoker invoker_;
@@ -5991,6 +6000,61 @@
             caller()->error_event().host_candidate.find(":"));
 }
 
+TEST_F(PeerConnectionIntegrationTestUnifiedPlan,
+       AudioKeepsFlowingAfterImplicitRollback) {
+  PeerConnectionInterface::RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.enable_implicit_rollback = true;
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfig(config, config));
+  ConnectFakeSignaling();
+  caller()->AddAudioTrack();
+  callee()->AddAudioTrack();
+  caller()->CreateAndSetAndSignalOffer();
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+  MediaExpectations media_expectations;
+  media_expectations.ExpectBidirectionalAudio();
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+  SetSignalIceCandidates(false);  // Workaround candidate outrace sdp.
+  caller()->AddVideoTrack();
+  callee()->AddVideoTrack();
+  rtc::scoped_refptr<MockSetSessionDescriptionObserver> observer(
+      new rtc::RefCountedObject<MockSetSessionDescriptionObserver>());
+  callee()->pc()->SetLocalDescription(observer,
+                                      callee()->CreateOfferAndWait().release());
+  EXPECT_TRUE_WAIT(observer->called(), kDefaultTimeout);
+  caller()->CreateAndSetAndSignalOffer();  // Implicit rollback.
+  ASSERT_TRUE_WAIT(SignalingStateStable(), kDefaultTimeout);
+  ASSERT_TRUE(ExpectNewFrames(media_expectations));
+}
+
+TEST_F(PeerConnectionIntegrationTestUnifiedPlan,
+       ImplicitRollbackVisitsStableState) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.enable_implicit_rollback = true;
+
+  ASSERT_TRUE(CreatePeerConnectionWrappersWithConfig(config, config));
+
+  rtc::scoped_refptr<MockSetSessionDescriptionObserver> sld_observer(
+      new rtc::RefCountedObject<MockSetSessionDescriptionObserver>());
+  callee()->pc()->SetLocalDescription(sld_observer,
+                                      callee()->CreateOfferAndWait().release());
+  EXPECT_TRUE_WAIT(sld_observer->called(), kDefaultTimeout);
+  EXPECT_EQ(sld_observer->error(), "");
+
+  rtc::scoped_refptr<MockSetSessionDescriptionObserver> srd_observer(
+      new rtc::RefCountedObject<MockSetSessionDescriptionObserver>());
+  callee()->pc()->SetRemoteDescription(
+      srd_observer, caller()->CreateOfferAndWait().release());
+  EXPECT_TRUE_WAIT(srd_observer->called(), kDefaultTimeout);
+  EXPECT_EQ(srd_observer->error(), "");
+
+  EXPECT_THAT(callee()->peer_connection_signaling_state_history(),
+              ElementsAre(PeerConnectionInterface::kHaveLocalOffer,
+                          PeerConnectionInterface::kStable,
+                          PeerConnectionInterface::kHaveRemoteOffer));
+}
+
 INSTANTIATE_TEST_SUITE_P(PeerConnectionIntegrationTest,
                          PeerConnectionIntegrationTest,
                          Values(SdpSemantics::kPlanB,
diff --git a/pc/peer_connection_jsep_unittest.cc b/pc/peer_connection_jsep_unittest.cc
index 1fe8d07..514374b 100644
--- a/pc/peer_connection_jsep_unittest.cc
+++ b/pc/peer_connection_jsep_unittest.cc
@@ -1727,4 +1727,220 @@
       error);
 }
 
+TEST_F(PeerConnectionJsepTest, RollbackSupportedInUnifiedPlan) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.enable_implicit_rollback = true;
+  auto caller = CreatePeerConnection(config);
+  auto callee = CreatePeerConnection(config);
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(caller->SetLocalDescription(caller->CreateRollback()));
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(caller->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(caller->SetRemoteDescription(callee->CreateOffer()));
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackNotSupportedInPlanB) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kPlanB;
+  config.enable_implicit_rollback = true;
+  auto caller = CreatePeerConnection(config);
+  auto callee = CreatePeerConnection(config);
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_FALSE(caller->SetLocalDescription(caller->CreateRollback()));
+  EXPECT_FALSE(caller->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_FALSE(caller->SetRemoteDescription(callee->CreateOffer()));
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackFailsInStableState) {
+  auto caller = CreatePeerConnection();
+  EXPECT_FALSE(caller->SetLocalDescription(caller->CreateRollback()));
+  EXPECT_FALSE(caller->SetRemoteDescription(caller->CreateRollback()));
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackToStableStateAndClearLocalOffer) {
+  auto caller = CreatePeerConnection();
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(caller->SetLocalDescription(caller->CreateRollback()));
+  EXPECT_EQ(caller->signaling_state(), PeerConnectionInterface::kStable);
+  EXPECT_EQ(caller->pc()->pending_local_description(), nullptr);
+
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(caller->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_EQ(caller->signaling_state(), PeerConnectionInterface::kStable);
+  EXPECT_EQ(caller->pc()->pending_local_description(), nullptr);
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackToStableStateAndClearRemoteOffer) {
+  auto caller = CreatePeerConnection();
+  auto callee = CreatePeerConnection();
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_EQ(callee->signaling_state(), PeerConnectionInterface::kStable);
+  EXPECT_EQ(callee->pc()->pending_remote_description(), nullptr);
+
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_TRUE(callee->SetLocalDescription(caller->CreateRollback()));
+  EXPECT_EQ(callee->signaling_state(), PeerConnectionInterface::kStable);
+  EXPECT_EQ(callee->pc()->pending_remote_description(), nullptr);
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackLocalOfferImplicitly) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.enable_implicit_rollback = true;
+  auto caller = CreatePeerConnection(config);
+  auto callee = CreatePeerConnection(config);
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_EQ(callee->signaling_state(),
+            PeerConnectionInterface::kHaveRemoteOffer);
+}
+
+TEST_F(PeerConnectionJsepTest, AttemptToRollbackLocalOfferImplicitly) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.enable_implicit_rollback = true;
+  auto caller = CreatePeerConnection(config);
+  auto callee = CreatePeerConnection(config);
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  EXPECT_FALSE(callee->SetRemoteDescription(
+      CreateSessionDescription(SdpType::kOffer, "invalid sdp")));
+  EXPECT_EQ(callee->signaling_state(),
+            PeerConnectionInterface::kHaveLocalOffer);
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackRemovesTransceiver) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{1});
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{0});
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackKeepsTransceiverAndClearsMid) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  callee->AddAudioTrack("a");
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{1});
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback()));
+  // Transceiver can't be removed as track was added to it.
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{1});
+  // Mid got cleared to make it reusable.
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt);
+  // Transceiver should be counted as addTrack-created after rollback.
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{1});
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackRestoresMid) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+  auto offer = callee->CreateOffer();
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{1});
+  EXPECT_NE(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt);
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt);
+  EXPECT_TRUE(callee->SetLocalDescription(std::move(offer)));
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackRestoresMidAndRemovesTransceiver) {
+  auto callee = CreatePeerConnection();
+  callee->AddVideoTrack("a");
+  auto offer = callee->CreateOffer();
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("b");
+  caller->AddVideoTrack("c");
+  auto mid = callee->pc()->GetTransceivers()[0]->mid();
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{2});
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{1});
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), mid);
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->media_type(),
+            cricket::MEDIA_TYPE_VIDEO);
+  EXPECT_TRUE(callee->SetLocalDescription(std::move(offer)));
+}
+
+TEST_F(PeerConnectionJsepTest, ImplicitlyRollbackTransceiversWithSameMids) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.enable_implicit_rollback = true;
+  auto caller = CreatePeerConnection(config);
+  caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  auto callee = CreatePeerConnection(config);
+  callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  auto initial_mid = callee->pc()->GetTransceivers()[0]->mid();
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{2});
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt);
+  EXPECT_EQ(callee->pc()->GetTransceivers()[1]->mid(),
+            caller->pc()->GetTransceivers()[0]->mid());
+  EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal());  // Go to stable.
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  EXPECT_NE(callee->pc()->GetTransceivers()[0]->mid(), initial_mid);
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackToNegotiatedStableState) {
+  RTCConfiguration config;
+  config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+  config.bundle_policy = PeerConnectionInterface::kBundlePolicyMaxBundle;
+  auto caller = CreatePeerConnection(config);
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection(config);
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal());
+  caller->AddVideoTrack("a");
+  callee->AddVideoTrack("b");
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{2});
+  auto audio_transport =
+      callee->pc()->GetTransceivers()[0]->sender()->dtls_transport();
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(),
+            callee->pc()->GetTransceivers()[1]->sender()->dtls_transport());
+  EXPECT_NE(callee->pc()->GetTransceivers()[1]->sender()->dtls_transport(),
+            nullptr);
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateRollback()));
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(),
+            audio_transport);  // Audio must remain working after rollback.
+  EXPECT_EQ(callee->pc()->GetTransceivers()[1]->sender()->dtls_transport(),
+            nullptr);
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->sender()->dtls_transport(),
+            audio_transport);  // Audio transport is still the same.
+}
+
+TEST_F(PeerConnectionJsepTest, RollbackAfterMultipleSLD) {
+  auto callee = CreatePeerConnection();
+  callee->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  callee->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  EXPECT_TRUE(callee->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(callee->SetRemoteDescription(callee->CreateRollback()));
+  EXPECT_EQ(callee->pc()->GetTransceivers().size(), size_t{2});
+  EXPECT_EQ(callee->pc()->GetTransceivers()[0]->mid(), absl::nullopt);
+  EXPECT_EQ(callee->pc()->GetTransceivers()[1]->mid(), absl::nullopt);
+}
+
+TEST_F(PeerConnectionJsepTest, NoRollbackNeeded) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+  callee->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(caller->CreateOfferAndSetAsLocal());
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+  EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOffer()));
+}
+
 }  // namespace webrtc
diff --git a/pc/peer_connection_wrapper.cc b/pc/peer_connection_wrapper.cc
index b4b0782..7c0b339 100644
--- a/pc/peer_connection_wrapper.cc
+++ b/pc/peer_connection_wrapper.cc
@@ -125,6 +125,11 @@
   return answer;
 }
 
+std::unique_ptr<SessionDescriptionInterface>
+PeerConnectionWrapper::CreateRollback() {
+  return CreateSessionDescription(SdpType::kRollback, "");
+}
+
 std::unique_ptr<SessionDescriptionInterface> PeerConnectionWrapper::CreateSdp(
     rtc::FunctionView<void(CreateSessionDescriptionObserver*)> fn,
     std::string* error_out) {
diff --git a/pc/peer_connection_wrapper.h b/pc/peer_connection_wrapper.h
index fafee24..4d2bc28 100644
--- a/pc/peer_connection_wrapper.h
+++ b/pc/peer_connection_wrapper.h
@@ -87,6 +87,7 @@
       const PeerConnectionInterface::RTCOfferAnswerOptions& options);
   // Calls CreateAnswerAndSetAsLocal with default options.
   std::unique_ptr<SessionDescriptionInterface> CreateAnswerAndSetAsLocal();
+  std::unique_ptr<SessionDescriptionInterface> CreateRollback();
 
   // Calls the underlying PeerConnection's SetLocalDescription method with the
   // given session description and waits for the success/failure response.
diff --git a/pc/sdp_utils.cc b/pc/sdp_utils.cc
index 5bfdaa4..f5385a6 100644
--- a/pc/sdp_utils.cc
+++ b/pc/sdp_utils.cc
@@ -29,8 +29,10 @@
     SdpType type) {
   RTC_DCHECK(sdesc);
   auto clone = std::make_unique<JsepSessionDescription>(type);
-  clone->Initialize(sdesc->description()->Clone(), sdesc->session_id(),
-                    sdesc->session_version());
+  if (sdesc->description()) {
+    clone->Initialize(sdesc->description()->Clone(), sdesc->session_id(),
+                      sdesc->session_version());
+  }
   // As of writing, our version of GCC does not allow returning a unique_ptr of
   // a subclass as a unique_ptr of a base class. To get around this, we need to
   // std::move the return value.