Add support for JSEP offer/answer with transceivers

This change adds support to PeerConnection's CreateOffer/
CreateAnswer/SetLocalDescription/SetRemoteDescription for
Unified Plan SDP mapping to/from RtpTransceivers. This behavior
is enabled using the kUnifiedPlan SDP semantics in the
PeerConnection configuration.

Bug: webrtc:7600
Change-Id: I4b44f5d3690887d387bf9c47eac00db8ec974571
Reviewed-on: https://webrtc-review.googlesource.com/28341
Commit-Queue: Steve Anton <steveanton@webrtc.org>
Reviewed-by: Peter Thatcher <pthatcher@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#21442}
diff --git a/pc/peerconnection.cc b/pc/peerconnection.cc
index 5a21146..b99df99 100644
--- a/pc/peerconnection.cc
+++ b/pc/peerconnection.cc
@@ -11,6 +11,7 @@
 #include "pc/peerconnection.h"
 
 #include <algorithm>
+#include <queue>
 #include <set>
 #include <utility>
 #include <vector>
@@ -330,6 +331,11 @@
   }
 
   for (size_t i = 0; i < existing_desc->contents().size(); ++i) {
+    if (existing_desc->contents()[i].rejected) {
+      // If the media section can be recycled, it's valid for the MID and media
+      // type to change.
+      continue;
+    }
     if (new_desc->contents()[i].name != existing_desc->contents()[i].name) {
       return false;
     }
@@ -1637,34 +1643,69 @@
     }
   }
 
+  // Take a reference to the old local description since it's used below to
+  // compare against the new local description. When setting the new local
+  // description, grab ownership of the replaced session description in case it
+  // is the same as |old_local_description|, to keep it alive for the duration
+  // of the method.
+  const SessionDescriptionInterface* old_local_description =
+      local_description();
+  std::unique_ptr<SessionDescriptionInterface> replaced_local_description;
   if (type == SdpType::kAnswer) {
+    replaced_local_description = pending_local_description_
+                                     ? std::move(pending_local_description_)
+                                     : std::move(current_local_description_);
     current_local_description_ = std::move(desc);
     pending_local_description_ = nullptr;
     current_remote_description_ = std::move(pending_remote_description_);
   } else {
+    replaced_local_description = std::move(pending_local_description_);
     pending_local_description_ = std::move(desc);
   }
   // The session description to apply now must be accessed by
   // |local_description()|.
   RTC_DCHECK(local_description());
 
-  // Transport and Media channels will be created only when offer is set.
-  if (type == SdpType::kOffer) {
-    // TODO(mallinath) - Handle CreateChannel failure, as new local description
-    // is applied. Restore back to old description.
-    RTCError error = CreateChannels(local_description()->description());
+  if (IsUnifiedPlan()) {
+    RTCError error = UpdateTransceiversAndDataChannels(
+        cricket::CS_LOCAL, old_local_description, *local_description());
     if (!error.ok()) {
       return error;
     }
-  }
+    for (auto transceiver : transceivers_) {
+      const ContentInfo* content =
+          FindMediaSectionForTransceiver(transceiver, local_description());
+      if (!content) {
+        continue;
+      }
+      const MediaContentDescription* media_desc = content->media_description();
+      if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
+        transceiver->internal()->set_current_direction(media_desc->direction());
+      }
+      if (content->rejected && !transceiver->stopped()) {
+        transceiver->Stop();
+      }
+    }
+  } else {
+    // Transport and Media channels will be created only when offer is set.
+    if (type == SdpType::kOffer) {
+      // TODO(bugs.webrtc.org/4676) - Handle CreateChannel failure, as new local
+      // description is applied. Restore back to old description.
+      RTCError error = CreateChannels(*local_description()->description());
+      if (!error.ok()) {
+        return error;
+      }
+    }
 
-  // Remove unused channels if MediaContentDescription is rejected.
-  RemoveUnusedChannels(local_description()->description());
+    // Remove unused channels if MediaContentDescription is rejected.
+    RemoveUnusedChannels(local_description()->description());
+  }
 
   error = UpdateSessionState(type, cricket::CS_LOCAL);
   if (!error.ok()) {
     return error;
   }
+
   if (remote_description()) {
     // Now that we have a local description, we can push down remote candidates.
     UseCandidatesInSessionDescription(remote_description());
@@ -1682,29 +1723,31 @@
     AllocateSctpSids(role);
   }
 
