Introduce SdpOfferAnswerHandler::RemoteDescriptionOperation.

This is an operation specific subclass of SdpOfferAnswerHandler that in
this first step, takes over the implementation details that before this
CL were implemented in SdpOfferAnswerHandler::DoSetRemoteDescription.

This CL does not change the behavior of the implementation but it does
break up DoSetRemoteDescription into smaller methods and moves the state
related to the SRD operation, into a class that in upcoming steps can
be passed around asynchronously as needed, which will allow us to avoid
blocking threads.

Bug: webrtc:13540
Change-Id: Id2002d2390a4a13725f5967df5b82064b37c7490
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/244980
Reviewed-by: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#35669}
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index 9ed8217..7f3f829 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -747,6 +747,176 @@
 
 }  // namespace
 
+// This class stores state related to a SetRemoteDescription operation, captures
+// and reports potential errors that migth occur and makes sure to notify the
+// observer of the operation and the operations chain of completion.
+class SdpOfferAnswerHandler::RemoteDescriptionOperation {
+ public:
+  RemoteDescriptionOperation(
+      SdpOfferAnswerHandler* handler,
+      std::unique_ptr<SessionDescriptionInterface> desc,
+      rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer,
+      std::function<void()> operations_chain_callback)
+      : handler_(handler),
+        desc_(std::move(desc)),
+        observer_(std::move(observer)),
+        operations_chain_callback_(std::move(operations_chain_callback)) {
+    if (!desc_) {
+      InvalidParam("SessionDescription is NULL.");
+    } else {
+      type_ = desc_->GetType();
+    }
+  }
+
+  ~RemoteDescriptionOperation() {
+    RTC_DCHECK_RUN_ON(handler_->signaling_thread());
+    SignalCompletion();
+    operations_chain_callback_();
+  }
+
+  bool ok() const { return error_.ok(); }
+
+  // Notifies the observer that the operation is complete and releases the
+  // reference to the observer.
+  void SignalCompletion() {
+    if (observer_) {
+      observer_->OnSetRemoteDescriptionComplete(error_);
+      observer_ = nullptr;  // Only fire the notification once.
+    }
+  }
+
+  // If a session error has occurred the PeerConnection is in a possibly
+  // inconsistent state so fail right away.
+  bool HaveSessionError() {
+    RTC_DCHECK(ok());
+    if (handler_->session_error() == SessionError::kNone)
+      return false;
+    SetError(RTCErrorType::INTERNAL_ERROR, handler_->GetSessionErrorMsg());
+    return true;
+  }
+
+  // Returns true if the operation was a rollback operation. If this function
+  // returns true, the caller should consider the operation complete. Otherwise
+  // proceed to the next step.
+  bool MaybeRollback() {
+    RTC_DCHECK_RUN_ON(handler_->signaling_thread());
+    RTC_DCHECK(ok());
+    if (type_ != SdpType::kRollback) {
+      // Check if we can do an implicit rollback.
+      if (type_ == SdpType::kOffer && handler_->IsUnifiedPlan() &&
+          handler_->pc_->configuration()->enable_implicit_rollback &&
+          handler_->signaling_state() ==
+              PeerConnectionInterface::kHaveLocalOffer) {
+        handler_->Rollback(type_);
+      }
+      return false;
+    }
+
+    if (handler_->IsUnifiedPlan()) {
+      error_ = handler_->Rollback(type_);
+    } else if (type_ == SdpType::kRollback) {
+      Unsupported("Rollback not supported in Plan B");
+    }
+
+    return true;
+  }
+
+  // Report to UMA the format of the received offer or answer.
+  void ReportOfferAnswerUma() {
+    RTC_DCHECK(ok());
+    if (type_ == SdpType::kOffer || type_ == SdpType::kAnswer) {
+      handler_->pc_->ReportSdpFormatReceived(*desc_.get());
+      handler_->pc_->ReportSdpBundleUsage(*desc_.get());
+    }
+  }
+
+  // Checks if the session description for the operation is valid. If not, the
+  // function captures error information and returns false. Note that if the
+  // return value is false, the operation should be considered done.
+  bool IsDescriptionValid() {
+    RTC_DCHECK_RUN_ON(handler_->signaling_thread());
+    RTC_DCHECK(ok());
+    RTC_DCHECK(bundle_groups_by_mid_.empty()) << "Already called?";
+    bundle_groups_by_mid_ = GetBundleGroupsByMid(description());
+    error_ = handler_->ValidateSessionDescription(
+        desc_.get(), cricket::CS_REMOTE, bundle_groups_by_mid_);
+    if (!error_.ok()) {
+      std::string error_message =
+          GetSetDescriptionErrorMessage(cricket::CS_REMOTE, type_, error_);
+      RTC_LOG(LS_ERROR) << error_message;
+      error_.set_message(std::move(error_message));
+      return false;
+    }
+
+    return true;
+  }
+
+  // TODO(tommi): Instead of calling `handler_->ApplyRemoteDescription` here,
+  // pass the ownership of `this` to that method and break down the steps
+  // currently implemented in that function into smaller methods implemented
+  // by this class. Once we have all of this broken down, we can start avoiding
+  // the embedded calls to Invoke() and apply the description asynchronously.
+  bool ApplyRemoteDescription() {
+    RTC_DCHECK_RUN_ON(handler_->signaling_thread());
+    RTC_DCHECK(ok());
+    // TODO(tommi): It's not ideal to move desc_ ownership.
+    error_ = handler_->ApplyRemoteDescription(std::move(desc_),
+                                              bundle_groups_by_mid());
+    // `desc` may be destroyed at this point.
+
+    if (!error_.ok()) {
+      // If ApplyRemoteDescription fails, the PeerConnection could be in an
+      // inconsistent state, so act conservatively here and set the session
+      // error so that future calls to SetLocalDescription/SetRemoteDescription
+      // fail.
+      handler_->SetSessionError(SessionError::kContent, error_.message());
+      std::string error_message =
+          GetSetDescriptionErrorMessage(cricket::CS_REMOTE, type_, error_);
+      RTC_LOG(LS_ERROR) << error_message;
+      error_.set_message(std::move(error_message));
+      return false;
+    }
+
+    return true;
+  }
+
+  // Convenience getter for desc_->GetType().
+  SdpType type() const { return type_; }
+
+  cricket::SessionDescription* description() { return desc_->description(); }
+
+  // Returns a reference to a cached map of bundle groups ordered by mid.
+  // Note that this will only be valid after a successful call to
+  // `IsDescriptionValid`.
+  const std::map<std::string, const cricket::ContentGroup*>&
+  bundle_groups_by_mid() const {
+    RTC_DCHECK(ok());
+    return bundle_groups_by_mid_;
+  }
+
+ private:
+  // Convenience methods for populating the embedded `error_` object.
+  void Unsupported(std::string message) {
+    SetError(RTCErrorType::UNSUPPORTED_OPERATION, std::move(message));
+  }
+
+  void InvalidParam(std::string message) {
+    SetError(RTCErrorType::INVALID_PARAMETER, std::move(message));
+  }
+
+  void SetError(RTCErrorType type, std::string message) {
+    RTC_DCHECK(ok()) << "Overwriting an existing error?";
+    error_ = RTCError(type, std::move(message));
+  }
+
+  SdpOfferAnswerHandler* const handler_;
+  std::unique_ptr<SessionDescriptionInterface> desc_;
+  rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer_;
+  std::function<void()> operations_chain_callback_;
+  RTCError error_ = RTCError::OK();
+  std::map<std::string, const cricket::ContentGroup*> bundle_groups_by_mid_;
+  SdpType type_;
+};
 // Used by parameterless SetLocalDescription() to create an offer or answer.
 // Upon completion of creating the session description, SetLocalDescription() is
 // invoked with the result.
