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/api/rtptransceiverinterface.h b/api/rtptransceiverinterface.h
index 1d2988d..5d74ae1 100644
--- a/api/rtptransceiverinterface.h
+++ b/api/rtptransceiverinterface.h
@@ -29,6 +29,9 @@
   kInactive
 };
 
+// This is provided as a debugging aid. The format of the output is unspecified.
+std::ostream& operator<<(std::ostream& os, RtpTransceiverDirection direction);
+
 // Structure for initializing an RtpTransceiver in a call to
 // PeerConnectionInterface::AddTransceiver.
 // https://w3c.github.io/webrtc-pc/#dom-rtcrtptransceiverinit
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index a429290..a0f94af 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -400,6 +400,7 @@
       "peerconnection_datachannel_unittest.cc",
       "peerconnection_ice_unittest.cc",
       "peerconnection_integrationtest.cc",
+      "peerconnection_jsep_unittest.cc",
       "peerconnection_media_unittest.cc",
       "peerconnection_rtp_unittest.cc",
       "peerconnection_signaling_unittest.cc",
diff --git a/pc/mediasession.cc b/pc/mediasession.cc
index 24d0d34..692bcc2 100644
--- a/pc/mediasession.cc
+++ b/pc/mediasession.cc
@@ -1309,16 +1309,16 @@
 
   // Iterate through the media description options, matching with existing media
   // descriptions in |current_description|.