-  // Update state and SSRC of local MediaStreams and DataChannels based on the
-  // local session description.
-  const cricket::ContentInfo* audio_content =
-      GetFirstAudioContent(local_description()->description());
-  if (audio_content) {
-    if (audio_content->rejected) {
-      RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
-    } else {
-      const cricket::AudioContentDescription* audio_desc =
-          audio_content->media_description()->as_audio();
-      UpdateLocalSenders(audio_desc->streams(), audio_desc->type());
+  if (!IsUnifiedPlan()) {
+    // Update state and SSRC of local MediaStreams and DataChannels based on the
+    // local session description.
+    const cricket::ContentInfo* audio_content =
+        GetFirstAudioContent(local_description()->description());
+    if (audio_content) {
+      if (audio_content->rejected) {
+        RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
+      } else {
+        const cricket::AudioContentDescription* audio_desc =
+            audio_content->media_description()->as_audio();
+        UpdateLocalSenders(audio_desc->streams(), audio_desc->type());
+      }
     }
-  }
 
-  const cricket::ContentInfo* video_content =
-      GetFirstVideoContent(local_description()->description());
-  if (video_content) {
-    if (video_content->rejected) {
-      RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
-    } else {
-      const cricket::VideoContentDescription* video_desc =
-          video_content->media_description()->as_video();
-      UpdateLocalSenders(video_desc->streams(), video_desc->type());
+    const cricket::ContentInfo* video_content =
+        GetFirstVideoContent(local_description()->description());
+    if (video_content) {
+      if (video_content->rejected) {
+        RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
+      } else {
+        const cricket::VideoContentDescription* video_desc =
+            video_content->media_description()->as_video();
+        UpdateLocalSenders(video_desc->streams(), video_desc->type());
+      }
     }
   }
 
@@ -1787,13 +1830,14 @@
   // Update stats here so that we have the most recent stats for tracks and
   // streams that might be removed by updating the session description.
   stats_->UpdateStats(kStatsOutputLevelStandard);
-  // Takes the ownership of |desc|. On success, remote_description() is updated
-  // to reflect the description that was passed in.
 
+  // Take a reference to the old remote description since it's used below to
+  // compare against the new remote description. When setting the new remote
+  // description, grab ownership of the replaced session description in case it
+  // is the same as |old_remote_description|, to keep it alive for the duration
+  // of the method.
   const SessionDescriptionInterface* old_remote_description =
       remote_description();
-  // Grab ownership of the description being replaced for the remainder of this
-  // method, since it's used below as |old_remote_description|.
   std::unique_ptr<SessionDescriptionInterface> replaced_remote_description;
   SdpType type = desc->GetType();
   if (type == SdpType::kAnswer) {
@@ -1812,17 +1856,25 @@
   RTC_DCHECK(remote_description());
 
   // Transport and Media channels will be created only when offer is set.
-  if (type == SdpType::kOffer) {
-    // TODO(mallinath) - Handle CreateChannel failure, as new local description
-    // is applied. Restore back to old description.
-    RTCError error = CreateChannels(remote_description()->description());
+  if (IsUnifiedPlan()) {
+    RTCError error = UpdateTransceiversAndDataChannels(
+        cricket::CS_REMOTE, old_remote_description, *remote_description());
     if (!error.ok()) {
       return error;
     }
-  }
+  } else {
+    if (type == SdpType::kOffer) {
+      // TODO(bugs.webrtc.org/4676) - Handle CreateChannel failure, as new local
+      // description is applied. Restore back to old description.
+      RTCError error = CreateChannels(*remote_description()->description());
+      if (!error.ok()) {
+        return error;
+      }
+    }
 
-  // Remove unused channels if MediaContentDescription is rejected.
-  RemoveUnusedChannels(remote_description()->description());
+    // Remove unused channels if MediaContentDescription is rejected.
+    RemoveUnusedChannels(remote_description()->description());
+  }
 
   // NOTE: Candidates allocation will be initiated only when SetLocalDescription
   // is called.
@@ -1887,6 +1939,43 @@
     AllocateSctpSids(role);
   }
 
+  if (IsUnifiedPlan()) {
+    for (auto transceiver : transceivers_) {
+      const ContentInfo* content =
+          FindMediaSectionForTransceiver(transceiver, remote_description());
+      if (!content) {
+        continue;
+      }
+      const MediaContentDescription* media_desc = content->media_description();
+      RtpTransceiverDirection local_direction =
+          RtpTransceiverDirectionReversed(media_desc->direction());
+      // From the WebRTC specification, steps 2.2.8.5/6 of section 4.4.1.6 "Set
+      // the RTCSessionDescription: If direction is sendrecv or recvonly, and
+      // transceiver's current direction is neither sendrecv nor recvonly,
+      // process the addition of a remote track for the media description.
+      if (RtpTransceiverDirectionHasRecv(local_direction) &&
+          (!transceiver->current_direction() ||
+           !RtpTransceiverDirectionHasRecv(
+               *transceiver->current_direction()))) {
+        // TODO(bugs.webrtc.org/7600): Process the addition of a remote track.
+      }
+      // If direction is sendonly or inactive, and transceiver's current
+      // direction is neither sendonly nor inactive, process the removal of a
+      // remote track for the media description.
+      if (!RtpTransceiverDirectionHasRecv(local_direction) &&
+          (!transceiver->current_direction() ||
+           RtpTransceiverDirectionHasRecv(*transceiver->current_direction()))) {
+        // TODO(bugs.webrtc.org/7600): Process the removal of a remote track.
+      }
+      if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
+        transceiver->internal()->set_current_direction(local_direction);
+      }
+      if (content->rejected && !transceiver->stopped()) {
+        transceiver->Stop();
+      }
+    }
+  }
+
   const cricket::ContentInfo* audio_content =
       GetFirstAudioContent(remote_description()->description());
   const cricket::ContentInfo* video_content =
@@ -1910,64 +1999,256 @@
   // since only at that point will new streams have all their tracks.
   rtc::scoped_refptr<StreamCollection> new_streams(StreamCollection::Create());
 
-  // TODO(steveanton): When removing RTP senders/receivers in response to a
-  // rejected media section, there is some cleanup logic that expects the voice/
-  // video channel to still be set. But in this method the voice/video channel
-  // would have been destroyed by the SetRemoteDescription caller above so the
-  // cleanup that relies on them fails to run. The RemoveSenders calls should be
-  // moved to right before the DestroyChannel calls to fix this.
+  if (!IsUnifiedPlan()) {
+    // TODO(steveanton): When removing RTP senders/receivers in response to a
+    // rejected media section, there is some cleanup logic that expects the
+    // voice/ video channel to still be set. But in this method the voice/video
+    // channel would have been destroyed by the SetRemoteDescription caller
+    // above so the cleanup that relies on them fails to run. The RemoveSenders
+    // calls should be moved to right before the DestroyChannel calls to fix
+    // this.
 
-  // Find all audio rtp streams and create corresponding remote AudioTracks
-  // and MediaStreams.
-  if (audio_content) {
-    if (audio_content->rejected) {
-      RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
-    } else {
-      bool default_audio_track_needed =
-          !remote_peer_supports_msid_ &&
-          RtpTransceiverDirectionHasSend(audio_desc->direction());
-      UpdateRemoteSendersList(GetActiveStreams(audio_desc),
-                              default_audio_track_needed, audio_desc->type(),
-                              new_streams);
+    // Find all audio rtp streams and create corresponding remote AudioTracks
+    // and MediaStreams.
+    if (audio_content) {
+      if (audio_content->rejected) {
+        RemoveSenders(cricket::MEDIA_TYPE_AUDIO);
+      } else {
+        bool default_audio_track_needed =
+            !remote_peer_supports_msid_ &&
+            RtpTransceiverDirectionHasSend(audio_desc->direction());
+        UpdateRemoteSendersList(GetActiveStreams(audio_desc),
+                                default_audio_track_needed, audio_desc->type(),
+                                new_streams);
+      }
     }
-  }
 
-  // Find all video rtp streams and create corresponding remote VideoTracks
-  // and MediaStreams.
-  if (video_content) {
-    if (video_content->rejected) {
-      RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
-    } else {
-      bool default_video_track_needed =
-          !remote_peer_supports_msid_ &&
-          RtpTransceiverDirectionHasSend(video_desc->direction());
-      UpdateRemoteSendersList(GetActiveStreams(video_desc),
-                              default_video_track_needed, video_desc->type(),
-                              new_streams);
+    // Find all video rtp streams and create corresponding remote VideoTracks
+    // and MediaStreams.
+    if (video_content) {
+      if (video_content->rejected) {
+        RemoveSenders(cricket::MEDIA_TYPE_VIDEO);
+      } else {
+        bool default_video_track_needed =
+            !remote_peer_supports_msid_ &&
+            RtpTransceiverDirectionHasSend(video_desc->direction());
+        UpdateRemoteSendersList(GetActiveStreams(video_desc),
+                                default_video_track_needed, video_desc->type(),
+                                new_streams);
+      }
     }
-  }
 
-  // Update the DataChannels with the information from the remote peer.
-  if (data_desc) {
-    if (rtc::starts_with(data_desc->protocol().data(),
-                         cricket::kMediaProtocolRtpPrefix)) {
-      UpdateRemoteRtpDataChannels(GetActiveStreams(data_desc));
+    // Update the DataChannels with the information from the remote peer.
+    if (data_desc) {
+      if (rtc::starts_with(data_desc->protocol().data(),
+                           cricket::kMediaProtocolRtpPrefix)) {
+        UpdateRemoteRtpDataChannels(GetActiveStreams(data_desc));
+      }
     }
-  }
 
-  // Iterate new_streams and notify the observer about new MediaStreams.
-  for (size_t i = 0; i < new_streams->count(); ++i) {
-    MediaStreamInterface* new_stream = new_streams->at(i);
-    stats_->AddStream(new_stream);
-    observer_->OnAddStream(
-        rtc::scoped_refptr<MediaStreamInterface>(new_stream));
-  }
+    // Iterate new_streams and notify the observer about new MediaStreams.
+    for (size_t i = 0; i < new_streams->count(); ++i) {
+      MediaStreamInterface* new_stream = new_streams->at(i);
+      stats_->AddStream(new_stream);
+      observer_->OnAddStream(
+          rtc::scoped_refptr<MediaStreamInterface>(new_stream));
+    }
 
-  UpdateEndedRemoteMediaStreams();
+    UpdateEndedRemoteMediaStreams();
+  }
 
   return RTCError::OK();
 }
 
+RTCError PeerConnection::UpdateTransceiversAndDataChannels(
+    cricket::ContentSource source,
+    const SessionDescriptionInterface* old_session,
+    const SessionDescriptionInterface& new_session) {
+  RTC_DCHECK(IsUnifiedPlan());
+
+  auto bundle_group_or_error = GetEarlyBundleGroup(*new_session.description());
+  if (!bundle_group_or_error.ok()) {
+    return bundle_group_or_error.MoveError();
+  }
+  const cricket::ContentGroup* bundle_group = bundle_group_or_error.MoveValue();
+
+  const ContentInfos& old_contents =
+      (old_session ? old_session->description()->contents() : ContentInfos());
+  const ContentInfos& new_contents = new_session.description()->contents();
+
+  for (size_t i = 0; i < new_contents.size(); ++i) {
+    const cricket::ContentInfo& new_content = new_contents[i];
+    const cricket::ContentInfo* old_content =
+        (i < old_contents.size() ? &old_contents[i] : nullptr);
+    cricket::MediaType media_type = new_content.media_description()->type();
+    seen_mids_.insert(new_content.name);
+    if (media_type == cricket::MEDIA_TYPE_AUDIO ||
+        media_type == cricket::MEDIA_TYPE_VIDEO) {
+      auto transceiver_or_error =
+          AssociateTransceiver(source, i, new_content, old_content);
+      if (!transceiver_or_error.ok()) {
+        return transceiver_or_error.MoveError();
+      }
+      auto transceiver = transceiver_or_error.MoveValue();
+      if (source == cricket::CS_LOCAL && transceiver->stopped()) {
+        continue;
+      }
+      RTCError error =
+          UpdateTransceiverChannel(transceiver, new_content, bundle_group);
+      if (!error.ok()) {
+        return error;
+      }
+    } else if (media_type == cricket::MEDIA_TYPE_DATA) {
+      // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+      // Plan.
+    } else {
+      LOG_AND_RETURN_ERROR(RTCErrorType::INTERNAL_ERROR,
+                           "Unknown section type.");
+    }
+  }
+
+  return RTCError::OK();
+}
+
+RTCError PeerConnection::UpdateTransceiverChannel(
+    rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+        transceiver,
+    const cricket::ContentInfo& content,
+    const cricket::ContentGroup* bundle_group) {
+  RTC_DCHECK(IsUnifiedPlan());
+  RTC_DCHECK(transceiver);
+  cricket::BaseChannel* channel = transceiver->internal()->channel();
+  if (content.rejected) {
+    if (channel) {
+      transceiver->internal()->SetChannel(nullptr);
+      DestroyBaseChannel(channel);
+    }
+  } else {
+    if (!channel) {
+      if (transceiver->internal()->media_type() == cricket::MEDIA_TYPE_AUDIO) {
+        channel = CreateVoiceChannel(
+            content.name,
+            GetTransportNameForMediaSection(content.name, bundle_group));
+      } else {
+        RTC_DCHECK_EQ(cricket::MEDIA_TYPE_VIDEO,
+                      transceiver->internal()->media_type());
+        channel = CreateVideoChannel(
+            content.name,
+            GetTransportNameForMediaSection(content.name, bundle_group));
+      }
+      if (!channel) {
+        LOG_AND_RETURN_ERROR(
+            RTCErrorType::INTERNAL_ERROR,
+            "Failed to create channel for mid=" + content.name);
+      }
+      transceiver->internal()->SetChannel(channel);
+    }
+  }
+  return RTCError::OK();
+}
+
+RTCErrorOr<rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+PeerConnection::AssociateTransceiver(cricket::ContentSource source,
+                                     size_t mline_index,
+                                     const ContentInfo& content,
+                                     const ContentInfo* old_content) {
+  RTC_DCHECK(IsUnifiedPlan());
+  // If the m= section is being recycled (rejected in previous remote
+  // description, not rejected in current description), dissociate the currently
+  // associated RtpTransceiver by setting its mid property to null, and discard
+  // the mapping between the transceiver and its m= section index.
+  if (old_content && old_content->rejected && !content.rejected) {
+    auto old_transceiver = GetAssociatedTransceiver(old_content->name);
+    if (old_transceiver) {
+      old_transceiver->internal()->set_mid(rtc::nullopt);
+      old_transceiver->internal()->set_mline_index(rtc::nullopt);
+    }
+  }
+  const MediaContentDescription* media_desc = content.media_description();
+  auto transceiver = GetAssociatedTransceiver(content.name);
+  if (source == cricket::CS_LOCAL) {
+    // Find the RtpTransceiver that corresponds to this m= section, using the
+    // mapping between transceivers and m= section indices established when
+    // creating the offer.
+    if (!transceiver) {
+      transceiver = GetTransceiverByMLineIndex(mline_index);
+    }
+    if (!transceiver) {
+      LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+                           "Unknown transceiver");
+    }
+  } else {
+    RTC_DCHECK_EQ(source, cricket::CS_REMOTE);
+    // If the m= section is sendrecv or recvonly, and there are RtpTransceivers
+    // of the same type...
+    if (!transceiver &&
+        RtpTransceiverDirectionHasRecv(media_desc->direction())) {
+      transceiver = FindAvailableTransceiverToReceive(media_desc->type());
+    }
+    // If no RtpTransceiver was found in the previous step, create one with a
+    // recvonly direction.
+    if (!transceiver) {
+      transceiver = CreateTransceiver(media_desc->type());
+      transceiver->internal()->set_direction(
+          RtpTransceiverDirection::kRecvOnly);
+    }
+  }
+  RTC_DCHECK(transceiver);
+  if (transceiver->internal()->media_type() != media_desc->type()) {
+    LOG_AND_RETURN_ERROR(
+        RTCErrorType::INVALID_PARAMETER,
+        "Transceiver type does not match media description type.");
+  }
+  // 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
+  // the m= section.
+  transceiver->internal()->set_mid(content.name);
+  transceiver->internal()->set_mline_index(mline_index);
+  return std::move(transceiver);
+}
+
+rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+PeerConnection::GetAssociatedTransceiver(const std::string& mid) const {
+  RTC_DCHECK(IsUnifiedPlan());
+  for (auto transceiver : transceivers_) {
+    if (transceiver->mid() == mid) {
+      return transceiver;
+    }
+  }
+  return nullptr;
+}
+
+rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+PeerConnection::GetTransceiverByMLineIndex(size_t mline_index) const {
+  RTC_DCHECK(IsUnifiedPlan());
+  for (auto transceiver : transceivers_) {
+    if (transceiver->internal()->mline_index() == mline_index) {
+      return transceiver;
+    }
+  }
+  return nullptr;
+}
+
+rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+PeerConnection::FindAvailableTransceiverToReceive(
+    cricket::MediaType media_type) const {
+  RTC_DCHECK(IsUnifiedPlan());
+  // From JSEP section 5.10 (Applying a Remote Description):
+  // If the m= section is sendrecv or recvonly, and there are RtpTransceivers of
+  // the same type that were added to the PeerConnection by addTrack and are not
+  // associated with any m= section and are not stopped, find the first such
+  // RtpTransceiver.
+  for (auto transceiver : transceivers_) {
+    if (transceiver->internal()->media_type() == media_type &&
+        transceiver->internal()->created_by_addtrack() && !transceiver->mid() &&
+        !transceiver->stopped()) {
+      return transceiver;
+    }
+  }
+  return nullptr;
+}
+
 const cricket::ContentInfo* PeerConnection::FindMediaSectionForTransceiver(
     rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
         transceiver,
@@ -2655,10 +2936,30 @@
 }
 
 void PeerConnection::GetOptionsForOffer(
-    const PeerConnectionInterface::RTCOfferAnswerOptions& rtc_options,
+    const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
     cricket::MediaSessionOptions* session_options) {
-  ExtractSharedMediaSessionOptions(rtc_options, session_options);
+  ExtractSharedMediaSessionOptions(offer_answer_options, session_options);
 
+  if (IsUnifiedPlan()) {
+    GetOptionsForUnifiedPlanOffer(offer_answer_options, session_options);
+  } else {
+    GetOptionsForPlanBOffer(offer_answer_options, session_options);
+  }
+
+  // Apply ICE restart flag and renomination flag.
+  for (auto& options : session_options->media_description_options) {
+    options.transport_options.ice_restart = offer_answer_options.ice_restart;
+    options.transport_options.enable_ice_renomination =
+        configuration_.enable_ice_renomination;
+  }
+
+  session_options->rtcp_cname = rtcp_cname_;
+  session_options->crypto_options = factory_->options().crypto_options;
+}
+
+void PeerConnection::GetOptionsForPlanBOffer(
+    const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
+    cricket::MediaSessionOptions* session_options) {
   // Figure out transceiver directional preferences.
   bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO);
   bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO);
@@ -2673,15 +2974,19 @@
   bool offer_new_data_description = HasDataChannels();
 
   // The "offer_to_receive_X" options allow those defaults to be overridden.
-  if (rtc_options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) {
-    recv_audio = (rtc_options.offer_to_receive_audio > 0);
+  if (offer_answer_options.offer_to_receive_audio !=
+      RTCOfferAnswerOptions::kUndefined) {
+    recv_audio = (offer_answer_options.offer_to_receive_audio > 0);
     offer_new_audio_description =
-        offer_new_audio_description || (rtc_options.offer_to_receive_audio > 0);
+        offer_new_audio_description ||
+        (offer_answer_options.offer_to_receive_audio > 0);
   }
-  if (rtc_options.offer_to_receive_video != RTCOfferAnswerOptions::kUndefined) {
-    recv_video = (rtc_options.offer_to_receive_video > 0);
+  if (offer_answer_options.offer_to_receive_video !=
+      RTCOfferAnswerOptions::kUndefined) {
+    recv_video = (offer_answer_options.offer_to_receive_video > 0);
     offer_new_video_description =
-        offer_new_video_description || (rtc_options.offer_to_receive_video > 0);
+        offer_new_video_description ||
+        (offer_answer_options.offer_to_receive_video > 0);
   }
 
   rtc::Optional<size_t> audio_index;
@@ -2733,13 +3038,6 @@
       !data_index ? nullptr
                   : &session_options->media_description_options[*data_index];
 
-  // Apply ICE restart flag and renomination flag.
-  for (auto& options : session_options->media_description_options) {
-    options.transport_options.ice_restart = rtc_options.ice_restart;
-    options.transport_options.enable_ice_renomination =
-        configuration_.enable_ice_renomination;
-  }
-
   AddRtpSenderOptions(GetSendersInternal(), audio_media_description_options,
                       video_media_description_options);
   AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options);
@@ -2752,16 +3050,162 @@
   if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) {
     session_options->data_channel_type = data_channel_type();
   }
+}
+
+// Find a new MID that is not already in |used_mids|, then add it to |used_mids|
+// and return a reference to it.
+// Generated MIDs should be no more than 3 bytes long to take up less space in
+// the RTP packet.
+static const std::string& AllocateMid(std::set<std::string>* used_mids) {
+  RTC_DCHECK(used_mids);
+  // We're boring: just generate MIDs 0, 1, 2, ...
+  size_t i = 0;
+  std::set<std::string>::iterator it;
+  bool inserted;
+  do {
+    std::string mid = rtc::ToString(i++);
+    auto insert_result = used_mids->insert(mid);
+    it = insert_result.first;
+    inserted = insert_result.second;
+  } while (!inserted);
+  return *it;
+}
+
+static cricket::MediaDescriptionOptions
+GetMediaDescriptionOptionsForTransceiver(
+    rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+        transceiver,
+    const std::string& mid) {
+  cricket::MediaDescriptionOptions media_description_options(
+      transceiver->internal()->media_type(), mid, transceiver->direction(),
+      transceiver->stopped());
+  cricket::SenderOptions sender_options;
+  sender_options.track_id = transceiver->sender()->id();
+  sender_options.stream_ids = transceiver->sender()->stream_ids();
+  // TODO(bugs.webrtc.org/7600): Set num_sim_layers to the number of encodings
+  // set in the RTP parameters when the transceiver was added.
+  sender_options.num_sim_layers = 1;
+  media_description_options.sender_options.push_back(sender_options);
+  return media_description_options;
+}
+
+void PeerConnection::GetOptionsForUnifiedPlanOffer(
+    const RTCOfferAnswerOptions& offer_answer_options,
+    cricket::MediaSessionOptions* session_options) {
+  // Rules for generating an offer are dictated by JSEP sections 5.2.1 (Initial
+  // Offers) and 5.2.2 (Subsequent Offers).
+  RTC_DCHECK_EQ(session_options->media_description_options.size(), 0);
+  const ContentInfos& local_contents =
+      (local_description() ? local_description()->description()->contents()
+                           : ContentInfos());
+  const ContentInfos& remote_contents =
+      (remote_description() ? remote_description()->description()->contents()
+                            : ContentInfos());
+  // The mline indices that can be recycled. New transceivers should reuse these
+  // slots first.
+  std::queue<size_t> recycleable_mline_indices;
+  // Track the MIDs used in previous offer/answer exchanges and the current
+  // offer so that new, unique MIDs are generated.
+  std::set<std::string> used_mids = seen_mids_;
+  // First, go through each media section that exists in either the local or
+  // remote description and generate a media section in this offer for the
+  // associated transceiver. If a media section can be recycled, generate a
+  // default, rejected media section here that can be later overwritten.
+  for (size_t i = 0;
+       i < std::max(local_contents.size(), remote_contents.size()); ++i) {
+    // Either |local_content| or |remote_content| is non-null.
+    const ContentInfo* local_content =
+        (i < local_contents.size() ? &local_contents[i] : nullptr);
+    const ContentInfo* remote_content =
+        (i < remote_contents.size() ? &remote_contents[i] : nullptr);
+    bool had_been_rejected = (local_content && local_content->rejected) ||
+                             (remote_content && remote_content->rejected);
+    const std::string& mid =
+        (local_content ? local_content->name : remote_content->name);
+    cricket::MediaType media_type =
+        (local_content ? local_content->media_description()->type()
+                       : remote_content->media_description()->type());
+    if (media_type == cricket::MEDIA_TYPE_AUDIO ||
+        media_type == cricket::MEDIA_TYPE_VIDEO) {
+      auto transceiver = GetAssociatedTransceiver(mid);
+      RTC_CHECK(transceiver);
+      // A media section is considered eligible for recycling if it is marked as
+      // rejected in either the local or remote description.
+      if (had_been_rejected) {
+        session_options->media_description_options.push_back(
+            cricket::MediaDescriptionOptions(
+                transceiver->internal()->media_type(), mid,
+                RtpTransceiverDirection::kInactive,
+                /*stopped=*/true));
+        recycleable_mline_indices.push(i);
+      } else {
+        session_options->media_description_options.push_back(
+            GetMediaDescriptionOptionsForTransceiver(transceiver, mid));
+        // CreateOffer shouldn't really cause any state changes in
+        // PeerConnection, but we need a way to match new transceivers to new
+        // media sections in SetLocalDescription and JSEP specifies this is done
+        // by recording the index of the media section generated for the
+        // transceiver in the offer.
+        transceiver->internal()->set_mline_index(i);
+      }
+    } else {
+      RTC_CHECK_EQ(cricket::MEDIA_TYPE_DATA, media_type);
+      // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+      // Plan.
+    }
+  }
+  // Next, look for transceivers that are newly added (that is, are not stopped
+  // and not associated). Reuse media sections marked as recyclable first,
+  // otherwise append to the end of the offer. New media sections should be
+  // added in the order they were added to the PeerConnection.
+  for (auto transceiver : transceivers_) {
+    if (transceiver->mid() || transceiver->stopped()) {
+      continue;
+    }
+    size_t mline_index;
+    if (!recycleable_mline_indices.empty()) {
+      mline_index = recycleable_mline_indices.front();
+      recycleable_mline_indices.pop();
+      session_options->media_description_options[mline_index] =
+          GetMediaDescriptionOptionsForTransceiver(transceiver,
+                                                   AllocateMid(&used_mids));
+    } else {
+      mline_index = session_options->media_description_options.size();
+      session_options->media_description_options.push_back(
+          GetMediaDescriptionOptionsForTransceiver(transceiver,
+                                                   AllocateMid(&used_mids)));
+    }
+    // See comment above for why CreateOffer changes the transceiver's state.
+    transceiver->internal()->set_mline_index(mline_index);
+  }
+  // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+  // Plan.
+}
+
+void PeerConnection::GetOptionsForAnswer(
+    const RTCOfferAnswerOptions& offer_answer_options,
+    cricket::MediaSessionOptions* session_options) {
+  ExtractSharedMediaSessionOptions(offer_answer_options, session_options);
+
+  if (IsUnifiedPlan()) {
+    GetOptionsForUnifiedPlanAnswer(offer_answer_options, session_options);
+  } else {
+    GetOptionsForPlanBAnswer(offer_answer_options, session_options);
+  }
+
+  // Apply ICE renomination flag.
+  for (auto& options : session_options->media_description_options) {
+    options.transport_options.enable_ice_renomination =
+        configuration_.enable_ice_renomination;
+  }
 
   session_options->rtcp_cname = rtcp_cname_;
   session_options->crypto_options = factory_->options().crypto_options;
 }
 
-void PeerConnection::GetOptionsForAnswer(
-    const RTCOfferAnswerOptions& rtc_options,
+void PeerConnection::GetOptionsForPlanBAnswer(
+    const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
     cricket::MediaSessionOptions* session_options) {
-  ExtractSharedMediaSessionOptions(rtc_options, session_options);
-
   // Figure out transceiver directional preferences.
   bool send_audio = HasRtpSender(cricket::MEDIA_TYPE_AUDIO);
   bool send_video = HasRtpSender(cricket::MEDIA_TYPE_VIDEO);
@@ -2772,11 +3216,13 @@
   bool recv_video = true;
 
   // The "offer_to_receive_X" options allow those defaults to be overridden.
-  if (rtc_options.offer_to_receive_audio != RTCOfferAnswerOptions::kUndefined) {
-    recv_audio = (rtc_options.offer_to_receive_audio > 0);
+  if (offer_answer_options.offer_to_receive_audio !=
+      RTCOfferAnswerOptions::kUndefined) {
+    recv_audio = (offer_answer_options.offer_to_receive_audio > 0);
   }
-  if (rtc_options.offer_to_receive_video != RTCOfferAnswerOptions::kUndefined) {
-    recv_video = (rtc_options.offer_to_receive_video > 0);
+  if (offer_answer_options.offer_to_receive_video !=
+      RTCOfferAnswerOptions::kUndefined) {
+    recv_video = (offer_answer_options.offer_to_receive_video > 0);
   }
 
   rtc::Optional<size_t> audio_index;
@@ -2805,12 +3251,6 @@
       !data_index ? nullptr
                   : &session_options->media_description_options[*data_index];
 
-  // Apply ICE renomination flag.
-  for (auto& options : session_options->media_description_options) {
-    options.transport_options.enable_ice_renomination =
-        configuration_.enable_ice_renomination;
-  }
-
   AddRtpSenderOptions(GetSendersInternal(), audio_media_description_options,
                       video_media_description_options);
   AddRtpDataChannelOptions(rtp_data_channels_, data_media_description_options);
@@ -2822,9 +3262,30 @@
   if (!rtp_data_channels_.empty() || data_channel_type() != cricket::DCT_RTP) {
     session_options->data_channel_type = data_channel_type();
   }
+}
 
-  session_options->rtcp_cname = rtcp_cname_;
-  session_options->crypto_options = factory_->options().crypto_options;
+void PeerConnection::GetOptionsForUnifiedPlanAnswer(
+    const PeerConnectionInterface::RTCOfferAnswerOptions& offer_answer_options,
+    cricket::MediaSessionOptions* session_options) {
+  // Rules for generating an answer are dictated by JSEP sections 5.3.1 (Initial
+  // Answers) and 5.3.2 (Subsequent Answers).
+  RTC_DCHECK(remote_description());
+  RTC_DCHECK(remote_description()->GetType() == SdpType::kOffer);
+  for (const ContentInfo& content :
+       remote_description()->description()->contents()) {
+    cricket::MediaType media_type = content.media_description()->type();
+    if (media_type == cricket::MEDIA_TYPE_AUDIO ||
+        media_type == cricket::MEDIA_TYPE_VIDEO) {
+      auto transceiver = GetAssociatedTransceiver(content.name);
+      RTC_CHECK(transceiver);
+      session_options->media_description_options.push_back(
+          GetMediaDescriptionOptionsForTransceiver(transceiver, content.name));
+    } else {
+      RTC_CHECK_EQ(cricket::MEDIA_TYPE_DATA, media_type);
+      // TODO(bugs.webrtc.org/7600): Add support for data channels with Unified
+      // Plan.
+    }
+  }
 }
 
 void PeerConnection::GenerateMediaDescriptionOptions(
@@ -3540,11 +4001,11 @@
 
 cricket::BaseChannel* PeerConnection::GetChannel(
     const std::string& content_name) {
-  if (voice_channel() && voice_channel()->content_name() == content_name) {
-    return voice_channel();
-  }
-  if (video_channel() && video_channel()->content_name() == content_name) {
-    return video_channel();
+  for (auto transceiver : transceivers_) {
+    cricket::BaseChannel* channel = transceiver->internal()->channel();
+    if (channel && channel->content_name() == content_name) {
+      return channel;
+    }
   }
   if (rtp_data_channel() &&
       rtp_data_channel()->content_name() == content_name) {
@@ -3779,9 +4240,9 @@
           tinfo.content_name, tinfo.description, type, &error);
     }
     if (!success) {
-      LOG_AND_RETURN_ERROR(
-          RTCErrorType::INVALID_PARAMETER,
-          "Failed to push down transport description: " + error);
+      LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+                           "Failed to push down transport description for " +
+                               tinfo.content_name + ": " + error);
     }
   }
 
@@ -4308,22 +4769,30 @@
   return *first_content_name;
 }
 
-RTCError PeerConnection::CreateChannels(const SessionDescription* desc) {
-  RTC_DCHECK(desc);
-
+RTCErrorOr<const cricket::ContentGroup*> PeerConnection::GetEarlyBundleGroup(
+    const SessionDescription& desc) const {
   const cricket::ContentGroup* bundle_group = nullptr;
   if (configuration_.bundle_policy ==
       PeerConnectionInterface::kBundlePolicyMaxBundle) {
-    bundle_group = desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+    bundle_group = desc.GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
     if (!bundle_group) {
       LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
                            "max-bundle configured but session description "
                            "has no BUNDLE group");
     }
   }
+  return std::move(bundle_group);
+}
+
+RTCError PeerConnection::CreateChannels(const SessionDescription& desc) {
+  auto bundle_group_or_error = GetEarlyBundleGroup(desc);
+  if (!bundle_group_or_error.ok()) {
+    return bundle_group_or_error.MoveError();
+  }
+  const cricket::ContentGroup* bundle_group = bundle_group_or_error.MoveValue();
 
   // Creating the media channels and transport proxies.
-  const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(desc);
+  const cricket::ContentInfo* voice = cricket::GetFirstAudioContent(&desc);
   if (voice && !voice->rejected &&
       !GetAudioTransceiver()->internal()->channel()) {
     cricket::VoiceChannel* voice_channel = CreateVoiceChannel(
@@ -4336,7 +4805,7 @@
     GetAudioTransceiver()->internal()->SetChannel(voice_channel);
   }
 
-  const cricket::ContentInfo* video = cricket::GetFirstVideoContent(desc);
+  const cricket::ContentInfo* video = cricket::GetFirstVideoContent(&desc);
   if (video && !video->rejected &&
       !GetVideoTransceiver()->internal()->channel()) {
     cricket::VideoChannel* video_channel = CreateVideoChannel(
@@ -4349,7 +4818,7 @@
     GetVideoTransceiver()->internal()->SetChannel(video_channel);
   }
 
-  const cricket::ContentInfo* data = cricket::GetFirstDataContent(desc);
+  const cricket::ContentInfo* data = cricket::GetFirstDataContent(&desc);
   if (data_channel_type_ != cricket::DCT_NONE && data && !data->rejected &&
       !rtp_data_channel_ && !sctp_transport_) {
     if (!CreateDataChannel(data->name, GetTransportNameForMediaSection(