@@ -1501,15 +1671,11 @@
         // SetSessionDescriptionObserverAdapter takes care of making sure the
         // `observer_refptr` is invoked in a posted message.
         this_weak_ptr->DoSetRemoteDescription(
-            std::move(desc),
-            rtc::make_ref_counted<SetSessionDescriptionObserverAdapter>(
-                this_weak_ptr, observer_refptr));
-        // For backwards-compatability reasons, we declare the operation as
-        // completed here (rather than in a post), so that the operation chain
-        // is not blocked by this operation when the observer is invoked. This
-        // allows the observer to trigger subsequent offer/answer operations
-        // synchronously if the operation chain is now empty.
-        operations_chain_callback();
+            std::make_unique<RemoteDescriptionOperation>(
+                this_weak_ptr.get(), std::move(desc),
+                rtc::make_ref_counted<SetSessionDescriptionObserverAdapter>(
+                    this_weak_ptr, observer_refptr),
+                std::move(operations_chain_callback)));
       });
 }
 
@@ -1524,6 +1690,12 @@
       [this_weak_ptr = weak_ptr_factory_.GetWeakPtr(), observer,
        desc = std::move(desc)](
           std::function<void()> operations_chain_callback) mutable {
+        if (!observer) {
+          RTC_DLOG(LS_ERROR) << "SetRemoteDescription - observer is NULL.";
+          operations_chain_callback();
+          return;
+        }
+
         // Abort early if `this_weak_ptr` is no longer valid.
         if (!this_weak_ptr) {
           observer->OnSetRemoteDescriptionComplete(RTCError(
@@ -1532,12 +1704,11 @@
           operations_chain_callback();
           return;
         }
-        this_weak_ptr->DoSetRemoteDescription(std::move(desc),
-                                              std::move(observer));
-        // DoSetRemoteDescription() is implemented as a synchronous operation.
-        // The `observer` will already have been informed that it completed, and
-        // we can mark this operation as complete without any loose ends.
-        operations_chain_callback();
+
+        this_weak_ptr->DoSetRemoteDescription(
+            std::make_unique<RemoteDescriptionOperation>(
+                this_weak_ptr.get(), std::move(desc), std::move(observer),
+                std::move(operations_chain_callback)));
       });
 }
 