-  int msection_index = 0;
+  size_t msection_index = 0;
   for (const MediaDescriptionOptions& media_description_options :
        session_options.media_description_options) {
     const ContentInfo* current_content = nullptr;
     if (current_description &&
-        msection_index <
-            static_cast<int>(current_description->contents().size())) {
+        msection_index < current_description->contents().size()) {
       current_content = &current_description->contents()[msection_index];
-      // Media type must match.
-      RTC_DCHECK(IsMediaContentOfType(current_content,
+      // Media type must match unless this media section is being recycled.
+      RTC_DCHECK(current_content->rejected ||
+                 IsMediaContentOfType(current_content,
                                       media_description_options.type));
     }
     switch (media_description_options.type) {
@@ -1424,7 +1424,7 @@
              session_options.media_description_options.size());
   // Iterate through the media description options, matching with existing
   // media descriptions in |current_description|.
-  int msection_index = 0;
+  size_t msection_index = 0;
   for (const MediaDescriptionOptions& media_description_options :
        session_options.media_description_options) {
     const ContentInfo* offer_content = &offer->contents()[msection_index];
@@ -1435,8 +1435,7 @@
     RTC_DCHECK(media_description_options.mid == offer_content->name);
     const ContentInfo* current_content = nullptr;
     if (current_description &&
-        msection_index <
-            static_cast<int>(current_description->contents().size())) {
+        msection_index < current_description->contents().size()) {
       current_content = &current_description->contents()[msection_index];
     }
     switch (media_description_options.type) {
@@ -1802,8 +1801,8 @@
       GetAudioCodecsForOffer(media_description_options.direction);
 
   AudioCodecs filtered_codecs;
-  // Add the codecs from current content if exists.
-  if (current_content) {
+  // Add the codecs from current content if it exists and is not being recycled.
+  if (current_content && !current_content->rejected) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
     const AudioContentDescription* acd =
         current_content->media_description()->as_audio();
@@ -1877,8 +1876,8 @@
                                         &crypto_suites);
 
   VideoCodecs filtered_codecs;
-  // Add the codecs from current content if exists.
-  if (current_content) {
+  // Add the codecs from current content if it exists and is not being recycled.
+  if (current_content && !current_content->rejected) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
     const VideoContentDescription* vcd =
         current_content->media_description()->as_video();
@@ -2038,8 +2037,8 @@
       GetAudioCodecsForAnswer(offer_rtd, answer_rtd);
 
   AudioCodecs filtered_codecs;
-  // Add the codecs from current content if exists.
-  if (current_content) {
+  // Add the codecs from current content if it exists and is not being recycled.
+  if (current_content && !current_content->rejected) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
     const AudioContentDescription* acd =
         current_content->media_description()->as_audio();
@@ -2120,8 +2119,8 @@
   }
 
   VideoCodecs filtered_codecs;
-  // Add the codecs from current content if exists.
-  if (current_content) {
+  // Add the codecs from current content if it exists and is not being recycled.
+  if (current_content && !current_content->rejected) {
     RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
     const VideoContentDescription* vcd =
         current_content->media_description()->as_video();
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(
diff --git a/pc/peerconnection.h b/pc/peerconnection.h
index b1a1d9e..b9a86d8 100644
--- a/pc/peerconnection.h
+++ b/pc/peerconnection.h
@@ -412,6 +412,43 @@
   RTCError ApplyRemoteDescription(
       std::unique_ptr<SessionDescriptionInterface> desc);
 
+  // Updates the local RtpTransceivers according to the JSEP rules. Called as
+  // part of setting the local/remote description.
+  RTCError UpdateTransceiversAndDataChannels(
+      cricket::ContentSource source,
+      const SessionDescriptionInterface* old_session,
+      const SessionDescriptionInterface& new_session);
+
+  // Either creates or destroys the transceiver's BaseChannel according to the
+  // given media section.
+  RTCError UpdateTransceiverChannel(
+      rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+          transceiver,
+      const cricket::ContentInfo& content,
+      const cricket::ContentGroup* bundle_group);
+
+  // Associate the given transceiver according to the JSEP rules.
+  RTCErrorOr<
+      rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
+  AssociateTransceiver(cricket::ContentSource source,
+                       size_t mline_index,
+                       const cricket::ContentInfo& content,
+                       const cricket::ContentInfo* old_content);
+
+  // Returns the RtpTransceiver, if found, that is associated to the given MID.
+  rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+  GetAssociatedTransceiver(const std::string& mid) const;
+
+  // Returns the RtpTransceiver, if found, that was assigned to the given mline
+  // index in CreateOffer.
+  rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+  GetTransceiverByMLineIndex(size_t mline_index) const;
+
+  // Returns an RtpTransciever, if available, that can be used to receive the
+  // given media type according to JSEP rules.
+  rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>
+  FindAvailableTransceiverToReceive(cricket::MediaType media_type) const;
+
   // Returns the media section in the given session description that is
   // associated with the RtpTransceiver. Returns null if none found or this
   // RtpTransceiver is not associated. Logic varies depending on the
@@ -427,14 +464,30 @@
 
   // Returns a MediaSessionOptions struct with options decided by |options|,
   // the local MediaStreams and DataChannels.
-  void GetOptionsForOffer(
-      const PeerConnectionInterface::RTCOfferAnswerOptions& rtc_options,
+  void GetOptionsForOffer(const PeerConnectionInterface::RTCOfferAnswerOptions&
+                              offer_answer_options,
+                          cricket::MediaSessionOptions* session_options);
+  void GetOptionsForPlanBOffer(
+      const PeerConnectionInterface::RTCOfferAnswerOptions&
+          offer_answer_options,
+      cricket::MediaSessionOptions* session_options);
+  void GetOptionsForUnifiedPlanOffer(
+      const PeerConnectionInterface::RTCOfferAnswerOptions&
+          offer_answer_options,
       cricket::MediaSessionOptions* session_options);
 
   // Returns a MediaSessionOptions struct with options decided by
   // |constraints|, the local MediaStreams and DataChannels.
-  void GetOptionsForAnswer(const RTCOfferAnswerOptions& options,
+  void GetOptionsForAnswer(const RTCOfferAnswerOptions& offer_answer_options,
                            cricket::MediaSessionOptions* session_options);
+  void GetOptionsForPlanBAnswer(
+      const PeerConnectionInterface::RTCOfferAnswerOptions&
+          offer_answer_options,
+      cricket::MediaSessionOptions* session_options);
+  void GetOptionsForUnifiedPlanAnswer(
+      const PeerConnectionInterface::RTCOfferAnswerOptions&
+          offer_answer_options,
+      cricket::MediaSessionOptions* session_options);
 
   // Generates MediaDescriptionOptions for the |session_opts| based on existing
   // local description or remote description.
@@ -706,7 +759,15 @@
   // Allocates media channels based on the |desc|. If |desc| doesn't have
   // the BUNDLE option, this method will disable BUNDLE in PortAllocator.
   // This method will also delete any existing media channels before creating.
-  RTCError CreateChannels(const cricket::SessionDescription* desc);
+  RTCError CreateChannels(const cricket::SessionDescription& desc);
+
+  // If the BUNDLE policy is max-bundle, then we know for sure that all
+  // transports will be bundled from the start. This method returns the BUNDLE
+  // group if that's the case, or null if BUNDLE will be negotiated later. An
+  // error is returned if max-bundle is specified but the session description
+  // does not have a BUNDLE group.
+  RTCErrorOr<const cricket::ContentGroup*> GetEarlyBundleGroup(
+      const cricket::SessionDescription& desc) const;
 
   // Helper methods to create media channels.
   cricket::VoiceChannel* CreateVoiceChannel(const std::string& mid,
@@ -859,6 +920,9 @@
   std::vector<
       rtc::scoped_refptr<RtpTransceiverProxyWithInternal<RtpTransceiver>>>
       transceivers_;
+  // MIDs that have been seen either by SetLocalDescription or
+  // SetRemoteDescription over the life of the PeerConnection.
+  std::set<std::string> seen_mids_;
 
   SessionError session_error_ = SessionError::kNone;
   std::string session_error_desc_;
diff --git a/pc/peerconnection_jsep_unittest.cc b/pc/peerconnection_jsep_unittest.cc
new file mode 100644
index 0000000..119b135
--- /dev/null
+++ b/pc/peerconnection_jsep_unittest.cc
@@ -0,0 +1,734 @@
+/*
+ *  Copyright 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "api/audio_codecs/builtin_audio_decoder_factory.h"
+#include "api/audio_codecs/builtin_audio_encoder_factory.h"
+#include "pc/mediasession.h"
+#include "pc/peerconnectionwrapper.h"
+#include "pc/sdputils.h"
+#ifdef WEBRTC_ANDROID
+#include "pc/test/androidtestinitializer.h"
+#endif
+#include "pc/test/fakeaudiocapturemodule.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/ptr_util.h"
+#include "rtc_base/virtualsocketserver.h"
+#include "test/gmock.h"
+
+// This file contains tests that ensure the PeerConnection's implementation of
+// CreateOffer/CreateAnswer/SetLocalDescription/SetRemoteDescription conform
+// to the JavaScript Session Establishment Protocol (JSEP).
+// For now these semantics are only available when configuring the
+// PeerConnection with Unified Plan, but eventually that will be the default.
+
+namespace webrtc {
+
+using cricket::MediaContentDescription;
+using RTCConfiguration = PeerConnectionInterface::RTCConfiguration;
+using ::testing::Values;
+using ::testing::Combine;
+using ::testing::ElementsAre;
+
+class PeerConnectionJsepTest : public ::testing::Test {
+ protected:
+  typedef std::unique_ptr<PeerConnectionWrapper> WrapperPtr;
+
+  PeerConnectionJsepTest()
+      : vss_(new rtc::VirtualSocketServer()), main_(vss_.get()) {
+#ifdef WEBRTC_ANDROID
+    InitializeAndroidObjects();
+#endif
+    pc_factory_ = CreatePeerConnectionFactory(
+        rtc::Thread::Current(), rtc::Thread::Current(), rtc::Thread::Current(),
+        FakeAudioCaptureModule::Create(), CreateBuiltinAudioEncoderFactory(),
+        CreateBuiltinAudioDecoderFactory(), nullptr, nullptr);
+  }
+
+  WrapperPtr CreatePeerConnection() {
+    RTCConfiguration config;
+    config.sdp_semantics = SdpSemantics::kUnifiedPlan;
+    return CreatePeerConnection(config);
+  }
+
+  WrapperPtr CreatePeerConnection(const RTCConfiguration& config) {
+    auto observer = rtc::MakeUnique<MockPeerConnectionObserver>();
+    auto pc = pc_factory_->CreatePeerConnection(config, nullptr, nullptr,
+                                                observer.get());
+    if (!pc) {
+      return nullptr;
+    }
+
+    return rtc::MakeUnique<PeerConnectionWrapper>(pc_factory_, pc,
+                                                  std::move(observer));
+  }
+
+  std::unique_ptr<rtc::VirtualSocketServer> vss_;
+  rtc::AutoSocketServerThread main_;
+  rtc::scoped_refptr<PeerConnectionFactoryInterface> pc_factory_;
+};
+
+// Tests for JSEP initial offer generation.
+
+// Test that an offer created by a PeerConnection with no transceivers generates
+// no media sections.
+TEST_F(PeerConnectionJsepTest, EmptyInitialOffer) {
+  auto caller = CreatePeerConnection();
+  auto offer = caller->CreateOffer();
+  EXPECT_EQ(0u, offer->description()->contents().size());
+}
+
+// Test that an initial offer with one audio track generates one audio media
+// section.
+TEST_F(PeerConnectionJsepTest, AudioOnlyInitialOffer) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto offer = caller->CreateOffer();
+
+  auto contents = offer->description()->contents();
+  ASSERT_EQ(1u, contents.size());
+  EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, contents[0].media_description()->type());
+}
+
+// Test than an initial offer with one video track generates one video media
+// section
+TEST_F(PeerConnectionJsepTest, VideoOnlyInitialOffer) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  auto offer = caller->CreateOffer();
+
+  auto contents = offer->description()->contents();
+  ASSERT_EQ(1u, contents.size());
+  EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, contents[0].media_description()->type());
+}
+
+// Test that multiple media sections in the initial offer are ordered in the
+// order the transceivers were added to the PeerConnection. This is required by
+// JSEP section 5.2.1.
+TEST_F(PeerConnectionJsepTest, MediaSectionsInInitialOfferOrderedCorrectly) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  RtpTransceiverInit init;
+  init.direction = RtpTransceiverDirection::kSendOnly;
+  caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO, init);
+  auto offer = caller->CreateOffer();
+
+  auto contents = offer->description()->contents();
+  ASSERT_EQ(3u, contents.size());
+
+  const MediaContentDescription* media_description1 =
+      contents[0].media_description();
+  EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, media_description1->type());
+  EXPECT_EQ(RtpTransceiverDirection::kSendRecv,
+            media_description1->direction());
+
+  const MediaContentDescription* media_description2 =
+      contents[1].media_description();
+  EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, media_description2->type());
+  EXPECT_EQ(RtpTransceiverDirection::kSendRecv,
+            media_description2->direction());
+
+  const MediaContentDescription* media_description3 =
+      contents[2].media_description();
+  EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, media_description3->type());
+  EXPECT_EQ(RtpTransceiverDirection::kSendOnly,
+            media_description3->direction());
+}
+
+// Test that media sections in the initial offer have different mids.
+TEST_F(PeerConnectionJsepTest, MediaSectionsInInitialOfferHaveDifferentMids) {
+  auto caller = CreatePeerConnection();
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto offer = caller->CreateOffer();
+
+  std::string sdp;
+  offer->ToString(&sdp);
+  RTC_LOG(LS_INFO) << sdp;
+
+  auto contents = offer->description()->contents();
+  ASSERT_EQ(2u, contents.size());
+  EXPECT_NE(contents[0].name, contents[1].name);
+}
+
+TEST_F(PeerConnectionJsepTest,
+       StoppedTransceiverHasNoMediaSectionInInitialOffer) {
+  auto caller = CreatePeerConnection();
+  auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  transceiver->Stop();
+
+  auto offer = caller->CreateOffer();
+  EXPECT_EQ(0u, offer->description()->contents().size());
+}
+
+// Tests for JSEP SetLocalDescription with a local offer.
+
+TEST_F(PeerConnectionJsepTest, SetLocalEmptyOfferCreatesNoTransceivers) {
+  auto caller = CreatePeerConnection();
+  ASSERT_TRUE(caller->SetLocalDescription(caller->CreateOffer()));
+
+  EXPECT_THAT(caller->pc()->GetTransceivers(), ElementsAre());
+  EXPECT_THAT(caller->pc()->GetSenders(), ElementsAre());
+  EXPECT_THAT(caller->pc()->GetReceivers(), ElementsAre());
+}
+
+TEST_F(PeerConnectionJsepTest, SetLocalOfferSetsTransceiverMid) {
+  auto caller = CreatePeerConnection();
+  auto audio_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto video_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+
+  auto offer = caller->CreateOffer();
+  std::string audio_mid = offer->description()->contents()[0].name;
+  std::string video_mid = offer->description()->contents()[1].name;
+
+  ASSERT_TRUE(caller->SetLocalDescription(std::move(offer)));
+
+  EXPECT_EQ(audio_mid, audio_transceiver->mid());
+  EXPECT_EQ(video_mid, video_transceiver->mid());
+}
+
+// Tests for JSEP SetRemoteDescription with a remote offer.
+
+// Test that setting a remote offer with sendrecv audio and video creates two
+// transceivers, one for receiving audio and one for receiving video.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferCreatesTransceivers) {
+  auto caller = CreatePeerConnection();
+  auto caller_audio = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto caller_video = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, transceivers.size());
+  EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO,
+            transceivers[0]->receiver()->media_type());
+  EXPECT_EQ(caller_audio->mid(), transceivers[0]->mid());
+  EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceivers[0]->direction());
+  EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO,
+            transceivers[1]->receiver()->media_type());
+  EXPECT_EQ(caller_video->mid(), transceivers[1]->mid());
+  EXPECT_EQ(RtpTransceiverDirection::kRecvOnly, transceivers[1]->direction());
+}
+
+// Test that setting a remote offer with an audio track will reuse the
+// transceiver created for a local audio track added by AddTrack.
+// This is specified in JSEP section 5.10 (Applying a Remote Description). The
+// intent is to preserve backwards compatibility with clients who only use the
+// AddTrack API.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferReusesTransceiverFromAddTrack) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto caller_audio = caller->pc()->GetTransceivers()[0];
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(1u, transceivers.size());
+  EXPECT_EQ(MediaStreamTrackInterface::kAudioKind,
+            transceivers[0]->receiver()->track()->kind());
+  EXPECT_EQ(caller_audio->mid(), transceivers[0]->mid());
+}
+
+// Test that setting a remote offer with an audio track marked sendonly will not
+// reuse a transceiver created by AddTrack. JSEP only allows the transceiver to
+// be reused if the offer direction is sendrecv or recvonly.
+TEST_F(PeerConnectionJsepTest,
+       SetRemoteOfferDoesNotReuseTransceiverIfDirectionSendOnly) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto caller_audio = caller->pc()->GetTransceivers()[0];
+  caller_audio->SetDirection(RtpTransceiverDirection::kSendOnly);
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, transceivers.size());
+  EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+  EXPECT_EQ(caller_audio->mid(), transceivers[1]->mid());
+}
+
+// Test that setting a remote offer with an audio track will not reuse a
+// transceiver added by AddTransceiver. The logic for reusing a transceiver is
+// specific to those added by AddTrack and is tested above.
+TEST_F(PeerConnectionJsepTest,
+       SetRemoteOfferDoesNotReuseTransceiverFromAddTransceiver) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  auto transceiver = callee->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, transceivers.size());
+  EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+  EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[1]->mid());
+  EXPECT_EQ(MediaStreamTrackInterface::kAudioKind,
+            transceivers[1]->receiver()->track()->kind());
+}
+
+// Test that setting a remote offer with an audio track will not reuse a
+// transceiver created for a local video track added by AddTrack.
+TEST_F(PeerConnectionJsepTest,
+       SetRemoteOfferDoesNotReuseTransceiverOfWrongType) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  auto video_sender = callee->AddVideoTrack("v");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, transceivers.size());
+  EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+  EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[1]->mid());
+  EXPECT_EQ(MediaStreamTrackInterface::kAudioKind,
+            transceivers[1]->receiver()->track()->kind());
+}
+
+// Test that setting a remote offer with an audio track will not reuse a
+// stopped transceiver.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferDoesNotReuseStoppedTransceiver) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+  callee->pc()->GetTransceivers()[0]->Stop();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, transceivers.size());
+  EXPECT_EQ(rtc::nullopt, transceivers[0]->mid());
+  EXPECT_TRUE(transceivers[0]->stopped());
+  EXPECT_EQ(caller->pc()->GetTransceivers()[0]->mid(), transceivers[1]->mid());
+  EXPECT_FALSE(transceivers[1]->stopped());
+}
+
+// Test that audio and video transceivers created on the remote side with
+// AddTrack will all be reused if there is the same number of audio/video tracks
+// in the remote offer. Additionally, this tests that transceivers are
+// successfully matched even if they are in a different order on the remote
+// side.
+TEST_F(PeerConnectionJsepTest, SetRemoteOfferReusesTransceiversOfBothTypes) {
+  auto caller = CreatePeerConnection();
+  caller->AddVideoTrack("v");
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+  callee->AddVideoTrack("v");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto caller_transceivers = caller->pc()->GetTransceivers();
+  auto callee_transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, callee_transceivers.size());
+  EXPECT_EQ(caller_transceivers[0]->mid(), callee_transceivers[1]->mid());
+  EXPECT_EQ(caller_transceivers[1]->mid(), callee_transceivers[0]->mid());
+}
+
+// Tests for JSEP initial CreateAnswer.
+
+// Test that the answer to a remote offer creates media sections for each
+// offered media in the same order and with the same mids.
+TEST_F(PeerConnectionJsepTest, CreateAnswerHasSameMidsAsOffer) {
+  auto caller = CreatePeerConnection();
+  auto first_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  auto second_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto third_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_VIDEO);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto answer = callee->CreateAnswer();
+  auto contents = answer->description()->contents();
+  ASSERT_EQ(3u, contents.size());
+  EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, contents[0].media_description()->type());
+  EXPECT_EQ(*first_transceiver->mid(), contents[0].name);
+  EXPECT_EQ(cricket::MEDIA_TYPE_AUDIO, contents[1].media_description()->type());
+  EXPECT_EQ(*second_transceiver->mid(), contents[1].name);
+  EXPECT_EQ(cricket::MEDIA_TYPE_VIDEO, contents[2].media_description()->type());
+  EXPECT_EQ(*third_transceiver->mid(), contents[2].name);
+}
+
+// Test that an answering media section is marked as rejected if the underlying
+// transceiver has been stopped.
+TEST_F(PeerConnectionJsepTest, CreateAnswerRejectsStoppedTransceiver) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  callee->pc()->GetTransceivers()[0]->Stop();
+
+  auto answer = callee->CreateAnswer();
+  auto contents = answer->description()->contents();
+  ASSERT_EQ(1u, contents.size());
+  EXPECT_TRUE(contents[0].rejected);
+}
+
+// Test that CreateAnswer will generate media sections which will only send or
+// receive if the offer indicates it can do the reciprocating direction.
+// The full matrix is tested more extensively in MediaSession.
+TEST_F(PeerConnectionJsepTest, CreateAnswerNegotiatesDirection) {
+  auto caller = CreatePeerConnection();
+  RtpTransceiverInit init;
+  init.direction = RtpTransceiverDirection::kSendOnly;
+  caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO, init);
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto answer = callee->CreateAnswer();
+  auto contents = answer->description()->contents();
+  ASSERT_EQ(1u, contents.size());
+  EXPECT_EQ(RtpTransceiverDirection::kRecvOnly,
+            contents[0].media_description()->direction());
+}
+
+// Tests for JSEP SetLocalDescription with a local answer.
+// Note that these test only the additional behaviors not covered by
+// SetLocalDescription with a local offer.
+
+// Test that SetLocalDescription with an answer sets the current_direction
+// property of the transceivers mentioned in the session description.
+TEST_F(PeerConnectionJsepTest, SetLocalAnswerUpdatesCurrentDirection) {
+  auto caller = CreatePeerConnection();
+  auto caller_audio = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  caller_audio->SetDirection(RtpTransceiverDirection::kRecvOnly);
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(1u, transceivers.size());
+  // Since the offer was recvonly and the transceiver direction is sendrecv,
+  // the negotiated direction will be sendonly.
+  EXPECT_EQ(RtpTransceiverDirection::kSendOnly,
+            transceivers[0]->current_direction());
+}
+
+// Tests for JSEP SetRemoteDescription with a remote answer.
+// Note that these test only the additional behaviors not covered by
+// SetRemoteDescription with a remote offer.
+
+TEST_F(PeerConnectionJsepTest, SetRemoteAnswerUpdatesCurrentDirection) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+  auto callee_audio = callee->pc()->GetTransceivers()[0];
+  callee_audio->SetDirection(RtpTransceiverDirection::kSendOnly);
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  auto transceivers = caller->pc()->GetTransceivers();
+  ASSERT_EQ(1u, transceivers.size());
+  // Since the remote transceiver was set to sendonly, the negotiated direction
+  // in the answer would be sendonly which we apply as recvonly to the local
+  // transceiver.
+  EXPECT_EQ(RtpTransceiverDirection::kRecvOnly,
+            transceivers[0]->current_direction());
+}
+
+// Tests for multiple round trips.
+
+// Test that setting a transceiver with the inactive direction does not stop it
+// on either the caller or the callee.
+TEST_F(PeerConnectionJsepTest, SettingTransceiverInactiveDoesNotStopIt) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+  callee->pc()->GetTransceivers()[0]->SetDirection(
+      RtpTransceiverDirection::kInactive);
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  EXPECT_FALSE(caller->pc()->GetTransceivers()[0]->stopped());
+  EXPECT_FALSE(callee->pc()->GetTransceivers()[0]->stopped());
+}
+
+// Test that if a transceiver had been associated and later stopped, then a
+// media section is still generated for it and the media section is marked as
+// rejected.
+TEST_F(PeerConnectionJsepTest,
+       ReOfferMediaSectionForAssociatedStoppedTransceiverIsRejected) {
+  auto caller = CreatePeerConnection();
+  auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  ASSERT_TRUE(transceiver->mid());
+  transceiver->Stop();
+
+  auto reoffer = caller->CreateOffer();
+  auto contents = reoffer->description()->contents();
+  ASSERT_EQ(1u, contents.size());
+  EXPECT_TRUE(contents[0].rejected);
+}
+
+// Test that stopping an associated transceiver on the caller side will stop the
+// corresponding transceiver on the remote side when the remote offer is
+// applied.
+TEST_F(PeerConnectionJsepTest,
+       StoppingTransceiverInOfferStopsTransceiverOnRemoteSide) {
+  auto caller = CreatePeerConnection();
+  auto transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  transceiver->Stop();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  auto transceivers = callee->pc()->GetTransceivers();
+  EXPECT_TRUE(transceivers[0]->stopped());
+  EXPECT_TRUE(transceivers[0]->mid());
+}
+
+// Test that CreateOffer will only generate a recycled media section if the
+// transceiver to be recycled has been seen stopped by the other side first.
+TEST_F(PeerConnectionJsepTest,
+       CreateOfferDoesNotRecycleMediaSectionIfFirstStopped) {
+  auto caller = CreatePeerConnection();
+  auto first_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+  ASSERT_TRUE(
+      caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
+
+  auto second_transceiver = caller->AddTransceiver(cricket::MEDIA_TYPE_AUDIO);
+  first_transceiver->Stop();
+
+  auto reoffer = caller->CreateOffer();
+  auto contents = reoffer->description()->contents();
+  ASSERT_EQ(2u, contents.size());
+  EXPECT_TRUE(contents[0].rejected);
+  EXPECT_FALSE(contents[1].rejected);
+}
+
+// Test that the offer/answer and transceivers for both the caller and callee
+// side are generated/updated correctly when recycling an audio/video media
+// section as a media section of either the same or opposite type.
+class RecycleMediaSectionTest
+    : public PeerConnectionJsepTest,
+      public testing::WithParamInterface<
+          std::tuple<cricket::MediaType, cricket::MediaType>> {
+ protected:
+  RecycleMediaSectionTest() {
+    first_type_ = std::get<0>(GetParam());
+    second_type_ = std::get<1>(GetParam());
+  }
+
+  cricket::MediaType first_type_;
+  cricket::MediaType second_type_;
+};
+
+TEST_P(RecycleMediaSectionTest, VerifyOfferAnswerAndTransceivers) {
+  auto caller = CreatePeerConnection();
+  auto first_transceiver = caller->AddTransceiver(first_type_);
+  auto callee = CreatePeerConnection();
+
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+  std::string first_mid = *first_transceiver->mid();
+  first_transceiver->Stop();
+
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+  auto second_transceiver = caller->AddTransceiver(second_type_);
+
+  // The offer should reuse the previous media section but allocate a new MID
+  // and change the media type.
+  auto offer = caller->CreateOffer();
+  auto offer_contents = offer->description()->contents();
+  ASSERT_EQ(1u, offer_contents.size());
+  EXPECT_FALSE(offer_contents[0].rejected);
+  EXPECT_EQ(second_type_, offer_contents[0].media_description()->type());
+  std::string second_mid = offer_contents[0].name;
+  EXPECT_NE(first_mid, second_mid);
+
+  // Setting the local offer will dissociate the previous transceiver and set
+  // the MID for the new transceiver.
+  ASSERT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+  EXPECT_EQ(rtc::nullopt, first_transceiver->mid());
+  EXPECT_EQ(second_mid, second_transceiver->mid());
+
+  // Setting the remote offer will dissociate the previous transceiver and
+  // create a new transceiver for the media section.
+  ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+  auto callee_transceivers = callee->pc()->GetTransceivers();
+  ASSERT_EQ(2u, callee_transceivers.size());
+  EXPECT_EQ(rtc::nullopt, callee_transceivers[0]->mid());
+  EXPECT_EQ(first_type_, callee_transceivers[0]->receiver()->media_type());
+  EXPECT_EQ(second_mid, callee_transceivers[1]->mid());
+  EXPECT_EQ(second_type_, callee_transceivers[1]->receiver()->media_type());
+
+  // The answer should have only one media section for the new transceiver.
+  auto answer = callee->CreateAnswer();
+  auto answer_contents = answer->description()->contents();
+  ASSERT_EQ(1u, answer_contents.size());
+  EXPECT_FALSE(answer_contents[0].rejected);
+  EXPECT_EQ(second_mid, answer_contents[0].name);
+  EXPECT_EQ(second_type_, answer_contents[0].media_description()->type());
+
+  // Setting the local answer should succeed.
+  ASSERT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+
+  // Setting the remote answer should succeed.
+  ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+}
+
+// Test all combinations of audio and video as the first and second media type
+// for the media section. This is needed for full test coverage because
+// MediaSession has separate functions for processing audio and video media
+// sections.
+INSTANTIATE_TEST_CASE_P(
+    PeerConnectionJsepTest,
+    RecycleMediaSectionTest,
+    Combine(Values(cricket::MEDIA_TYPE_AUDIO, cricket::MEDIA_TYPE_VIDEO),
+            Values(cricket::MEDIA_TYPE_AUDIO, cricket::MEDIA_TYPE_VIDEO)));
+
+// Tests for MID properties.
+
+static void RenameSection(size_t mline_index,
+                          const std::string& new_mid,
+                          SessionDescriptionInterface* sdesc) {
+  cricket::SessionDescription* desc = sdesc->description();
+  std::string old_mid = desc->contents()[mline_index].name;
+  desc->contents()[mline_index].name = new_mid;
+  desc->transport_infos()[mline_index].content_name = new_mid;
+  const cricket::ContentGroup* bundle =
+      desc->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  if (bundle) {
+    cricket::ContentGroup new_bundle = *bundle;
+    if (new_bundle.RemoveContentName(old_mid)) {
+      new_bundle.AddContentName(new_mid);
+    }
+    desc->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+    desc->AddGroup(new_bundle);
+  }
+}
+
+// Test that two PeerConnections can have a successful offer/answer exchange if
+// the MIDs are changed from the defaults.
+TEST_F(PeerConnectionJsepTest, OfferAnswerWithChangedMids) {
+  constexpr char kFirstMid[] = "nondefaultmid";
+  constexpr char kSecondMid[] = "randommid";
+
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  caller->AddAudioTrack("b");
+  auto callee = CreatePeerConnection();
+
+  auto offer = caller->CreateOffer();
+  RenameSection(0, kFirstMid, offer.get());
+  RenameSection(1, kSecondMid, offer.get());
+
+  ASSERT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+  auto caller_transceivers = caller->pc()->GetTransceivers();
+  EXPECT_EQ(kFirstMid, caller_transceivers[0]->mid());
+  EXPECT_EQ(kSecondMid, caller_transceivers[1]->mid());
+
+  ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+  auto callee_transceivers = callee->pc()->GetTransceivers();
+  EXPECT_EQ(kFirstMid, callee_transceivers[0]->mid());
+  EXPECT_EQ(kSecondMid, callee_transceivers[1]->mid());
+
+  auto answer = callee->CreateAnswer();
+  auto answer_contents = answer->description()->contents();
+  EXPECT_EQ(kFirstMid, answer_contents[0].name);
+  EXPECT_EQ(kSecondMid, answer_contents[1].name);
+
+  ASSERT_TRUE(
+      callee->SetLocalDescription(CloneSessionDescription(answer.get())));
+  ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
+}
+
+// Test that CreateOffer will generate a MID that is not already used if the
+// default it would have picked is already taken. This is tested by using a
+// third PeerConnection to determine what the default would be for the second
+// media section then setting that as the first media section's MID.
+TEST_F(PeerConnectionJsepTest, CreateOfferGeneratesUniqueMidIfAlreadyTaken) {
+  // First, find what the default MID is for the second media section.
+  auto pc = CreatePeerConnection();
+  pc->AddAudioTrack("a");
+  pc->AddAudioTrack("b");
+  auto default_offer = pc->CreateOffer();
+  std::string default_second_mid =
+      default_offer->description()->contents()[1].name;
+
+  // Now, do an offer/answer with one track which has the MID set to the default
+  // second MID.
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+
+  auto offer = caller->CreateOffer();
+  RenameSection(0, default_second_mid, offer.get());
+
+  ASSERT_TRUE(
+      caller->SetLocalDescription(CloneSessionDescription(offer.get())));
+  ASSERT_TRUE(callee->SetRemoteDescription(std::move(offer)));
+  ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
+
+  // Add a second track and ensure that the MID is different.
+  caller->AddAudioTrack("b");
+
+  auto reoffer = caller->CreateOffer();
+  auto reoffer_contents = reoffer->description()->contents();
+  EXPECT_EQ(default_second_mid, reoffer_contents[0].name);
+  EXPECT_NE(reoffer_contents[0].name, reoffer_contents[1].name);
+}
+
+// Test that a reoffer initiated by the callee adds a new track to the caller.
+TEST_F(PeerConnectionJsepTest, CalleeDoesReoffer) {
+  auto caller = CreatePeerConnection();
+  caller->AddAudioTrack("a");
+  auto callee = CreatePeerConnection();
+  callee->AddAudioTrack("a");
+  callee->AddVideoTrack("v");
+
+  ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+
+  EXPECT_EQ(1u, caller->pc()->GetTransceivers().size());
+  EXPECT_EQ(2u, callee->pc()->GetTransceivers().size());
+
+  ASSERT_TRUE(callee->ExchangeOfferAnswerWith(caller.get()));
+
+  EXPECT_EQ(2u, caller->pc()->GetTransceivers().size());
+  EXPECT_EQ(2u, callee->pc()->GetTransceivers().size());
+}
+
+}  // namespace webrtc
diff --git a/pc/peerconnectionwrapper.cc b/pc/peerconnectionwrapper.cc
index 121cc64..d297054 100644
--- a/pc/peerconnectionwrapper.cc
+++ b/pc/peerconnectionwrapper.cc
@@ -179,6 +179,45 @@
   return observer->result();
 }
 
+bool PeerConnectionWrapper::ExchangeOfferAnswerWith(
+    PeerConnectionWrapper* answerer) {
+  RTC_DCHECK(answerer);
+  if (answerer == this) {
+    RTC_LOG(LS_ERROR) << "Cannot exchange offer/answer with ourself!";
+    return false;
+  }
+  auto offer = CreateOffer();
+  EXPECT_TRUE(offer);
+  if (!offer) {
+    return false;
+  }
+  bool set_local_offer =
+      SetLocalDescription(CloneSessionDescription(offer.get()));
+  EXPECT_TRUE(set_local_offer);
+  if (!set_local_offer) {
+    return false;
+  }
+  bool set_remote_offer = answerer->SetRemoteDescription(std::move(offer));
+  EXPECT_TRUE(set_remote_offer);
+  if (!set_remote_offer) {
+    return false;
+  }
+  auto answer = answerer->CreateAnswer();
+  EXPECT_TRUE(answer);
+  if (!answer) {
+    return false;
+  }
+  bool set_local_answer =
+      answerer->SetLocalDescription(CloneSessionDescription(answer.get()));
+  EXPECT_TRUE(set_local_answer);
+  if (!set_local_answer) {
+    return false;
+  }
+  bool set_remote_answer = SetRemoteDescription(std::move(answer));
+  EXPECT_TRUE(set_remote_answer);
+  return set_remote_answer;
+}
+
 rtc::scoped_refptr<RtpTransceiverInterface>
 PeerConnectionWrapper::AddTransceiver(cricket::MediaType media_type) {
   RTCErrorOr<rtc::scoped_refptr<RtpTransceiverInterface>> result =
diff --git a/pc/peerconnectionwrapper.h b/pc/peerconnectionwrapper.h
index e7d19ea..9208207 100644
--- a/pc/peerconnectionwrapper.h
+++ b/pc/peerconnectionwrapper.h
@@ -93,6 +93,21 @@
   bool SetRemoteDescription(std::unique_ptr<SessionDescriptionInterface> desc,
                             RTCError* error_out);
 
+  // Does a round of offer/answer with the local PeerConnectionWrapper
+  // generating the offer and the given PeerConnectionWrapper generating the
+  // answer.
+  // Equivalent to:
+  // 1. this->CreateOffer()
+  // 2. this->SetLocalDescription(offer)
+  // 3. answerer->SetRemoteDescription(offer)
+  // 4. answerer->CreateAnswer()
+  // 5. answerer->SetLocalDescription(answer)
+  // 6. this->SetRemoteDescription(answer)
+  // Returns true if all steps succeed, false otherwise.
+  // Suggested usage:
+  //   ASSERT_TRUE(caller->ExchangeOfferAnswerWith(callee.get()));
+  bool ExchangeOfferAnswerWith(PeerConnectionWrapper* answerer);
+
   // The following are wrappers for the underlying PeerConnection's
   // AddTransceiver method. They return the result of calling AddTransceiver
   // with the given arguments, DCHECKing if there is an error.
diff --git a/pc/rtptransceiver.cc b/pc/rtptransceiver.cc
index 2919ee7..61ebdc7 100644
--- a/pc/rtptransceiver.cc
+++ b/pc/rtptransceiver.cc
@@ -12,8 +12,14 @@
 
 #include <string>
 
+#include "pc/rtpmediautils.h"
+
 namespace webrtc {
 
+std::ostream& operator<<(std::ostream& os, RtpTransceiverDirection direction) {
+  return os << RtpTransceiverDirectionToString(direction);
+}
+
 RtpTransceiver::RtpTransceiver(cricket::MediaType media_type)
     : unified_plan_(false), media_type_(media_type) {
   RTC_DCHECK(media_type == cricket::MEDIA_TYPE_AUDIO ||
@@ -142,6 +148,13 @@
   return receivers_[0];
 }
 
+void RtpTransceiver::set_current_direction(RtpTransceiverDirection direction) {
+  current_direction_ = direction;
+  if (RtpTransceiverDirectionHasSend(*current_direction_)) {
+    has_ever_been_used_to_send_ = true;
+  }
+}
+
 bool RtpTransceiver::stopped() const {
   return stopped_;
 }
@@ -152,7 +165,7 @@
 
 void RtpTransceiver::SetDirection(RtpTransceiverDirection new_direction) {
   // TODO(steveanton): This should fire OnNegotiationNeeded.
-  direction_ = new_direction;
+  set_direction(new_direction);
 }
 
 rtc::Optional<RtpTransceiverDirection> RtpTransceiver::current_direction()
diff --git a/pc/rtptransceiver.h b/pc/rtptransceiver.h
index 19f393f..9e8565b 100644
--- a/pc/rtptransceiver.h
+++ b/pc/rtptransceiver.h
@@ -115,6 +115,33 @@
   // Returns the backing object for the transceiver's Unified Plan receiver.
   rtc::scoped_refptr<RtpReceiverInternal> receiver_internal() const;
 
+  // RtpTransceivers are not associated until they have a corresponding media
+  // section set in SetLocalDescription or SetRemoteDescription. Therefore,
+  // when setting a local offer we need a way to remember which transceiver was
+  // used to create which media section in the offer. Storing the mline index
+  // in CreateOffer is specified in JSEP to allow us to do that.
+  rtc::Optional<size_t> mline_index() const { return mline_index_; }
+  void set_mline_index(rtc::Optional<size_t> mline_index) {
+    mline_index_ = mline_index;
+  }
+
+  // Sets the MID for this transceiver. If the MID is not null, then the
+  // transceiver is considered "associated" with the media section that has the
+  // same MID.
+  void set_mid(const rtc::Optional<std::string>& mid) { mid_ = mid; }
+
+  // Sets the intended direction for this transceiver. Intended to be used
+  // internally over SetDirection since this does not trigger a negotiation
+  // needed callback.
+  void set_direction(RtpTransceiverDirection direction) {
+    direction_ = direction;
+  }
+
+  // Sets the current direction for this transceiver as negotiated in an offer/
+  // answer exchange. The current direction is null before an answer with this
+  // transceiver has been set.
+  void set_current_direction(RtpTransceiverDirection direction);
+
   // According to JSEP rules for SetRemoteDescription, RtpTransceivers can be
   // reused only if they were added by AddTrack.
   void set_created_by_addtrack(bool created_by_addtrack) {
@@ -152,9 +179,8 @@
   RtpTransceiverDirection direction_ = RtpTransceiverDirection::kInactive;
   rtc::Optional<RtpTransceiverDirection> current_direction_;
   rtc::Optional<std::string> mid_;
+  rtc::Optional<size_t> mline_index_;
   bool created_by_addtrack_ = false;
-  // TODO(steveanton): Implement this once there is a mechanism to set the
-  // current direction.
   bool has_ever_been_used_to_send_ = false;
 
   cricket::BaseChannel* channel_ = nullptr;