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_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