@@ -1702,10 +1873,13 @@
         GetFirstVideoContent(remote_description()->description()), video_desc);
   }
 
-  if (type == SdpType::kAnswer &&
-      local_ice_credentials_to_replace_->SatisfiesIceRestart(
-          *current_local_description_)) {
-    local_ice_credentials_to_replace_->ClearIceCredentials();
+  if (type == SdpType::kAnswer) {
+    if (local_ice_credentials_to_replace_->SatisfiesIceRestart(
+            *current_local_description_)) {
+      local_ice_credentials_to_replace_->ClearIceCredentials();
+    }
+
+    RemoveStoppedTransceivers();
   }
 
   return RTCError::OK();
@@ -2131,96 +2305,41 @@
 }
 
 void SdpOfferAnswerHandler::DoSetRemoteDescription(
-    std::unique_ptr<SessionDescriptionInterface> desc,
-    rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer) {
+    std::unique_ptr<RemoteDescriptionOperation> operation) {
   RTC_DCHECK_RUN_ON(signaling_thread());
   TRACE_EVENT0("webrtc", "SdpOfferAnswerHandler::DoSetRemoteDescription");
 
-  if (!observer) {
-    RTC_LOG(LS_ERROR) << "SetRemoteDescription - observer is NULL.";
+  if (!operation->ok())
     return;
-  }
 
-  if (!desc) {
-    observer->OnSetRemoteDescriptionComplete(RTCError(
-        RTCErrorType::INVALID_PARAMETER, "SessionDescription is NULL."));
+  if (operation->HaveSessionError())
     return;
-  }
 
-  // If a session error has occurred the PeerConnection is in a possibly
-  // inconsistent state so fail right away.
-  if (session_error() != SessionError::kNone) {
-    std::string error_message = GetSessionErrorMsg();
-    RTC_LOG(LS_ERROR) << "SetRemoteDescription: " << error_message;
-    observer->OnSetRemoteDescriptionComplete(
-        RTCError(RTCErrorType::INTERNAL_ERROR, std::move(error_message)));
+  if (operation->MaybeRollback())
     return;
-  }
-  if (IsUnifiedPlan()) {
-    if (pc_->configuration()->enable_implicit_rollback) {
-      if (desc->GetType() == SdpType::kOffer &&
-          signaling_state() == PeerConnectionInterface::kHaveLocalOffer) {
-        Rollback(desc->GetType());
-      }
-    }
-    // Explicit rollback.
-    if (desc->GetType() == SdpType::kRollback) {
-      observer->OnSetRemoteDescriptionComplete(Rollback(desc->GetType()));
-      return;
-    }
-  } else if (desc->GetType() == SdpType::kRollback) {
-    observer->OnSetRemoteDescriptionComplete(
-        RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
-                 "Rollback not supported in Plan B"));
+
+  operation->ReportOfferAnswerUma();
+
+  // Handle remote descriptions missing a=mid lines for interop with legacy
+  // end points.
+  FillInMissingRemoteMids(operation->description());
+  if (!operation->IsDescriptionValid())
     return;
-  }
-  if (desc->GetType() == SdpType::kOffer ||
-      desc->GetType() == SdpType::kAnswer) {
-    // Report to UMA the format of the received offer or answer.
-    pc_->ReportSdpFormatReceived(*desc);
-    pc_->ReportSdpBundleUsage(*desc);
-  }
 
-  // Handle remote descriptions missing a=mid lines for interop with legacy end
-  // points.
-  FillInMissingRemoteMids(desc->description());
-
-  std::map<std::string, const cricket::ContentGroup*> bundle_groups_by_mid =
-      GetBundleGroupsByMid(desc->description());
-  RTCError error = ValidateSessionDescription(desc.get(), cricket::CS_REMOTE,
-                                              bundle_groups_by_mid);
-  if (!error.ok()) {
-    std::string error_message = GetSetDescriptionErrorMessage(
-        cricket::CS_REMOTE, desc->GetType(), error);
-    RTC_LOG(LS_ERROR) << error_message;
-    observer->OnSetRemoteDescriptionComplete(
-        RTCError(error.type(), std::move(error_message)));
+  if (!operation->ApplyRemoteDescription())
     return;
-  }
 
-  // Grab the description type before moving ownership to
-  // ApplyRemoteDescription, which may destroy it before returning.
-  const SdpType type = desc->GetType();
+  // Consider the operation complete at this point.
+  operation->SignalCompletion();
 
-  error = ApplyRemoteDescription(std::move(desc), bundle_groups_by_mid);
-  // `desc` may be destroyed at this point.
+  SetRemoteDescriptionPostProcess(operation->type() == SdpType::kAnswer);
+}
 
-  if (!error.ok()) {
-    // If ApplyRemoteDescription fails, the PeerConnection could be in an
-    // inconsistent state, so act conservatively here and set the session error
-    // so that future calls to SetLocalDescription/SetRemoteDescription fail.
-    SetSessionError(SessionError::kContent, error.message());
-    std::string error_message =
-        GetSetDescriptionErrorMessage(cricket::CS_REMOTE, type, error);
-    RTC_LOG(LS_ERROR) << error_message;
-    observer->OnSetRemoteDescriptionComplete(
-        RTCError(error.type(), std::move(error_message)));
-    return;
-  }
+// Called after a DoSetRemoteDescription operation completes.
+void SdpOfferAnswerHandler::SetRemoteDescriptionPostProcess(bool was_answer) {
   RTC_DCHECK(remote_description());
 
-  if (type == SdpType::kAnswer) {
-    RemoveStoppedTransceivers();
+  if (was_answer) {
     // TODO(deadbeef): We already had to hop to the network thread for
     // MaybeStartGathering...
     pc_->network_thread()->Invoke<void>(
@@ -2229,12 +2348,11 @@
     ReportNegotiatedSdpSemantics(*remote_description());
   }
 
-  observer->OnSetRemoteDescriptionComplete(RTCError::OK());
   pc_->NoteUsageEvent(UsageEvent::SET_REMOTE_DESCRIPTION_SUCCEEDED);
 
   // Check if negotiation is needed. We must do this after informing the
-  // observer that SetRemoteDescription() has completed to ensure negotiation is
-  // not needed prior to the promise resolving.
+  // observer that SetRemoteDescription() has completed to ensure negotiation
+  // is not needed prior to the promise resolving.
   if (IsUnifiedPlan()) {
     bool was_negotiation_needed = is_negotiation_needed_;
     UpdateNegotiationNeeded();
@@ -2279,10 +2397,10 @@
   }
   std::vector<rtc::scoped_refptr<MediaStreamInterface>> previous_streams =
       receiver->streams();
-  // SetStreams() will add/remove the receiver's track to/from the streams. This
-  // differs from the spec - the spec uses an "addList" and "removeList" to
-  // update the stream-track relationships in a later step. We do this earlier,
-  // changing the order of things, but the end-result is the same.
+  // SetStreams() will add/remove the receiver's track to/from the streams.
+  // This differs from the spec - the spec uses an "addList" and "removeList"
+  // to update the stream-track relationships in a later step. We do this
+  // earlier, changing the order of things, but the end-result is the same.
   // TODO(hbos): When we remove remote_streams(), use set_stream_ids()
   // instead. https://crbug.com/webrtc/9480
   receiver->SetStreams(media_streams);
@@ -2293,8 +2411,8 @@
     const IceCandidateInterface* ice_candidate) {
   const AddIceCandidateResult result = AddIceCandidateInternal(ice_candidate);
   NoteAddIceCandidateResult(result);
-  // If the return value is kAddIceCandidateFailNotReady, the candidate has been
-  // added, although not 'ready', but that's a success.
+  // If the return value is kAddIceCandidateFailNotReady, the candidate has
+  // been added, although not 'ready', but that's a success.
   return result == kAddIceCandidateSuccess ||
          result == kAddIceCandidateFailNotReady;
 }
@@ -2350,9 +2468,9 @@
     std::function<void(RTCError)> callback) {
   TRACE_EVENT0("webrtc", "SdpOfferAnswerHandler::AddIceCandidate");
   RTC_DCHECK_RUN_ON(signaling_thread());
-  // Chain this operation. If asynchronous operations are pending on the chain,
-  // this operation will be queued to be invoked, otherwise the contents of the
-  // lambda will execute immediately.
+  // Chain this operation. If asynchronous operations are pending on the
+  // chain, this operation will be queued to be invoked, otherwise the
+  // contents of the lambda will execute immediately.
   operations_chain_->ChainOperation(
       [this_weak_ptr = weak_ptr_factory_.GetWeakPtr(),
        candidate = std::move(candidate), callback = std::move(callback)](
@@ -2501,8 +2619,8 @@
         bundle_groups_by_mid) {
   RTC_DCHECK_RUN_ON(signaling_thread());
 
-  // If there's already a pending error then no state transition should happen.
-  // But all call-sites should be verifying this before calling us!
+  // If there's already a pending error then no state transition should
+  // happen. But all call-sites should be verifying this before calling us!
   RTC_DCHECK(session_error() == SessionError::kNone);
 
   // If this is answer-ish we're ready to let media flow.
@@ -2548,10 +2666,10 @@
   // one obsolete.
   if (!operations_chain_->IsEmpty()) {
     // Since we just suppressed an event that would have been fired, if
-    // negotiation is still needed by the time the chain becomes empty again, we
-    // must make sure to generate another event if negotiation is needed then.
-    // This happens when `is_negotiation_needed_` goes from false to true, so we
-    // set it to false until UpdateNegotiationNeeded() is called.
+    // negotiation is still needed by the time the chain becomes empty again,
+    // we must make sure to generate another event if negotiation is needed
+    // then. This happens when `is_negotiation_needed_` goes from false to
+    // true, so we set it to false until UpdateNegotiationNeeded() is called.
     is_negotiation_needed_ = false;
     update_negotiation_needed_on_empty_chain_ = true;
     return false;
@@ -2777,8 +2895,8 @@
     pc_->Observer()->OnRemoveStream(stream);
   }
 
-  // The assumption is that in case of implicit rollback UpdateNegotiationNeeded
-  // gets called in SetRemoteDescription.
+  // The assumption is that in case of implicit rollback
+  // UpdateNegotiationNeeded gets called in SetRemoteDescription.
   if (desc_type == SdpType::kRollback) {
     UpdateNegotiationNeeded();
     if (is_negotiation_needed_) {
@@ -2800,9 +2918,9 @@
   if (pc_->IsClosed() || !update_negotiation_needed_on_empty_chain_)
     return;
   update_negotiation_needed_on_empty_chain_ = false;
-  // Firing when chain is empty is only supported in Unified Plan to avoid Plan
-  // B regressions. (In Plan B, onnegotiationneeded is already broken anyway, so
-  // firing it even more might just be confusing.)
+  // Firing when chain is empty is only supported in Unified Plan to avoid
+  // Plan B regressions. (In Plan B, onnegotiationneeded is already broken
+  // anyway, so firing it even more might just be confusing.)
   if (IsUnifiedPlan()) {
     UpdateNegotiationNeeded();
   }
@@ -2844,8 +2962,8 @@
   }
 
   // In the spec, a task is queued here to run the following steps - this is
-  // meant to ensure we do not fire onnegotiationneeded prematurely if multiple
-  // changes are being made at once. In order to support Chromium's
+  // meant to ensure we do not fire onnegotiationneeded prematurely if
+  // multiple changes are being made at once. In order to support Chromium's
   // implementation where the JavaScript representation of the PeerConnection
   // lives on a separate thread though, the queuing of a task is instead
   // performed by the PeerConnectionObserver posting from the signaling thread
@@ -2868,8 +2986,8 @@
   // "stable", as part of the steps for setting an RTCSessionDescription.
 
   // If the result of checking if negotiation is needed is false, clear the
-  // negotiation-needed flag by setting connection's [[NegotiationNeeded]] slot
-  // to false, and abort these steps.
+  // negotiation-needed flag by setting connection's [[NegotiationNeeded]]
+  // slot to false, and abort these steps.
   bool is_negotiation_needed = CheckIfNegotiationIsNeeded();
   if (!is_negotiation_needed) {
     is_negotiation_needed_ = false;
@@ -2892,16 +3010,16 @@
   // If connection's [[NegotiationNeeded]] slot is false, abort these steps.
   // Fire an event named negotiationneeded at connection.
   pc_->Observer()->OnRenegotiationNeeded();
-  // Fire the spec-compliant version; when ShouldFireNegotiationNeededEvent() is
-  // used in the task queued by the observer, this event will only fire when the
-  // chain is empty.
+  // Fire the spec-compliant version; when ShouldFireNegotiationNeededEvent()
+  // is used in the task queued by the observer, this event will only fire
+  // when the chain is empty.
   GenerateNegotiationNeededEvent();
 }
 
 bool SdpOfferAnswerHandler::CheckIfNegotiationIsNeeded() {
   RTC_DCHECK_RUN_ON(signaling_thread());
-  // 1. If any implementation-specific negotiation is required, as described at
-  // the start of this section, return true.
+  // 1. If any implementation-specific negotiation is required, as described
+  // at the start of this section, return true.
 
   // 2. If connection.[[LocalIceCredentialsToReplace]] is not empty, return
   // true.
@@ -3048,9 +3166,8 @@
     cricket::ContentSource source,
     const std::map<std::string, const cricket::ContentGroup*>&
         bundle_groups_by_mid) {
-  if (session_error() != SessionError::kNone) {
-    LOG_AND_RETURN_ERROR(RTCErrorType::INTERNAL_ERROR, GetSessionErrorMsg());
-  }
+  // An assumption is that a check for session error is done at a higher level.
+  RTC_DCHECK_EQ(SessionError::kNone, session_error());
 
   if (!sdesc || !sdesc->description()) {
     LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER, kInvalidSdp);
@@ -3099,8 +3216,8 @@
 
   // Verify m-lines in Answer when compared against Offer.
   if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
-    // With an answer we want to compare the new answer session description with
-    // the offer's session description from the current negotiation.
+    // With an answer we want to compare the new answer session description
+    // with the offer's session description from the current negotiation.
     const cricket::SessionDescription* offer_desc =
         (source == cricket::CS_LOCAL) ? remote_description()->description()
                                       : local_description()->description();
@@ -3113,11 +3230,12 @@
   } else {
     // The re-offers should respect the order of m= sections in current
     // description. See RFC3264 Section 8 paragraph 4 for more details.
-    // With a re-offer, either the current local or current remote descriptions
-    // could be the most up to date, so we would like to check against both of
-    // them if they exist. It could be the case that one of them has a 0 port
-    // for a media section, but the other does not. This is important to check
-    // against in the case that we are recycling an m= section.
+    // With a re-offer, either the current local or current remote
+    // descriptions could be the most up to date, so we would like to check
+    // against both of them if they exist. It could be the case that one of
+    // them has a 0 port for a media section, but the other does not. This is
+    // important to check against in the case that we are recycling an m=
+    // section.
     const cricket::SessionDescription* current_desc = nullptr;
     const cricket::SessionDescription* secondary_current_desc = nullptr;
     if (local_description()) {
@@ -3172,8 +3290,9 @@
 
   if (new_session.GetType() == SdpType::kOffer) {
     // If the BUNDLE policy is max-bundle, then we know for sure that all
-    // transports will be bundled from the start. Return an error if max-bundle
-    // is specified but the session description does not have a BUNDLE group.
+    // transports will be bundled from the start. Return an error if
+    // max-bundle is specified but the session description does not have a
+    // BUNDLE group.
     if (pc_->configuration()->bundle_policy ==
             PeerConnectionInterface::kBundlePolicyMaxBundle &&
         bundle_groups_by_mid.empty()) {
diff --git a/pc/sdp_offer_answer.h b/pc/sdp_offer_answer.h
index 99af95b..3318a4f 100644
--- a/pc/sdp_offer_answer.h
+++ b/pc/sdp_offer_answer.h
@@ -182,6 +182,7 @@
   rtc::scoped_refptr<StreamCollectionInterface> remote_streams();
 
  private:
+  class RemoteDescriptionOperation;
   class ImplicitCreateSessionDescriptionObserver;
 
   friend class ImplicitCreateSessionDescriptionObserver;
@@ -259,8 +260,11 @@
       std::unique_ptr<SessionDescriptionInterface> desc,
       rtc::scoped_refptr<SetLocalDescriptionObserverInterface> observer);
   void DoSetRemoteDescription(
-      std::unique_ptr<SessionDescriptionInterface> desc,
-      rtc::scoped_refptr<SetRemoteDescriptionObserverInterface> observer);
+      std::unique_ptr<RemoteDescriptionOperation> operation);
+
+  // Called after a DoSetRemoteDescription operation completes.
+  void SetRemoteDescriptionPostProcess(bool was_answer)
+      RTC_RUN_ON(signaling_thread());
 
   // Update the state, signaling if necessary.
   void ChangeSignalingState(