Create the JsepTransportController and JsepTransport2.

JsepTransportController process the entire SDP and  handle the RTCP-mux,
SRTP setup, BUNDLE related logic internally. This will replace the current
TransportController.

JsepTransport2 is used by the JsepTransportController which processes the
transport part of SDP and owns the DtlsTransport created internally.
JsepTransport2 will replace JsepTransport and be renamed eventually.

Bug: webrtc:8587
Change-Id: Ib02dfa52fe9b7a5b8b132afcc8e4363eb8bd9cf4
Reviewed-on: https://webrtc-review.googlesource.com/48841
Commit-Queue: Zhi Huang <zhihuang@webrtc.org>
Reviewed-by: Taylor Brandstetter <deadbeef@webrtc.org>
Reviewed-by: Peter Thatcher <pthatcher@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#22164}
diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn
index c155289..9e3655d 100644
--- a/p2p/BUILD.gn
+++ b/p2p/BUILD.gn
@@ -66,6 +66,7 @@
     "base/transportdescription.h",
     "base/transportdescriptionfactory.cc",
     "base/transportdescriptionfactory.h",
+    "base/transportfactoryinterface.h",
     "base/transportinfo.h",
     "base/turnport.cc",
     "base/turnport.h",
diff --git a/p2p/base/dtlstransport.cc b/p2p/base/dtlstransport.cc
index 0e7bd60..a576a1b 100644
--- a/p2p/base/dtlstransport.cc
+++ b/p2p/base/dtlstransport.cc
@@ -121,20 +121,26 @@
       ice_transport_(ice_transport),
       downward_(NULL),
       srtp_ciphers_(GetSupportedDtlsSrtpCryptoSuites(crypto_options)),
-      ssl_role_(rtc::SSL_CLIENT),
       ssl_max_version_(rtc::SSL_PROTOCOL_DTLS_12),
       crypto_options_(crypto_options) {
   RTC_DCHECK(ice_transport_);
-  ice_transport_->SignalWritableState.connect(this,
-                                              &DtlsTransport::OnWritableState);
-  ice_transport_->SignalReadPacket.connect(this, &DtlsTransport::OnReadPacket);
-  ice_transport_->SignalSentPacket.connect(this, &DtlsTransport::OnSentPacket);
-  ice_transport_->SignalReadyToSend.connect(this,
-                                            &DtlsTransport::OnReadyToSend);
-  ice_transport_->SignalReceivingState.connect(
-      this, &DtlsTransport::OnReceivingState);
-  ice_transport_->SignalNetworkRouteChanged.connect(
-      this, &DtlsTransport::OnNetworkRouteChanged);
+  ConnectToIceTransport();
+}
+
+DtlsTransport::DtlsTransport(
+    std::unique_ptr<IceTransportInternal> ice_transport,
+    const rtc::CryptoOptions& crypto_options)
+    : transport_name_(ice_transport->transport_name()),
+      component_(ice_transport->component()),
+      network_thread_(rtc::Thread::Current()),
+      ice_transport_(ice_transport.get()),
+      owned_ice_transport_(std::move(ice_transport)),
+      downward_(NULL),
+      srtp_ciphers_(GetSupportedDtlsSrtpCryptoSuites(crypto_options)),
+      ssl_max_version_(rtc::SSL_PROTOCOL_DTLS_12),
+      crypto_options_(crypto_options) {
+  RTC_DCHECK(owned_ice_transport_);
+  ConnectToIceTransport();
 }
 
 DtlsTransport::~DtlsTransport() = default;
@@ -198,9 +204,10 @@
   return true;
 }
 
-bool DtlsTransport::SetSslRole(rtc::SSLRole role) {
+bool DtlsTransport::SetDtlsRole(rtc::SSLRole role) {
   if (dtls_) {
-    if (ssl_role_ != role) {
+    RTC_DCHECK(dtls_role_);
+    if (*dtls_role_ != role) {
       RTC_LOG(LS_ERROR)
           << "SSL Role can't be reversed after the session is setup.";
       return false;
@@ -208,12 +215,15 @@
     return true;
   }
 
-  ssl_role_ = role;
+  dtls_role_ = std::move(role);
   return true;
 }
 
-bool DtlsTransport::GetSslRole(rtc::SSLRole* role) const {
-  *role = ssl_role_;
+bool DtlsTransport::GetDtlsRole(rtc::SSLRole* role) const {
+  if (!dtls_role_) {
+    return false;
+  }
+  *role = *dtls_role_;
   return true;
 }
 
@@ -330,6 +340,7 @@
 }
 
 bool DtlsTransport::SetupDtls() {
+  RTC_DCHECK(dtls_role_);
   StreamInterfaceChannel* downward = new StreamInterfaceChannel(ice_transport_);
 
   dtls_.reset(rtc::SSLStreamAdapter::Create(downward));
@@ -344,7 +355,7 @@
   dtls_->SetIdentity(local_certificate_->identity()->GetReference());
   dtls_->SetMode(rtc::SSL_MODE_DTLS);
   dtls_->SetMaxProtocolVersion(ssl_max_version_);
-  dtls_->SetServerRole(ssl_role_);
+  dtls_->SetServerRole(*dtls_role_);
   dtls_->SignalEvent.connect(this, &DtlsTransport::OnDtlsEvent);
   dtls_->SignalSSLHandshakeError.connect(this,
                                          &DtlsTransport::OnDtlsHandshakeError);
@@ -456,6 +467,20 @@
   return ice_transport_->SetOption(opt, value);
 }
 
+void DtlsTransport::ConnectToIceTransport() {
+  RTC_DCHECK(ice_transport_);
+  ice_transport_->SignalWritableState.connect(this,
+                                              &DtlsTransport::OnWritableState);
+  ice_transport_->SignalReadPacket.connect(this, &DtlsTransport::OnReadPacket);
+  ice_transport_->SignalSentPacket.connect(this, &DtlsTransport::OnSentPacket);
+  ice_transport_->SignalReadyToSend.connect(this,
+                                            &DtlsTransport::OnReadyToSend);
+  ice_transport_->SignalReceivingState.connect(
+      this, &DtlsTransport::OnReceivingState);
+  ice_transport_->SignalNetworkRouteChanged.connect(
+      this, &DtlsTransport::OnNetworkRouteChanged);
+}
+
 // The state transition logic here is as follows:
 // (1) If we're not doing DTLS-SRTP, then the state is just the
 //     state of the underlying impl()
@@ -543,7 +568,7 @@
         // the peer has chosen the client role, and proceed with the handshake.
         // The fingerprint will be verified when it's set.
         if (!dtls_ && local_certificate_) {
-          SetSslRole(rtc::SSL_SERVER);
+          SetDtlsRole(rtc::SSL_SERVER);
           SetupDtls();
         }
       } else {
@@ -677,7 +702,7 @@
     // Now that the handshake has started, we can process a cached ClientHello
     // (if one exists).
     if (cached_client_hello_.size()) {
-      if (ssl_role_ == rtc::SSL_SERVER) {
+      if (*dtls_role_ == rtc::SSL_SERVER) {
         LOG_J(LS_INFO, this) << "Handling cached DTLS ClientHello packet.";
         if (!HandleDtlsPacket(cached_client_hello_.data<char>(),
                               cached_client_hello_.size())) {
diff --git a/p2p/base/dtlstransport.h b/p2p/base/dtlstransport.h
index a4f4bcd..da0e7d4 100644
--- a/p2p/base/dtlstransport.h
+++ b/p2p/base/dtlstransport.h
@@ -89,8 +89,12 @@
   //
   // |crypto_options| are the options used for the DTLS handshake. This affects
   // whether GCM crypto suites are negotiated.
+  // TODO(zhihuang): Remove this once we switch to JsepTransportController.
   explicit DtlsTransport(IceTransportInternal* ice_transport,
                          const rtc::CryptoOptions& crypto_options);
+  explicit DtlsTransport(std::unique_ptr<IceTransportInternal> ice_transport,
+                         const rtc::CryptoOptions& crypto_options);
+
   ~DtlsTransport() override;
 
   const rtc::CryptoOptions& crypto_options() const override;
@@ -114,7 +118,7 @@
   rtc::scoped_refptr<rtc::RTCCertificate> GetLocalCertificate() const override;
 
   // SetRemoteFingerprint must be called after SetLocalCertificate, and any
-  // other methods like SetSslRole. It's what triggers the actual DTLS setup.
+  // other methods like SetDtlsRole. It's what triggers the actual DTLS setup.
   // TODO(deadbeef): Rename to "Start" like in ORTC?
   bool SetRemoteFingerprint(const std::string& digest_alg,
                             const uint8_t* digest,
@@ -128,13 +132,13 @@
 
   bool GetOption(rtc::Socket::Option opt, int* value) override;
 
-  virtual bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version);
+  bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) override;
 
   // Find out which DTLS-SRTP cipher was negotiated
   bool GetSrtpCryptoSuite(int* cipher) override;
 
-  bool GetSslRole(rtc::SSLRole* role) const override;
-  bool SetSslRole(rtc::SSLRole role) override;
+  bool GetDtlsRole(rtc::SSLRole* role) const override;
+  bool SetDtlsRole(rtc::SSLRole role) override;
 
   // Find out which DTLS cipher was negotiated
   bool GetSslCipherSuite(int* cipher) override;
@@ -186,6 +190,8 @@
   }
 
  private:
+  void ConnectToIceTransport();
+
   void OnWritableState(rtc::PacketTransportInternal* transport);
   void OnReadPacket(rtc::PacketTransportInternal* transport,
                     const char* data,
@@ -215,13 +221,14 @@
   rtc::Thread* network_thread_;  // Everything should occur on this thread.
   // Underlying ice_transport, not owned by this class.
   IceTransportInternal* const ice_transport_;
+  std::unique_ptr<IceTransportInternal> owned_ice_transport_;
   std::unique_ptr<rtc::SSLStreamAdapter> dtls_;  // The DTLS stream
   StreamInterfaceChannel*
       downward_;  // Wrapper for ice_transport_, owned by dtls_.
   std::vector<int> srtp_ciphers_;  // SRTP ciphers to use with DTLS.
   bool dtls_active_ = false;
   rtc::scoped_refptr<rtc::RTCCertificate> local_certificate_;
-  rtc::SSLRole ssl_role_;
+  rtc::Optional<rtc::SSLRole> dtls_role_;
   rtc::SSLProtocolVersion ssl_max_version_;
   rtc::CryptoOptions crypto_options_;
   rtc::Buffer remote_fingerprint_value_;
diff --git a/p2p/base/dtlstransport_unittest.cc b/p2p/base/dtlstransport_unittest.cc
index 430b41d..d77f77c 100644
--- a/p2p/base/dtlstransport_unittest.cc
+++ b/p2p/base/dtlstransport_unittest.cc
@@ -353,10 +353,10 @@
   void Negotiate(bool client1_server = true) {
     client1_.SetupTransports(ICEROLE_CONTROLLING);
     client2_.SetupTransports(ICEROLE_CONTROLLED);
-    client1_.dtls_transport()->SetSslRole(client1_server ? rtc::SSL_SERVER
-                                                         : rtc::SSL_CLIENT);
-    client2_.dtls_transport()->SetSslRole(client1_server ? rtc::SSL_CLIENT
-                                                         : rtc::SSL_SERVER);
+    client1_.dtls_transport()->SetDtlsRole(client1_server ? rtc::SSL_SERVER
+                                                          : rtc::SSL_CLIENT);
+    client2_.dtls_transport()->SetDtlsRole(client1_server ? rtc::SSL_CLIENT
+                                                          : rtc::SSL_SERVER);
     if (client2_.certificate()) {
       SetRemoteFingerprintFromCert(client1_.dtls_transport(),
                                    client2_.certificate());
@@ -627,8 +627,8 @@
     client1_.SetupTransports(ICEROLE_CONTROLLING, simulated_delay_ms);
     client2_.SetupTransports(ICEROLE_CONTROLLED, simulated_delay_ms);
     // Similar to how NegotiateOrdering works.
-    client1_.dtls_transport()->SetSslRole(rtc::SSL_SERVER);
-    client2_.dtls_transport()->SetSslRole(rtc::SSL_CLIENT);
+    client1_.dtls_transport()->SetDtlsRole(rtc::SSL_SERVER);
+    client2_.dtls_transport()->SetDtlsRole(rtc::SSL_CLIENT);
     SetRemoteFingerprintFromCert(client2_.dtls_transport(),
                                  client1_.certificate());
 
diff --git a/p2p/base/dtlstransportinternal.h b/p2p/base/dtlstransportinternal.h
index 1af32ac..e100305 100644
--- a/p2p/base/dtlstransportinternal.h
+++ b/p2p/base/dtlstransportinternal.h
@@ -59,9 +59,9 @@
 
   virtual bool IsDtlsActive() const = 0;
 
-  virtual bool GetSslRole(rtc::SSLRole* role) const = 0;
+  virtual bool GetDtlsRole(rtc::SSLRole* role) const = 0;
 
-  virtual bool SetSslRole(rtc::SSLRole role) = 0;
+  virtual bool SetDtlsRole(rtc::SSLRole role) = 0;
 
   // Finds out which DTLS-SRTP cipher was negotiated.
   // TODO(zhihuang): Remove this once all dependencies implement this.
@@ -98,6 +98,8 @@
                                     const uint8_t* digest,
                                     size_t digest_len) = 0;
 
+  virtual bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) = 0;
+
   // Expose the underneath IceTransport.
   virtual IceTransportInternal* ice_transport() = 0;
 
diff --git a/p2p/base/fakedtlstransport.h b/p2p/base/fakedtlstransport.h
index 391486c..770f7c9 100644
--- a/p2p/base/fakedtlstransport.h
+++ b/p2p/base/fakedtlstransport.h
@@ -13,11 +13,13 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "p2p/base/dtlstransportinternal.h"
 #include "p2p/base/fakeicetransport.h"
 #include "rtc_base/fakesslidentity.h"
+#include "rtc_base/ptr_util.h"
 
 namespace cricket {
 
@@ -38,10 +40,8 @@
         this, &FakeDtlsTransport::OnNetworkRouteChanged);
   }
 
-  // If this constructor is called, a new fake ICE transport will be created,
-  // and this FakeDtlsTransport will take the ownership.
-  explicit FakeDtlsTransport(const std::string& name, int component)
-      : owned_ice_transport_(new FakeIceTransport(name, component)),
+  explicit FakeDtlsTransport(std::unique_ptr<FakeIceTransport> ice)
+      : owned_ice_transport_(std::move(ice)),
         transport_name_(owned_ice_transport_->transport_name()),
         component_(owned_ice_transport_->component()),
         dtls_fingerprint_("", nullptr, 0) {
@@ -52,6 +52,11 @@
         this, &FakeDtlsTransport::OnNetworkRouteChanged);
   }
 
+  // If this constructor is called, a new fake ICE transport will be created,
+  // and this FakeDtlsTransport will take the ownership.
+  explicit FakeDtlsTransport(const std::string& name, int component)
+      : FakeDtlsTransport(rtc::MakeUnique<FakeIceTransport>(name, component)) {}
+
   ~FakeDtlsTransport() override {
     if (dest_ && dest_->dest_ == this) {
       dest_->dest_ = nullptr;
@@ -101,6 +106,10 @@
         dest->SetDestination(this, true);
       }
       dtls_state_ = DTLS_TRANSPORT_CONNECTED;
+      // If the |dtls_role_| is unset, set it to SSL_CLIENT by default.
+      if (!dtls_role_) {
+        dtls_role_ = std::move(rtc::SSL_CLIENT);
+      }
       SignalDtlsState(this, dtls_state_);
       ice_transport_->SetDestination(
           static_cast<FakeIceTransport*>(dest->ice_transport()), asymmetric);
@@ -125,12 +134,18 @@
     dtls_fingerprint_ = rtc::SSLFingerprint(alg, digest, digest_len);
     return true;
   }
-  bool SetSslRole(rtc::SSLRole role) override {
-    ssl_role_ = role;
+  bool SetSslMaxProtocolVersion(rtc::SSLProtocolVersion version) override {
     return true;
   }
-  bool GetSslRole(rtc::SSLRole* role) const override {
-    *role = ssl_role_;
+  bool SetDtlsRole(rtc::SSLRole role) override {
+    dtls_role_ = std::move(role);
+    return true;
+  }
+  bool GetDtlsRole(rtc::SSLRole* role) const override {
+    if (!dtls_role_) {
+      return false;
+    }
+    *role = *dtls_role_;
     return true;
   }
   const rtc::CryptoOptions& crypto_options() const override {
@@ -261,7 +276,7 @@
   bool do_dtls_ = false;
   rtc::SSLProtocolVersion ssl_max_version_ = rtc::SSL_PROTOCOL_DTLS_12;
   rtc::SSLFingerprint dtls_fingerprint_;
-  rtc::SSLRole ssl_role_ = rtc::SSL_CLIENT;
+  rtc::Optional<rtc::SSLRole> dtls_role_;
   int crypto_suite_ = rtc::SRTP_AES128_CM_SHA1_80;
   rtc::CryptoOptions crypto_options_;
 
diff --git a/p2p/base/fakeicetransport.h b/p2p/base/fakeicetransport.h
index 427481e..1a569c6 100644
--- a/p2p/base/fakeicetransport.h
+++ b/p2p/base/fakeicetransport.h
@@ -144,7 +144,16 @@
   void AddRemoteCandidate(const Candidate& candidate) override {
     remote_candidates_.push_back(candidate);
   }
-  void RemoveRemoteCandidate(const Candidate& candidate) override {}
+  void RemoveRemoteCandidate(const Candidate& candidate) override {
+    auto it = std::find(remote_candidates_.begin(), remote_candidates_.end(),
+                        candidate);
+    if (it == remote_candidates_.end()) {
+      RTC_LOG(LS_INFO) << "Trying to remove a candidate which doesn't exist.";
+      return;
+    }
+
+    remote_candidates_.erase(it);
+  }
 
   bool GetStats(ConnectionInfos* candidate_pair_stats_list,
                 CandidateStatsList* candidate_stats_list) override {
diff --git a/p2p/base/transportfactoryinterface.h b/p2p/base/transportfactoryinterface.h
new file mode 100644
index 0000000..ce32ee8
--- /dev/null
+++ b/p2p/base/transportfactoryinterface.h
@@ -0,0 +1,42 @@
+/*
+ *  Copyright 2018 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.
+ */
+
+#ifndef P2P_BASE_TRANSPORTFACTORYINTERFACE_H_
+#define P2P_BASE_TRANSPORTFACTORYINTERFACE_H_
+
+#include <memory>
+#include <string>
+
+#include "p2p/base/dtlstransportinternal.h"
+#include "p2p/base/icetransportinternal.h"
+
+namespace cricket {
+
+// This interface is used to create DTLS/ICE transports. The external transports
+// can be injected into the JsepTransportController through it. For example, the
+// FakeIceTransport/FakeDtlsTransport can be injected by setting a
+// FakeTransportFactory which implements this interface to the
+// JsepTransportController.
+class TransportFactoryInterface {
+ public:
+  virtual ~TransportFactoryInterface() {}
+
+  virtual std::unique_ptr<IceTransportInternal> CreateIceTransport(
+      const std::string& transport_name,
+      int component) = 0;
+
+  virtual std::unique_ptr<DtlsTransportInternal> CreateDtlsTransport(
+      std::unique_ptr<IceTransportInternal> ice,
+      const rtc::CryptoOptions& crypto_options) = 0;
+};
+
+}  // namespace cricket
+
+#endif  // P2P_BASE_TRANSPORTFACTORYINTERFACE_H_
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index d536109..c79a2ad 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -44,6 +44,10 @@
     "externalhmac.h",
     "jseptransport.cc",
     "jseptransport.h",
+    "jseptransport2.cc",
+    "jseptransport2.h",
+    "jseptransportcontroller.cc",
+    "jseptransportcontroller.h",
     "mediasession.cc",
     "mediasession.h",
     "rtcpmuxfilter.cc",
@@ -64,6 +68,8 @@
     "srtptransport.h",
     "transportcontroller.cc",
     "transportcontroller.h",
+    "transportstats.cc",
+    "transportstats.h",
   ]
 
   deps = [
@@ -270,7 +276,9 @@
       "channelmanager_unittest.cc",
       "currentspeakermonitor_unittest.cc",
       "dtlssrtptransport_unittest.cc",
+      "jseptransport2_unittest.cc",
       "jseptransport_unittest.cc",
+      "jseptransportcontroller_unittest.cc",
       "mediasession_unittest.cc",
       "rtcpmuxfilter_unittest.cc",
       "rtptransport_unittest.cc",
diff --git a/pc/dtlssrtptransport.cc b/pc/dtlssrtptransport.cc
index f6cbf4d..ff1bc0b 100644
--- a/pc/dtlssrtptransport.cc
+++ b/pc/dtlssrtptransport.cc
@@ -263,8 +263,8 @@
   memcpy(&server_write_key[key_len], &dtls_buffer[offset], salt_len);
 
   rtc::SSLRole role;
-  if (!dtls_transport->GetSslRole(&role)) {
-    RTC_LOG(LS_WARNING) << "GetSslRole failed";
+  if (!dtls_transport->GetDtlsRole(&role)) {
+    RTC_LOG(LS_WARNING) << "Failed to get the DTLS role.";
     return false;
   }
 
diff --git a/pc/jseptransport.cc b/pc/jseptransport.cc
index a6a6135..0127d84 100644
--- a/pc/jseptransport.cc
+++ b/pc/jseptransport.cc
@@ -25,18 +25,8 @@
 
 namespace cricket {
 
-TransportChannelStats::TransportChannelStats() = default;
-
-TransportChannelStats::TransportChannelStats(const TransportChannelStats&) =
-    default;
-
-TransportChannelStats::~TransportChannelStats() = default;
-
-TransportStats::TransportStats() = default;
-
-TransportStats::~TransportStats() = default;
-
-bool BadTransportDescription(const std::string& desc, std::string* err_desc) {
+static bool BadTransportDescription(const std::string& desc,
+                                    std::string* err_desc) {
   if (err_desc) {
     *err_desc = desc;
   }
@@ -289,7 +279,7 @@
     std::string* error_desc) {
   // Set SSL role. Role must be set before fingerprint is applied, which
   // initiates DTLS setup.
-  if (ssl_role_ && !dtls_transport->SetSslRole(*ssl_role_)) {
+  if (ssl_role_ && !dtls_transport->SetDtlsRole(*ssl_role_)) {
     return BadTransportDescription("Failed to set SSL role for the channel.",
                                    error_desc);
   }
diff --git a/pc/jseptransport.h b/pc/jseptransport.h
index 719ff7a..4a8aec8 100644
--- a/pc/jseptransport.h
+++ b/pc/jseptransport.h
@@ -23,6 +23,7 @@
 #include "p2p/base/p2pconstants.h"
 #include "p2p/base/transportinfo.h"
 #include "pc/sessiondescription.h"
+#include "pc/transportstats.h"
 #include "rtc_base/constructormagic.h"
 #include "rtc_base/messagequeue.h"
 #include "rtc_base/rtccertificate.h"
@@ -33,34 +34,6 @@
 
 class DtlsTransportInternal;
 
-struct TransportChannelStats {
-  TransportChannelStats();
-  TransportChannelStats(const TransportChannelStats&);
-  ~TransportChannelStats();
-
-  int component = 0;
-  CandidateStatsList candidate_stats_list;
-  ConnectionInfos connection_infos;
-  int srtp_crypto_suite = rtc::SRTP_INVALID_CRYPTO_SUITE;
-  int ssl_cipher_suite = rtc::TLS_NULL_WITH_NULL_NULL;
-  DtlsTransportState dtls_state = DTLS_TRANSPORT_NEW;
-};
-
-// Information about all the channels of a transport.
-// TODO(hta): Consider if a simple vector is as good as a map.
-typedef std::vector<TransportChannelStats> TransportChannelStatsList;
-
-// Information about the stats of a transport.
-struct TransportStats {
-  TransportStats();
-  ~TransportStats();
-
-  std::string transport_name;
-  TransportChannelStatsList channel_stats;
-};
-
-bool BadTransportDescription(const std::string& desc, std::string* err_desc);
-
 // Helper class used by TransportController that processes
 // TransportDescriptions. A TransportDescription represents the
 // transport-specific properties of an SDP m= section, processed according to
diff --git a/pc/jseptransport2.cc b/pc/jseptransport2.cc
new file mode 100644
index 0000000..11f9438
--- /dev/null
+++ b/pc/jseptransport2.cc
@@ -0,0 +1,617 @@
+/*
+ *  Copyright 2018 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 "pc/jseptransport2.h"
+
+#include <memory>
+#include <utility>  // for std::pair
+
+#include "api/candidate.h"
+#include "p2p/base/p2pconstants.h"
+#include "p2p/base/p2ptransportchannel.h"
+#include "p2p/base/port.h"
+#include "rtc_base/bind.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/ptr_util.h"
+
+using webrtc::SdpType;
+
+namespace cricket {
+
+static bool VerifyIceParams(const JsepTransportDescription& jsep_description) {
+  // For legacy protocols.
+  // TODO(zhihuang): Remove this once the legacy protocol is no longer
+  // supported.
+  if (jsep_description.transport_desc.ice_ufrag.empty() &&
+      jsep_description.transport_desc.ice_pwd.empty()) {
+    return true;
+  }
+
+  if (jsep_description.transport_desc.ice_ufrag.length() <
+          ICE_UFRAG_MIN_LENGTH ||
+      jsep_description.transport_desc.ice_ufrag.length() >
+          ICE_UFRAG_MAX_LENGTH) {
+    return false;
+  }
+  if (jsep_description.transport_desc.ice_pwd.length() < ICE_PWD_MIN_LENGTH ||
+      jsep_description.transport_desc.ice_pwd.length() > ICE_PWD_MAX_LENGTH) {
+    return false;
+  }
+  return true;
+}
+
+JsepTransportDescription::JsepTransportDescription() {}
+
+JsepTransportDescription::JsepTransportDescription(
+    bool rtcp_mux_enabled,
+    const std::vector<CryptoParams>& cryptos,
+    const std::vector<int>& encrypted_header_extension_ids,
+    const TransportDescription& transport_desc)
+    : rtcp_mux_enabled(rtcp_mux_enabled),
+      cryptos(cryptos),
+      encrypted_header_extension_ids(encrypted_header_extension_ids),
+      transport_desc(transport_desc) {}
+
+JsepTransportDescription::JsepTransportDescription(
+    const JsepTransportDescription& from)
+    : rtcp_mux_enabled(from.rtcp_mux_enabled),
+      cryptos(from.cryptos),
+      encrypted_header_extension_ids(from.encrypted_header_extension_ids),
+      transport_desc(from.transport_desc) {}
+
+JsepTransportDescription::~JsepTransportDescription() = default;
+
+JsepTransportDescription& JsepTransportDescription::operator=(
+    const JsepTransportDescription& from) {
+  if (this == &from) {
+    return *this;
+  }
+  rtcp_mux_enabled = from.rtcp_mux_enabled;
+  cryptos = from.cryptos;
+  encrypted_header_extension_ids = from.encrypted_header_extension_ids;
+  transport_desc = from.transport_desc;
+
+  return *this;
+}
+
+JsepTransport2::JsepTransport2(
+    const std::string& mid,
+    const rtc::scoped_refptr<rtc::RTCCertificate>& local_certificate,
+    std::unique_ptr<webrtc::RtpTransport> unencrypted_rtp_transport,
+    std::unique_ptr<webrtc::SrtpTransport> sdes_transport,
+    std::unique_ptr<webrtc::DtlsSrtpTransport> dtls_srtp_transport,
+    std::unique_ptr<DtlsTransportInternal> rtp_dtls_transport,
+    std::unique_ptr<DtlsTransportInternal> rtcp_dtls_transport)
+    : mid_(mid),
+      local_certificate_(local_certificate),
+      rtp_dtls_transport_(std::move(rtp_dtls_transport)),
+      rtcp_dtls_transport_(std::move(rtcp_dtls_transport)) {
+  RTC_DCHECK(rtp_dtls_transport_);
+  if (unencrypted_rtp_transport) {
+    RTC_DCHECK(!sdes_transport);
+    RTC_DCHECK(!dtls_srtp_transport);
+    unencrypted_rtp_transport_ = std::move(unencrypted_rtp_transport);
+  } else if (sdes_transport) {
+    RTC_DCHECK(!unencrypted_rtp_transport);
+    RTC_DCHECK(!dtls_srtp_transport);
+    sdes_transport_ = std::move(sdes_transport);
+  } else {
+    RTC_DCHECK(dtls_srtp_transport);
+    RTC_DCHECK(!unencrypted_rtp_transport);
+    RTC_DCHECK(!sdes_transport);
+    dtls_srtp_transport_ = std::move(dtls_srtp_transport);
+  }
+}
+
+JsepTransport2::~JsepTransport2() {}
+
+webrtc::RTCError JsepTransport2::SetLocalJsepTransportDescription(
+    const JsepTransportDescription& jsep_description,
+    SdpType type) {
+  webrtc::RTCError error;
+
+  if (!VerifyIceParams(jsep_description)) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Invalid ice-ufrag or ice-pwd length.");
+  }
+
+  if (!SetRtcpMux(jsep_description.rtcp_mux_enabled, type,
+                  ContentSource::CS_LOCAL)) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Failed to setup RTCP mux.");
+  }
+
+  // If doing SDES, setup the SDES crypto parameters.
+  if (sdes_transport_) {
+    RTC_DCHECK(!unencrypted_rtp_transport_);
+    RTC_DCHECK(!dtls_srtp_transport_);
+    if (!SetSdes(jsep_description.cryptos,
+                 jsep_description.encrypted_header_extension_ids, type,
+                 ContentSource::CS_LOCAL)) {
+      return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                              "Failed to setup SDES crypto parameters.");
+    }
+  } else if (dtls_srtp_transport_) {
+    RTC_DCHECK(!unencrypted_rtp_transport_);
+    RTC_DCHECK(!sdes_transport_);
+    dtls_srtp_transport_->UpdateRecvEncryptedHeaderExtensionIds(
+        jsep_description.encrypted_header_extension_ids);
+  }
+
+  bool ice_restarting =
+      local_description_ != nullptr &&
+      IceCredentialsChanged(local_description_->transport_desc.ice_ufrag,
+                            local_description_->transport_desc.ice_pwd,
+                            jsep_description.transport_desc.ice_ufrag,
+                            jsep_description.transport_desc.ice_pwd);
+  local_description_.reset(new JsepTransportDescription(jsep_description));
+
+  rtc::SSLFingerprint* local_fp =
+      local_description_->transport_desc.identity_fingerprint.get();
+
+  if (!local_fp) {
+    local_certificate_ = nullptr;
+  } else {
+    error = VerifyCertificateFingerprint(local_certificate_.get(), local_fp);
+    if (!error.ok()) {
+      local_description_.reset();
+      return error;
+    }
+  }
+
+  SetLocalIceParameters(rtp_dtls_transport_->ice_transport());
+
+  if (rtcp_dtls_transport_) {
+    SetLocalIceParameters(rtcp_dtls_transport_->ice_transport());
+  }
+
+  // If PRANSWER/ANSWER is set, we should decide transport protocol type.
+  if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
+    error = NegotiateAndSetDtlsParameters(type);
+  }
+  if (!error.ok()) {
+    local_description_.reset();
+    return error;
+  }
+
+  if (needs_ice_restart_ && ice_restarting) {
+    needs_ice_restart_ = false;
+    RTC_LOG(LS_VERBOSE) << "needs-ice-restart flag cleared for transport "
+                        << mid();
+  }
+
+  return webrtc::RTCError::OK();
+}
+
+webrtc::RTCError JsepTransport2::SetRemoteJsepTransportDescription(
+    const JsepTransportDescription& jsep_description,
+    webrtc::SdpType type) {
+  webrtc::RTCError error;
+
+  if (!VerifyIceParams(jsep_description)) {
+    remote_description_.reset();
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Invalid ice-ufrag or ice-pwd length.");
+  }
+
+  if (!SetRtcpMux(jsep_description.rtcp_mux_enabled, type,
+                  ContentSource::CS_REMOTE)) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Failed to setup RTCP mux.");
+  }
+
+  // If doing SDES, setup the SDES crypto parameters.
+  if (sdes_transport_) {
+    RTC_DCHECK(!unencrypted_rtp_transport_);
+    RTC_DCHECK(!dtls_srtp_transport_);
+    if (!SetSdes(jsep_description.cryptos,
+                 jsep_description.encrypted_header_extension_ids, type,
+                 ContentSource::CS_REMOTE)) {
+      return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                              "Failed to setup SDES crypto parameters.");
+    }
+  } else if (dtls_srtp_transport_) {
+    RTC_DCHECK(!unencrypted_rtp_transport_);
+    RTC_DCHECK(!sdes_transport_);
+    dtls_srtp_transport_->UpdateSendEncryptedHeaderExtensionIds(
+        jsep_description.encrypted_header_extension_ids);
+  }
+
+  remote_description_.reset(new JsepTransportDescription(jsep_description));
+  SetRemoteIceParameters(rtp_dtls_transport_->ice_transport());
+
+  if (rtcp_dtls_transport_) {
+    SetRemoteIceParameters(rtcp_dtls_transport_->ice_transport());
+  }
+
+  // If PRANSWER/ANSWER is set, we should decide transport protocol type.
+  if (type == SdpType::kPrAnswer || type == SdpType::kAnswer) {
+    error = NegotiateAndSetDtlsParameters(SdpType::kOffer);
+  }
+  if (!error.ok()) {
+    remote_description_.reset();
+    return error;
+  }
+  return webrtc::RTCError::OK();
+}
+
+webrtc::RTCError JsepTransport2::AddRemoteCandidates(
+    const Candidates& candidates) {
+  if (!local_description_ || !remote_description_) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_STATE,
+                            mid() +
+                                " is not ready to use the remote candidate "
+                                "because the local or remote description is "
+                                "not set.");
+  }
+
+  for (const cricket::Candidate& candidate : candidates) {
+    auto transport =
+        candidate.component() == cricket::ICE_CANDIDATE_COMPONENT_RTP
+            ? rtp_dtls_transport_.get()
+            : rtcp_dtls_transport_.get();
+    if (!transport) {
+      return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                              "Candidate has an unknown component: " +
+                                  candidate.ToString() + " for mid " + mid());
+    }
+    transport->ice_transport()->AddRemoteCandidate(candidate);
+  }
+  return webrtc::RTCError::OK();
+}
+
+void JsepTransport2::SetNeedsIceRestartFlag() {
+  if (!needs_ice_restart_) {
+    needs_ice_restart_ = true;
+    RTC_LOG(LS_VERBOSE) << "needs-ice-restart flag set for transport " << mid();
+  }
+}
+
+rtc::Optional<rtc::SSLRole> JsepTransport2::GetDtlsRole() const {
+  RTC_DCHECK(rtp_dtls_transport_);
+  rtc::SSLRole dtls_role;
+  if (!rtp_dtls_transport_->GetDtlsRole(&dtls_role)) {
+    return rtc::Optional<rtc::SSLRole>();
+  }
+
+  return rtc::Optional<rtc::SSLRole>(dtls_role);
+}
+
+bool JsepTransport2::GetStats(TransportStats* stats) {
+  stats->transport_name = mid();
+  stats->channel_stats.clear();
+  bool ret = GetTransportStats(rtp_dtls_transport_.get(), stats);
+  if (rtcp_dtls_transport_) {
+    ret &= GetTransportStats(rtcp_dtls_transport_.get(), stats);
+  }
+  return ret;
+}
+
+webrtc::RTCError JsepTransport2::VerifyCertificateFingerprint(
+    const rtc::RTCCertificate* certificate,
+    const rtc::SSLFingerprint* fingerprint) const {
+  if (!fingerprint) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "No fingerprint");
+  }
+  if (!certificate) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Fingerprint provided but no identity available.");
+  }
+  std::unique_ptr<rtc::SSLFingerprint> fp_tmp(rtc::SSLFingerprint::Create(
+      fingerprint->algorithm, certificate->identity()));
+  RTC_DCHECK(fp_tmp.get() != NULL);
+  if (*fp_tmp == *fingerprint) {
+    return webrtc::RTCError::OK();
+  }
+  std::ostringstream desc;
+  desc << "Local fingerprint does not match identity. Expected: ";
+  desc << fp_tmp->ToString();
+  desc << " Got: " << fingerprint->ToString();
+  return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER, desc.str());
+}
+
+void JsepTransport2::SetLocalIceParameters(
+    IceTransportInternal* ice_transport) {
+  RTC_DCHECK(ice_transport);
+  RTC_DCHECK(local_description_);
+  ice_transport->SetIceParameters(
+      local_description_->transport_desc.GetIceParameters());
+}
+
+void JsepTransport2::SetRemoteIceParameters(
+    IceTransportInternal* ice_transport) {
+  RTC_DCHECK(ice_transport);
+  RTC_DCHECK(remote_description_);
+  ice_transport->SetRemoteIceParameters(
+      remote_description_->transport_desc.GetIceParameters());
+  ice_transport->SetRemoteIceMode(remote_description_->transport_desc.ice_mode);
+}
+
+webrtc::RTCError JsepTransport2::SetNegotiatedDtlsParameters(
+    DtlsTransportInternal* dtls_transport,
+    rtc::Optional<rtc::SSLRole> dtls_role,
+    rtc::SSLFingerprint* remote_fingerprint) {
+  RTC_DCHECK(dtls_transport);
+  // Set SSL role. Role must be set before fingerprint is applied, which
+  // initiates DTLS setup.
+  if (dtls_role && !dtls_transport->SetDtlsRole(*dtls_role)) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Failed to set SSL role for the transport.");
+  }
+  // Apply remote fingerprint.
+  if (!remote_fingerprint ||
+      !dtls_transport->SetRemoteFingerprint(
+          remote_fingerprint->algorithm,
+          reinterpret_cast<const uint8_t*>(remote_fingerprint->digest.data()),
+          remote_fingerprint->digest.size())) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "Failed to apply remote fingerprint.");
+  }
+  return webrtc::RTCError::OK();
+}
+
+bool JsepTransport2::SetRtcpMux(bool enable,
+                                webrtc::SdpType type,
+                                ContentSource source) {
+  bool ret = false;
+  switch (type) {
+    case SdpType::kOffer:
+      ret = rtcp_mux_negotiator_.SetOffer(enable, source);
+      break;
+    case SdpType::kPrAnswer:
+      // This may activate RTCP muxing, but we don't yet destroy the transport
+      // because the final answer may deactivate it.
+      ret = rtcp_mux_negotiator_.SetProvisionalAnswer(enable, source);
+      break;
+    case SdpType::kAnswer:
+      ret = rtcp_mux_negotiator_.SetAnswer(enable, source);
+      if (ret && rtcp_mux_negotiator_.IsActive()) {
+        ActivateRtcpMux();
+      }
+      break;
+    default:
+      RTC_NOTREACHED();
+  }
+
+  if (!ret) {
+    return false;
+  }
+
+  auto transport = rtp_transport();
+  transport->SetRtcpMuxEnabled(rtcp_mux_negotiator_.IsActive());
+  return ret;
+}
+
+void JsepTransport2::ActivateRtcpMux() {
+  if (unencrypted_rtp_transport_) {
+    RTC_DCHECK(!sdes_transport_);
+    RTC_DCHECK(!dtls_srtp_transport_);
+    unencrypted_rtp_transport_->SetRtcpPacketTransport(nullptr);
+  } else if (sdes_transport_) {
+    RTC_DCHECK(!unencrypted_rtp_transport_);
+    RTC_DCHECK(!dtls_srtp_transport_);
+    sdes_transport_->SetRtcpPacketTransport(nullptr);
+  } else {
+    RTC_DCHECK(dtls_srtp_transport_);
+    RTC_DCHECK(!unencrypted_rtp_transport_);
+    RTC_DCHECK(!sdes_transport_);
+    dtls_srtp_transport_->SetDtlsTransports(rtp_dtls_transport(),
+                                            /*rtcp_dtls_transport=*/nullptr);
+  }
+  rtcp_dtls_transport_.reset();
+  // Notify the JsepTransportController to update the aggregate states.
+  SignalRtcpMuxActive();
+}
+
+bool JsepTransport2::SetSdes(const std::vector<CryptoParams>& cryptos,
+                             const std::vector<int>& encrypted_extension_ids,
+                             webrtc::SdpType type,
+                             ContentSource source) {
+  bool ret = false;
+  ret = sdes_negotiator_.Process(cryptos, type, source);
+  if (!ret) {
+    return ret;
+  }
+
+  if (source == ContentSource::CS_LOCAL) {
+    recv_extension_ids_ = std::move(encrypted_extension_ids);
+  } else {
+    send_extension_ids_ = std::move(encrypted_extension_ids);
+  }
+
+  // If setting an SDES answer succeeded, apply the negotiated parameters
+  // to the SRTP transport.
+  if ((type == SdpType::kPrAnswer || type == SdpType::kAnswer) && ret) {
+    if (sdes_negotiator_.send_cipher_suite() &&
+        sdes_negotiator_.recv_cipher_suite()) {
+      RTC_DCHECK(send_extension_ids_);
+      RTC_DCHECK(recv_extension_ids_);
+      ret = sdes_transport_->SetRtpParams(
+          *(sdes_negotiator_.send_cipher_suite()),
+          sdes_negotiator_.send_key().data(),
+          static_cast<int>(sdes_negotiator_.send_key().size()),
+          *(send_extension_ids_), *(sdes_negotiator_.recv_cipher_suite()),
+          sdes_negotiator_.recv_key().data(),
+          static_cast<int>(sdes_negotiator_.recv_key().size()),
+          *(recv_extension_ids_));
+    } else {
+      RTC_LOG(LS_INFO) << "No crypto keys are provided for SDES.";
+      if (type == SdpType::kAnswer) {
+        // Explicitly reset the |sdes_transport_| if no crypto param is
+        // provided in the answer. No need to call |ResetParams()| for
+        // |sdes_negotiator_| because it resets the params inside |SetAnswer|.
+        sdes_transport_->ResetParams();
+      }
+    }
+  }
+  return ret;
+}
+
+webrtc::RTCError JsepTransport2::NegotiateAndSetDtlsParameters(
+    SdpType local_description_type) {
+  if (!local_description_ || !remote_description_) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_STATE,
+                            "Applying an answer transport description "
+                            "without applying any offer.");
+  }
+  std::unique_ptr<rtc::SSLFingerprint> remote_fingerprint;
+  rtc::Optional<rtc::SSLRole> negotiated_dtls_role;
+
+  rtc::SSLFingerprint* local_fp =
+      local_description_->transport_desc.identity_fingerprint.get();
+  rtc::SSLFingerprint* remote_fp =
+      remote_description_->transport_desc.identity_fingerprint.get();
+  if (remote_fp && local_fp) {
+    remote_fingerprint = rtc::MakeUnique<rtc::SSLFingerprint>(*remote_fp);
+    webrtc::RTCError error =
+        NegotiateDtlsRole(local_description_type,
+                          local_description_->transport_desc.connection_role,
+                          remote_description_->transport_desc.connection_role,
+                          &negotiated_dtls_role);
+    if (!error.ok()) {
+      return error;
+    }
+  } else if (local_fp && (local_description_type == SdpType::kAnswer)) {
+    return webrtc::RTCError(
+        webrtc::RTCErrorType::INVALID_PARAMETER,
+        "Local fingerprint supplied when caller didn't offer DTLS.");
+  } else {
+    // We are not doing DTLS
+    remote_fingerprint = rtc::MakeUnique<rtc::SSLFingerprint>("", nullptr, 0);
+  }
+  // Now that we have negotiated everything, push it downward.
+  // Note that we cache the result so that if we have race conditions
+  // between future SetRemote/SetLocal invocations and new transport
+  // creation, we have the negotiation state saved until a new
+  // negotiation happens.
+  webrtc::RTCError error = SetNegotiatedDtlsParameters(
+      rtp_dtls_transport_.get(), negotiated_dtls_role,
+      remote_fingerprint.get());
+  if (!error.ok()) {
+    return error;
+  }
+
+  if (rtcp_dtls_transport_) {
+    error = SetNegotiatedDtlsParameters(rtcp_dtls_transport_.get(),
+                                        negotiated_dtls_role,
+                                        remote_fingerprint.get());
+  }
+  return error;
+}
+
+webrtc::RTCError JsepTransport2::NegotiateDtlsRole(
+    SdpType local_description_type,
+    ConnectionRole local_connection_role,
+    ConnectionRole remote_connection_role,
+    rtc::Optional<rtc::SSLRole>* negotiated_dtls_role) {
+  // From RFC 4145, section-4.1, The following are the values that the
+  // 'setup' attribute can take in an offer/answer exchange:
+  //       Offer      Answer
+  //      ________________
+  //      active     passive / holdconn
+  //      passive    active / holdconn
+  //      actpass    active / passive / holdconn
+  //      holdconn   holdconn
+  //
+  // Set the role that is most conformant with RFC 5763, Section 5, bullet 1
+  // The endpoint MUST use the setup attribute defined in [RFC4145].
+  // The endpoint that is the offerer MUST use the setup attribute
+  // value of setup:actpass and be prepared to receive a client_hello
+  // before it receives the answer.  The answerer MUST use either a
+  // setup attribute value of setup:active or setup:passive.  Note that
+  // if the answerer uses setup:passive, then the DTLS handshake will
+  // not begin until the answerer is received, which adds additional
+  // latency. setup:active allows the answer and the DTLS handshake to
+  // occur in parallel.  Thus, setup:active is RECOMMENDED.  Whichever
+  // party is active MUST initiate a DTLS handshake by sending a
+  // ClientHello over each flow (host/port quartet).
+  // IOW - actpass and passive modes should be treated as server and
+  // active as client.
+  bool is_remote_server = false;
+  if (local_description_type == SdpType::kOffer) {
+    if (local_connection_role != CONNECTIONROLE_ACTPASS) {
+      return webrtc::RTCError(
+          webrtc::RTCErrorType::INVALID_PARAMETER,
+          "Offerer must use actpass value for setup attribute.");
+    }
+
+    if (remote_connection_role == CONNECTIONROLE_ACTIVE ||
+        remote_connection_role == CONNECTIONROLE_PASSIVE ||
+        remote_connection_role == CONNECTIONROLE_NONE) {
+      is_remote_server = (remote_connection_role == CONNECTIONROLE_PASSIVE);
+    } else {
+      return webrtc::RTCError(
+          webrtc::RTCErrorType::INVALID_PARAMETER,
+          "Answerer must use either active or passive value "
+          "for setup attribute.");
+    }
+    // If remote is NONE or ACTIVE it will act as client.
+  } else {
+    if (remote_connection_role != CONNECTIONROLE_ACTPASS &&
+        remote_connection_role != CONNECTIONROLE_NONE) {
+      // Accept a remote role attribute that's not "actpass", but matches the
+      // current negotiated role. This is allowed by dtls-sdp, though our
+      // implementation will never generate such an offer as it's not
+      // recommended.
+      //
+      // See https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-dtls-sdp,
+      // section 5.5.
+      auto current_dtls_role = GetDtlsRole();
+      if (!current_dtls_role ||
+          (*current_dtls_role == rtc::SSL_CLIENT &&
+           remote_connection_role == CONNECTIONROLE_ACTIVE) ||
+          (*current_dtls_role == rtc::SSL_SERVER &&
+           remote_connection_role == CONNECTIONROLE_PASSIVE)) {
+        return webrtc::RTCError(
+            webrtc::RTCErrorType::INVALID_PARAMETER,
+            "Offerer must use actpass value or current negotiated role for "
+            "setup attribute.");
+      }
+    }
+
+    if (local_connection_role == CONNECTIONROLE_ACTIVE ||
+        local_connection_role == CONNECTIONROLE_PASSIVE) {
+      is_remote_server = (local_connection_role == CONNECTIONROLE_ACTIVE);
+    } else {
+      return webrtc::RTCError(
+          webrtc::RTCErrorType::INVALID_PARAMETER,
+          "Answerer must use either active or passive value "
+          "for setup attribute.");
+    }
+
+    // If local is passive, local will act as server.
+  }
+
+  *negotiated_dtls_role = (is_remote_server ? std::move(rtc::SSL_CLIENT)
+                                            : std::move(rtc::SSL_SERVER));
+  return webrtc::RTCError::OK();
+}
+
+bool JsepTransport2::GetTransportStats(DtlsTransportInternal* dtls_transport,
+                                       TransportStats* stats) {
+  RTC_DCHECK(dtls_transport);
+  TransportChannelStats substats;
+  substats.component = dtls_transport == rtcp_dtls_transport_.get()
+                           ? ICE_CANDIDATE_COMPONENT_RTCP
+                           : ICE_CANDIDATE_COMPONENT_RTP;
+  dtls_transport->GetSrtpCryptoSuite(&substats.srtp_crypto_suite);
+  dtls_transport->GetSslCipherSuite(&substats.ssl_cipher_suite);
+  substats.dtls_state = dtls_transport->dtls_state();
+  if (!dtls_transport->ice_transport()->GetStats(
+          &substats.connection_infos, &substats.candidate_stats_list)) {
+    return false;
+  }
+  stats->channel_stats.push_back(substats);
+  return true;
+}
+
+}  // namespace cricket
diff --git a/pc/jseptransport2.h b/pc/jseptransport2.h
new file mode 100644
index 0000000..16a20b9
--- /dev/null
+++ b/pc/jseptransport2.h
@@ -0,0 +1,245 @@
+/*
+ *  Copyright 2018 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.
+ */
+
+#ifndef PC_JSEPTRANSPORT2_H_
+#define PC_JSEPTRANSPORT2_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "api/candidate.h"
+#include "api/jsep.h"
+#include "api/optional.h"
+#include "p2p/base/dtlstransport.h"
+#include "p2p/base/p2pconstants.h"
+#include "p2p/base/transportinfo.h"
+#include "pc/dtlssrtptransport.h"
+#include "pc/rtcpmuxfilter.h"
+#include "pc/rtptransport.h"
+#include "pc/sessiondescription.h"
+#include "pc/srtpfilter.h"
+#include "pc/srtptransport.h"
+#include "pc/transportstats.h"
+#include "rtc_base/constructormagic.h"
+#include "rtc_base/messagequeue.h"
+#include "rtc_base/rtccertificate.h"
+#include "rtc_base/sigslot.h"
+#include "rtc_base/sslstreamadapter.h"
+
+namespace cricket {
+
+class DtlsTransportInternal;
+
+struct JsepTransportDescription {
+ public:
+  JsepTransportDescription();
+  JsepTransportDescription(
+      bool rtcp_mux_enabled,
+      const std::vector<CryptoParams>& cryptos,
+      const std::vector<int>& encrypted_header_extension_ids,
+      const TransportDescription& transport_description);
+  JsepTransportDescription(const JsepTransportDescription& from);
+  ~JsepTransportDescription();
+
+  JsepTransportDescription& operator=(const JsepTransportDescription& from);
+
+  bool rtcp_mux_enabled = true;
+  std::vector<CryptoParams> cryptos;
+  std::vector<int> encrypted_header_extension_ids;
+  // TODO(zhihuang): Add the ICE and DTLS related variables and methods from
+  // TransportDescription and remove this extra layer of abstraction.
+  TransportDescription transport_desc;
+};
+
+// Helper class used by JsepTransportController that processes
+// TransportDescriptions. A TransportDescription represents the
+// transport-specific properties of an SDP m= section, processed according to
+// JSEP. Each transport consists of DTLS and ICE transport channels for RTP
+// (and possibly RTCP, if rtcp-mux isn't used).
+//
+// On Threading:  JsepTransport performs work solely on the network thread, and
+// so its methods should only be called on the network thread.
+class JsepTransport2 : public sigslot::has_slots<> {
+ public:
+  // |mid| is just used for log statements in order to identify the Transport.
+  // Note that |local_certificate| is allowed to be null since a remote
+  // description may be set before a local certificate is generated.
+  JsepTransport2(
+      const std::string& mid,
+      const rtc::scoped_refptr<rtc::RTCCertificate>& local_certificate,
+      std::unique_ptr<webrtc::RtpTransport> unencrypted_rtp_transport,
+      std::unique_ptr<webrtc::SrtpTransport> sdes_transport,
+      std::unique_ptr<webrtc::DtlsSrtpTransport> dtls_srtp_transport,
+      std::unique_ptr<DtlsTransportInternal> rtp_dtls_transport,
+      std::unique_ptr<DtlsTransportInternal> rtcp_dtls_transport);
+
+  ~JsepTransport2() override;
+
+  // Returns the MID of this transport. This is only used for logging.
+  const std::string& mid() const { return mid_; }
+
+  // Must be called before applying local session description.
+  // Needed in order to verify the local fingerprint.
+  void SetLocalCertificate(
+      const rtc::scoped_refptr<rtc::RTCCertificate>& local_certificate) {
+    local_certificate_ = local_certificate;
+  }
+
+  // Return the local certificate provided by SetLocalCertificate.
+  rtc::scoped_refptr<rtc::RTCCertificate> GetLocalCertificate() const {
+    return local_certificate_;
+  }
+
+  webrtc::RTCError SetLocalJsepTransportDescription(
+      const JsepTransportDescription& jsep_description,
+      webrtc::SdpType type);
+
+  // Set the remote TransportDescription to be used by DTLS and ICE channels
+  // that are part of this Transport.
+  webrtc::RTCError SetRemoteJsepTransportDescription(
+      const JsepTransportDescription& jsep_description,
+      webrtc::SdpType type);
+
+  webrtc::RTCError AddRemoteCandidates(const Candidates& candidates);
+
+  // Set the "needs-ice-restart" flag as described in JSEP. After the flag is
+  // set, offers should generate new ufrags/passwords until an ICE restart
+  // occurs.
+  //
+  // This and the below method can be called safely from any thread as long as
+  // SetXTransportDescription is not in progress.
+  void SetNeedsIceRestartFlag();
+  // Returns true if the ICE restart flag above was set, and no ICE restart has
+  // occurred yet for this transport (by applying a local description with
+  // changed ufrag/password).
+  bool needs_ice_restart() const { return needs_ice_restart_; }
+
+  // Returns role if negotiated, or empty Optional if it hasn't been negotiated
+  // yet.
+  rtc::Optional<rtc::SSLRole> GetDtlsRole() const;
+
+  // TODO(deadbeef): Make this const. See comment in transportcontroller.h.
+  bool GetStats(TransportStats* stats);
+
+  const JsepTransportDescription* local_description() const {
+    return local_description_.get();
+  }
+
+  const JsepTransportDescription* remote_description() const {
+    return remote_description_.get();
+  }
+
+  webrtc::RtpTransportInternal* rtp_transport() const {
+    if (dtls_srtp_transport_) {
+      return dtls_srtp_transport_.get();
+    } else if (sdes_transport_) {
+      return sdes_transport_.get();
+    } else {
+      return unencrypted_rtp_transport_.get();
+    }
+  }
+
+  DtlsTransportInternal* rtp_dtls_transport() const {
+    return rtp_dtls_transport_.get();
+  }
+
+  DtlsTransportInternal* rtcp_dtls_transport() const {
+    return rtcp_dtls_transport_.get();
+  }
+
+  // This is signaled when RTCP-mux becomes active and
+  // |rtcp_dtls_transport_| is destroyed. The JsepTransportController will
+  // handle the signal and update the aggregate transport states.
+  sigslot::signal<> SignalRtcpMuxActive;
+
+  // TODO(deadbeef): The methods below are only public for testing. Should make
+  // them utility functions or objects so they can be tested independently from
+  // this class.
+
+  // Returns an error if the certificate's identity does not match the
+  // fingerprint, or either is NULL.
+  webrtc::RTCError VerifyCertificateFingerprint(
+      const rtc::RTCCertificate* certificate,
+      const rtc::SSLFingerprint* fingerprint) const;
+
+ private:
+  bool SetRtcpMux(bool enable, webrtc::SdpType type, ContentSource source);
+
+  void ActivateRtcpMux();
+
+  bool SetSdes(const std::vector<CryptoParams>& cryptos,
+               const std::vector<int>& encrypted_extension_ids,
+               webrtc::SdpType type,
+               ContentSource source);
+
+  // Negotiates and sets the DTLS parameters based on the current local and
+  // remote transport description, such as the DTLS role to use, and whether
+  // DTLS should be activated.
+  //
+  // Called when an answer TransportDescription is applied.
+  webrtc::RTCError NegotiateAndSetDtlsParameters(
+      webrtc::SdpType local_description_type);
+
+  // Negotiates the DTLS role based off the offer and answer as specified by
+  // RFC 4145, section-4.1. Returns an RTCError if role cannot be determined
+  // from the local description and remote description.
+  webrtc::RTCError NegotiateDtlsRole(
+      webrtc::SdpType local_description_type,
+      ConnectionRole local_connection_role,
+      ConnectionRole remote_connection_role,
+      rtc::Optional<rtc::SSLRole>* negotiated_dtls_role);
+
+  // Pushes down the ICE parameters from the local description, such
+  // as the ICE ufrag and pwd.
+  void SetLocalIceParameters(IceTransportInternal* ice);
+
+  // Pushes down the ICE parameters from the remote description.
+  void SetRemoteIceParameters(IceTransportInternal* ice);
+
+  // Pushes down the DTLS parameters obtained via negotiation.
+  webrtc::RTCError SetNegotiatedDtlsParameters(
+      DtlsTransportInternal* dtls_transport,
+      rtc::Optional<rtc::SSLRole> dtls_role,
+      rtc::SSLFingerprint* remote_fingerprint);
+
+  bool GetTransportStats(DtlsTransportInternal* dtls_transport,
+                         TransportStats* stats);
+
+  const std::string mid_;
+  // needs-ice-restart bit as described in JSEP.
+  bool needs_ice_restart_ = false;
+  rtc::scoped_refptr<rtc::RTCCertificate> local_certificate_;
+  std::unique_ptr<JsepTransportDescription> local_description_;
+  std::unique_ptr<JsepTransportDescription> remote_description_;
+
+  // To avoid downcasting and make it type safe, keep three unique pointers for
+  // different SRTP mode and only one of these is non-nullptr.
+  std::unique_ptr<webrtc::RtpTransport> unencrypted_rtp_transport_;
+  std::unique_ptr<webrtc::SrtpTransport> sdes_transport_;
+  std::unique_ptr<webrtc::DtlsSrtpTransport> dtls_srtp_transport_;
+
+  std::unique_ptr<DtlsTransportInternal> rtp_dtls_transport_;
+  std::unique_ptr<DtlsTransportInternal> rtcp_dtls_transport_;
+
+  SrtpFilter sdes_negotiator_;
+  RtcpMuxFilter rtcp_mux_negotiator_;
+
+  // Cache the encrypted header extension IDs for SDES negoitation.
+  rtc::Optional<std::vector<int>> send_extension_ids_;
+  rtc::Optional<std::vector<int>> recv_extension_ids_;
+
+  RTC_DISALLOW_COPY_AND_ASSIGN(JsepTransport2);
+};
+
+}  // namespace cricket
+
+#endif  // PC_JSEPTRANSPORT2_H_
diff --git a/pc/jseptransport2_unittest.cc b/pc/jseptransport2_unittest.cc
new file mode 100644
index 0000000..fc098ae
--- /dev/null
+++ b/pc/jseptransport2_unittest.cc
@@ -0,0 +1,1000 @@
+/*
+ *  Copyright 2018 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 <memory>
+#include <utility>
+
+#include "p2p/base/fakedtlstransport.h"
+#include "p2p/base/fakeicetransport.h"
+#include "pc/jseptransport2.h"
+#include "rtc_base/gunit.h"
+
+namespace cricket {
+using webrtc::SdpType;
+
+static const char kIceUfrag1[] = "U001";
+static const char kIcePwd1[] = "TESTICEPWD00000000000001";
+static const char kIceUfrag2[] = "U002";
+static const char kIcePwd2[] = "TESTIEPWD00000000000002";
+static const char kTransportName[] = "Test Transport";
+
+enum class SrtpMode {
+  kSdes,
+  kDtlsSrtp,
+};
+
+struct NegotiateRoleParams {
+  ConnectionRole local_role;
+  ConnectionRole remote_role;
+  SdpType local_type;
+  SdpType remote_type;
+};
+
+class JsepTransport2Test : public testing::Test, public sigslot::has_slots<> {
+ protected:
+  std::unique_ptr<webrtc::SrtpTransport> CreateSdesTransport(
+      const std::string& transport_name,
+      rtc::PacketTransportInternal* rtp_packet_transport,
+      rtc::PacketTransportInternal* rtcp_packet_transport) {
+    bool rtcp_mux_enabled = (rtcp_packet_transport == nullptr);
+    auto srtp_transport =
+        rtc::MakeUnique<webrtc::SrtpTransport>(rtcp_mux_enabled);
+
+    srtp_transport->SetRtpPacketTransport(rtp_packet_transport);
+    if (rtcp_packet_transport) {
+      srtp_transport->SetRtcpPacketTransport(rtp_packet_transport);
+    }
+    return srtp_transport;
+  }
+
+  std::unique_ptr<webrtc::DtlsSrtpTransport> CreateDtlsSrtpTransport(
+      const std::string& transport_name,
+      cricket::DtlsTransportInternal* rtp_dtls_transport,
+      cricket::DtlsTransportInternal* rtcp_dtls_transport) {
+    bool rtcp_mux_enabled = (rtcp_dtls_transport == nullptr);
+    auto srtp_transport =
+        rtc::MakeUnique<webrtc::SrtpTransport>(rtcp_mux_enabled);
+    auto dtls_srtp_transport =
+        rtc::MakeUnique<webrtc::DtlsSrtpTransport>(std::move(srtp_transport));
+
+    dtls_srtp_transport->SetDtlsTransports(rtp_dtls_transport,
+                                           rtcp_dtls_transport);
+    return dtls_srtp_transport;
+  }
+
+  // Create a new JsepTransport2 with a FakeDtlsTransport and a
+  // FakeIceTransport.
+  void CreateJsepTransport2(bool rtcp_mux_enabled, SrtpMode srtp_mode) {
+    auto ice = rtc::MakeUnique<FakeIceTransport>(kTransportName,
+                                                 ICE_CANDIDATE_COMPONENT_RTP);
+    auto rtp_dtls_transport =
+        rtc::MakeUnique<FakeDtlsTransport>(std::move(ice));
+
+    std::unique_ptr<FakeDtlsTransport> rtcp_dtls_transport;
+    if (!rtcp_mux_enabled) {
+      ice = rtc::MakeUnique<FakeIceTransport>(kTransportName,
+                                              ICE_CANDIDATE_COMPONENT_RTCP);
+      rtcp_dtls_transport = rtc::MakeUnique<FakeDtlsTransport>(std::move(ice));
+    }
+
+    std::unique_ptr<webrtc::RtpTransport> unencrypted_rtp_transport;
+    std::unique_ptr<webrtc::SrtpTransport> sdes_transport;
+    std::unique_ptr<webrtc::DtlsSrtpTransport> dtls_srtp_transport;
+    switch (srtp_mode) {
+      case SrtpMode::kSdes:
+        sdes_transport =
+            CreateSdesTransport(kTransportName, rtp_dtls_transport.get(),
+                                rtcp_dtls_transport.get());
+        sdes_transport_ = sdes_transport.get();
+        break;
+      case SrtpMode::kDtlsSrtp:
+        dtls_srtp_transport =
+            CreateDtlsSrtpTransport(kTransportName, rtp_dtls_transport.get(),
+                                    rtcp_dtls_transport.get());
+        break;
+      default:
+        RTC_NOTREACHED();
+    }
+
+    jsep_transport_ = rtc::MakeUnique<JsepTransport2>(
+        kTransportName, /*local_certificate=*/nullptr,
+        std::move(unencrypted_rtp_transport), std::move(sdes_transport),
+        std::move(dtls_srtp_transport), std::move(rtp_dtls_transport),
+        std::move(rtcp_dtls_transport));
+
+    signal_rtcp_mux_active_received_ = false;
+    jsep_transport_->SignalRtcpMuxActive.connect(
+        this, &JsepTransport2Test::OnRtcpMuxActive);
+  }
+
+  JsepTransportDescription MakeJsepTransportDescription(
+      bool rtcp_mux_enabled,
+      const char* ufrag,
+      const char* pwd,
+      const rtc::scoped_refptr<rtc::RTCCertificate>& cert,
+      ConnectionRole role = CONNECTIONROLE_NONE) {
+    JsepTransportDescription jsep_description;
+    jsep_description.rtcp_mux_enabled = rtcp_mux_enabled;
+
+    std::unique_ptr<rtc::SSLFingerprint> fingerprint;
+    if (cert) {
+      fingerprint.reset(rtc::SSLFingerprint::CreateFromCertificate(cert));
+    }
+    jsep_description.transport_desc =
+        TransportDescription(std::vector<std::string>(), ufrag, pwd,
+                             ICEMODE_FULL, role, fingerprint.get());
+    return jsep_description;
+  }
+
+  Candidate CreateCandidate(int component) {
+    Candidate c;
+    c.set_address(rtc::SocketAddress("192.168.1.1", 8000));
+    c.set_component(component);
+    c.set_protocol(UDP_PROTOCOL_NAME);
+    c.set_priority(1);
+    return c;
+  }
+
+  void OnRtcpMuxActive() { signal_rtcp_mux_active_received_ = true; }
+
+  std::unique_ptr<JsepTransport2> jsep_transport_;
+  bool signal_rtcp_mux_active_received_ = false;
+  // The SrtpTransport is owned by |jsep_transport_|. Keep a raw pointer here
+  // for testing.
+  webrtc::SrtpTransport* sdes_transport_ = nullptr;
+};
+
+// The parameterized tests cover both cases when RTCP mux is enable and
+// disabled.
+class JsepTransport2WithRtcpMux : public JsepTransport2Test,
+                                  public testing::WithParamInterface<bool> {};
+
+// This test verifies the ICE parameters are properly applied to the transports.
+TEST_P(JsepTransport2WithRtcpMux, SetIceParameters) {
+  bool rtcp_mux_enabled = GetParam();
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+
+  JsepTransportDescription jsep_description;
+  jsep_description.transport_desc = TransportDescription(kIceUfrag1, kIcePwd1);
+  jsep_description.rtcp_mux_enabled = rtcp_mux_enabled;
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(jsep_description, SdpType::kOffer)
+          .ok());
+  auto fake_ice_transport = static_cast<FakeIceTransport*>(
+      jsep_transport_->rtp_dtls_transport()->ice_transport());
+  EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode());
+  EXPECT_EQ(kIceUfrag1, fake_ice_transport->ice_ufrag());
+  EXPECT_EQ(kIcePwd1, fake_ice_transport->ice_pwd());
+  if (!rtcp_mux_enabled) {
+    fake_ice_transport = static_cast<FakeIceTransport*>(
+        jsep_transport_->rtcp_dtls_transport()->ice_transport());
+    ASSERT_TRUE(fake_ice_transport);
+    EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode());
+    EXPECT_EQ(kIceUfrag1, fake_ice_transport->ice_ufrag());
+    EXPECT_EQ(kIcePwd1, fake_ice_transport->ice_pwd());
+  }
+
+  jsep_description.transport_desc = TransportDescription(kIceUfrag2, kIcePwd2);
+  ASSERT_TRUE(jsep_transport_
+                  ->SetRemoteJsepTransportDescription(jsep_description,
+                                                      SdpType::kAnswer)
+                  .ok());
+  fake_ice_transport = static_cast<FakeIceTransport*>(
+      jsep_transport_->rtp_dtls_transport()->ice_transport());
+  EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode());
+  EXPECT_EQ(kIceUfrag2, fake_ice_transport->remote_ice_ufrag());
+  EXPECT_EQ(kIcePwd2, fake_ice_transport->remote_ice_pwd());
+  if (!rtcp_mux_enabled) {
+    fake_ice_transport = static_cast<FakeIceTransport*>(
+        jsep_transport_->rtcp_dtls_transport()->ice_transport());
+    ASSERT_TRUE(fake_ice_transport);
+    EXPECT_EQ(ICEMODE_FULL, fake_ice_transport->remote_ice_mode());
+    EXPECT_EQ(kIceUfrag2, fake_ice_transport->remote_ice_ufrag());
+    EXPECT_EQ(kIcePwd2, fake_ice_transport->remote_ice_pwd());
+  }
+}
+
+// Similarly, test DTLS parameters are properly applied to the transports.
+TEST_P(JsepTransport2WithRtcpMux, SetDtlsParameters) {
+  bool rtcp_mux_enabled = GetParam();
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+
+  // Create certificates.
+  rtc::scoped_refptr<rtc::RTCCertificate> local_cert =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("local", rtc::KT_DEFAULT)));
+  rtc::scoped_refptr<rtc::RTCCertificate> remote_cert =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("remote", rtc::KT_DEFAULT)));
+  jsep_transport_->SetLocalCertificate(local_cert);
+
+  // Apply offer.
+  JsepTransportDescription local_description =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   local_cert, CONNECTIONROLE_ACTPASS);
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_description, SdpType::kOffer)
+          .ok());
+  // Apply Answer.
+  JsepTransportDescription remote_description =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   remote_cert, CONNECTIONROLE_ACTIVE);
+  ASSERT_TRUE(jsep_transport_
+                  ->SetRemoteJsepTransportDescription(remote_description,
+                                                      SdpType::kAnswer)
+                  .ok());
+
+  // Verify that SSL role and remote fingerprint were set correctly based on
+  // transport descriptions.
+  auto role = jsep_transport_->GetDtlsRole();
+  ASSERT_TRUE(role);
+  EXPECT_EQ(rtc::SSL_SERVER, role);  // Because remote description was "active".
+  auto fake_dtls =
+      static_cast<FakeDtlsTransport*>(jsep_transport_->rtp_dtls_transport());
+  EXPECT_EQ(remote_description.transport_desc.identity_fingerprint->ToString(),
+            fake_dtls->dtls_fingerprint().ToString());
+
+  if (!rtcp_mux_enabled) {
+    auto fake_rtcp_dtls =
+        static_cast<FakeDtlsTransport*>(jsep_transport_->rtcp_dtls_transport());
+    EXPECT_EQ(
+        remote_description.transport_desc.identity_fingerprint->ToString(),
+        fake_rtcp_dtls->dtls_fingerprint().ToString());
+  }
+}
+
+// Same as above test, but with remote transport description using
+// CONNECTIONROLE_PASSIVE, expecting SSL_CLIENT role.
+TEST_P(JsepTransport2WithRtcpMux, SetDtlsParametersWithPassiveAnswer) {
+  bool rtcp_mux_enabled = GetParam();
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+
+  // Create certificates.
+  rtc::scoped_refptr<rtc::RTCCertificate> local_cert =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("local", rtc::KT_DEFAULT)));
+  rtc::scoped_refptr<rtc::RTCCertificate> remote_cert =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("remote", rtc::KT_DEFAULT)));
+  jsep_transport_->SetLocalCertificate(local_cert);
+
+  // Apply offer.
+  JsepTransportDescription local_description =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   local_cert, CONNECTIONROLE_ACTPASS);
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_description, SdpType::kOffer)
+          .ok());
+  // Apply Answer.
+  JsepTransportDescription remote_description =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   remote_cert, CONNECTIONROLE_PASSIVE);
+  ASSERT_TRUE(jsep_transport_
+                  ->SetRemoteJsepTransportDescription(remote_description,
+                                                      SdpType::kAnswer)
+                  .ok());
+
+  // Verify that SSL role and remote fingerprint were set correctly based on
+  // transport descriptions.
+  auto role = jsep_transport_->GetDtlsRole();
+  ASSERT_TRUE(role);
+  EXPECT_EQ(rtc::SSL_CLIENT,
+            role);  // Because remote description was "passive".
+  auto fake_dtls =
+      static_cast<FakeDtlsTransport*>(jsep_transport_->rtp_dtls_transport());
+  EXPECT_EQ(remote_description.transport_desc.identity_fingerprint->ToString(),
+            fake_dtls->dtls_fingerprint().ToString());
+
+  if (!rtcp_mux_enabled) {
+    auto fake_rtcp_dtls =
+        static_cast<FakeDtlsTransport*>(jsep_transport_->rtcp_dtls_transport());
+    EXPECT_EQ(
+        remote_description.transport_desc.identity_fingerprint->ToString(),
+        fake_rtcp_dtls->dtls_fingerprint().ToString());
+  }
+}
+
+// Tests SetNeedsIceRestartFlag and need_ice_restart, ensuring needs_ice_restart
+// only starts returning "false" once an ICE restart has been initiated.
+TEST_P(JsepTransport2WithRtcpMux, NeedsIceRestart) {
+  bool rtcp_mux_enabled = GetParam();
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+
+  // Use the same JsepTransportDescription for both offer and answer.
+  JsepTransportDescription description;
+  description.transport_desc = TransportDescription(kIceUfrag1, kIcePwd1);
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(description, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(description, SdpType::kAnswer)
+          .ok());
+  // Flag initially should be false.
+  EXPECT_FALSE(jsep_transport_->needs_ice_restart());
+
+  // After setting flag, it should be true.
+  jsep_transport_->SetNeedsIceRestartFlag();
+  EXPECT_TRUE(jsep_transport_->needs_ice_restart());
+
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(description, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(description, SdpType::kAnswer)
+          .ok());
+  EXPECT_TRUE(jsep_transport_->needs_ice_restart());
+
+  // Doing an offer/answer that restarts ICE should clear the flag.
+  description.transport_desc = TransportDescription(kIceUfrag2, kIcePwd2);
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(description, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(description, SdpType::kAnswer)
+          .ok());
+  EXPECT_FALSE(jsep_transport_->needs_ice_restart());
+}
+
+TEST_P(JsepTransport2WithRtcpMux, GetStats) {
+  bool rtcp_mux_enabled = GetParam();
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+
+  size_t expected_stats_size = rtcp_mux_enabled ? 1u : 2u;
+  TransportStats stats;
+  EXPECT_TRUE(jsep_transport_->GetStats(&stats));
+  EXPECT_EQ(expected_stats_size, stats.channel_stats.size());
+  EXPECT_EQ(ICE_CANDIDATE_COMPONENT_RTP, stats.channel_stats[0].component);
+  if (!rtcp_mux_enabled) {
+    EXPECT_EQ(ICE_CANDIDATE_COMPONENT_RTCP, stats.channel_stats[1].component);
+  }
+}
+
+// Tests that VerifyCertificateFingerprint only returns true when the
+// certificate matches the fingerprint.
+TEST_P(JsepTransport2WithRtcpMux, VerifyCertificateFingerprint) {
+  bool rtcp_mux_enabled = GetParam();
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+
+  EXPECT_FALSE(
+      jsep_transport_->VerifyCertificateFingerprint(nullptr, nullptr).ok());
+  rtc::KeyType key_types[] = {rtc::KT_RSA, rtc::KT_ECDSA};
+
+  for (auto& key_type : key_types) {
+    rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+        rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+            rtc::SSLIdentity::Generate("testing", key_type)));
+    ASSERT_NE(nullptr, certificate);
+
+    std::string digest_algorithm;
+    ASSERT_TRUE(certificate->ssl_certificate().GetSignatureDigestAlgorithm(
+        &digest_algorithm));
+    ASSERT_FALSE(digest_algorithm.empty());
+    std::unique_ptr<rtc::SSLFingerprint> good_fingerprint(
+        rtc::SSLFingerprint::Create(digest_algorithm, certificate->identity()));
+    ASSERT_NE(nullptr, good_fingerprint);
+
+    EXPECT_TRUE(jsep_transport_
+                    ->VerifyCertificateFingerprint(certificate.get(),
+                                                   good_fingerprint.get())
+                    .ok());
+    EXPECT_FALSE(jsep_transport_
+                     ->VerifyCertificateFingerprint(certificate.get(), nullptr)
+                     .ok());
+    EXPECT_FALSE(
+        jsep_transport_
+            ->VerifyCertificateFingerprint(nullptr, good_fingerprint.get())
+            .ok());
+
+    rtc::SSLFingerprint bad_fingerprint = *good_fingerprint;
+    bad_fingerprint.digest.AppendData("0", 1);
+    EXPECT_FALSE(
+        jsep_transport_
+            ->VerifyCertificateFingerprint(certificate.get(), &bad_fingerprint)
+            .ok());
+  }
+}
+
+// Tests the logic of DTLS role negotiation for an initial offer/answer.
+TEST_P(JsepTransport2WithRtcpMux, ValidDtlsRoleNegotiation) {
+  bool rtcp_mux_enabled = GetParam();
+  // Just use the same certificate for both sides; doesn't really matter in a
+  // non end-to-end test.
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+
+  JsepTransportDescription local_description = MakeJsepTransportDescription(
+      rtcp_mux_enabled, kIceUfrag1, kIcePwd1, certificate);
+  JsepTransportDescription remote_description = MakeJsepTransportDescription(
+      rtcp_mux_enabled, kIceUfrag2, kIcePwd2, certificate);
+
+  // Parameters which set the SSL role to SSL_CLIENT.
+  NegotiateRoleParams valid_client_params[] = {
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTPASS, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTPASS, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kOffer,
+       SdpType::kPrAnswer}};
+
+  for (auto& param : valid_client_params) {
+    CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+    jsep_transport_->SetLocalCertificate(certificate);
+
+    local_description.transport_desc.connection_role = param.local_role;
+    remote_description.transport_desc.connection_role = param.remote_role;
+
+    // Set the offer first.
+    if (param.local_type == SdpType::kOffer) {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetLocalJsepTransportDescription(local_description,
+                                                         param.local_type)
+                      .ok());
+      EXPECT_TRUE(jsep_transport_
+                      ->SetRemoteJsepTransportDescription(remote_description,
+                                                          param.remote_type)
+                      .ok());
+    } else {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetRemoteJsepTransportDescription(remote_description,
+                                                          param.remote_type)
+                      .ok());
+      EXPECT_TRUE(jsep_transport_
+                      ->SetLocalJsepTransportDescription(local_description,
+                                                         param.local_type)
+                      .ok());
+    }
+    EXPECT_EQ(rtc::SSL_CLIENT, *jsep_transport_->GetDtlsRole());
+  }
+
+  // Parameters which set the SSL role to SSL_SERVER.
+  NegotiateRoleParams valid_server_params[] = {
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTIVE, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTIVE, SdpType::kOffer,
+       SdpType::kPrAnswer}};
+
+  for (auto& param : valid_server_params) {
+    CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+    jsep_transport_->SetLocalCertificate(certificate);
+
+    local_description.transport_desc.connection_role = param.local_role;
+    remote_description.transport_desc.connection_role = param.remote_role;
+
+    // Set the offer first.
+    if (param.local_type == SdpType::kOffer) {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetLocalJsepTransportDescription(local_description,
+                                                         param.local_type)
+                      .ok());
+      EXPECT_TRUE(jsep_transport_
+                      ->SetRemoteJsepTransportDescription(remote_description,
+                                                          param.remote_type)
+                      .ok());
+    } else {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetRemoteJsepTransportDescription(remote_description,
+                                                          param.remote_type)
+                      .ok());
+      EXPECT_TRUE(jsep_transport_
+                      ->SetLocalJsepTransportDescription(local_description,
+                                                         param.local_type)
+                      .ok());
+    }
+    EXPECT_EQ(rtc::SSL_SERVER, *jsep_transport_->GetDtlsRole());
+  }
+}
+
+// Tests the logic of DTLS role negotiation for an initial offer/answer.
+TEST_P(JsepTransport2WithRtcpMux, InvalidDtlsRoleNegotiation) {
+  bool rtcp_mux_enabled = GetParam();
+  // Just use the same certificate for both sides; doesn't really matter in a
+  // non end-to-end test.
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+
+  JsepTransportDescription local_description = MakeJsepTransportDescription(
+      rtcp_mux_enabled, kIceUfrag1, kIcePwd1, certificate);
+  JsepTransportDescription remote_description = MakeJsepTransportDescription(
+      rtcp_mux_enabled, kIceUfrag2, kIcePwd2, certificate);
+
+  NegotiateRoleParams duplicate_params[] = {
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer,
+       SdpType::kPrAnswer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_ACTPASS, SdpType::kOffer,
+       SdpType::kPrAnswer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer,
+       SdpType::kPrAnswer}};
+
+  for (auto& param : duplicate_params) {
+    CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+    jsep_transport_->SetLocalCertificate(certificate);
+
+    local_description.transport_desc.connection_role = param.local_role;
+    remote_description.transport_desc.connection_role = param.remote_role;
+
+    if (param.local_type == SdpType::kOffer) {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetLocalJsepTransportDescription(local_description,
+                                                         param.local_type)
+                      .ok());
+      EXPECT_FALSE(jsep_transport_
+                       ->SetRemoteJsepTransportDescription(remote_description,
+                                                           param.remote_type)
+                       .ok());
+    } else {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetRemoteJsepTransportDescription(remote_description,
+                                                          param.remote_type)
+                      .ok());
+      EXPECT_FALSE(jsep_transport_
+                       ->SetLocalJsepTransportDescription(local_description,
+                                                          param.local_type)
+                       .ok());
+    }
+  }
+
+  // Invalid parameters due to the offerer not using ACTPASS.
+  NegotiateRoleParams offerer_without_actpass_params[] = {
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTPASS, CONNECTIONROLE_PASSIVE, SdpType::kPrAnswer,
+       SdpType::kOffer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kOffer,
+       SdpType::kAnswer},
+      {CONNECTIONROLE_ACTIVE, CONNECTIONROLE_PASSIVE, SdpType::kOffer,
+       SdpType::kPrAnswer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTIVE, SdpType::kOffer,
+       SdpType::kPrAnswer},
+      {CONNECTIONROLE_PASSIVE, CONNECTIONROLE_ACTPASS, SdpType::kOffer,
+       SdpType::kPrAnswer}};
+
+  for (auto& param : offerer_without_actpass_params) {
+    CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+    jsep_transport_->SetLocalCertificate(certificate);
+
+    local_description.transport_desc.connection_role = param.local_role;
+    remote_description.transport_desc.connection_role = param.remote_role;
+
+    if (param.local_type == SdpType::kOffer) {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetLocalJsepTransportDescription(local_description,
+                                                         param.local_type)
+                      .ok());
+      EXPECT_FALSE(jsep_transport_
+                       ->SetRemoteJsepTransportDescription(remote_description,
+                                                           param.remote_type)
+                       .ok());
+    } else {
+      EXPECT_TRUE(jsep_transport_
+                      ->SetRemoteJsepTransportDescription(remote_description,
+                                                          param.remote_type)
+                      .ok());
+      EXPECT_FALSE(jsep_transport_
+                       ->SetLocalJsepTransportDescription(local_description,
+                                                          param.local_type)
+                       .ok());
+    }
+  }
+}
+
+INSTANTIATE_TEST_CASE_P(JsepTransport2Test,
+                        JsepTransport2WithRtcpMux,
+                        testing::Bool());
+
+// Test that a reoffer in the opposite direction is successful as long as the
+// role isn't changing. Doesn't test every possible combination like the test
+// above.
+TEST_F(JsepTransport2Test, ValidDtlsReofferFromAnswerer) {
+  // Just use the same certificate for both sides; doesn't really matter in a
+  // non end-to-end test.
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+  bool rtcp_mux_enabled = true;
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+  jsep_transport_->SetLocalCertificate(certificate);
+
+  JsepTransportDescription local_offer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_ACTPASS);
+  JsepTransportDescription remote_answer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_ACTIVE);
+
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_offer, SdpType::kOffer)
+          .ok());
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_answer, SdpType::kAnswer)
+          .ok());
+
+  // We were actpass->active previously, now in the other direction it's
+  // actpass->passive.
+  JsepTransportDescription remote_offer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_ACTPASS);
+  JsepTransportDescription local_answer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_PASSIVE);
+
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_offer, SdpType::kOffer)
+          .ok());
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_answer, SdpType::kAnswer)
+          .ok());
+}
+
+// Test that a reoffer in the opposite direction fails if the role changes.
+// Inverse of test above.
+TEST_F(JsepTransport2Test, InvalidDtlsReofferFromAnswerer) {
+  // Just use the same certificate for both sides; doesn't really matter in a
+  // non end-to-end test.
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+  bool rtcp_mux_enabled = true;
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+  jsep_transport_->SetLocalCertificate(certificate);
+
+  JsepTransportDescription local_offer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_ACTPASS);
+  JsepTransportDescription remote_answer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_ACTIVE);
+
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_offer, SdpType::kOffer)
+          .ok());
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_answer, SdpType::kAnswer)
+          .ok());
+
+  // Changing role to passive here isn't allowed. Though for some reason this
+  // only fails in SetLocalTransportDescription.
+  JsepTransportDescription remote_offer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_PASSIVE);
+  JsepTransportDescription local_answer =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_ACTIVE);
+
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_offer, SdpType::kOffer)
+          .ok());
+  EXPECT_FALSE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_answer, SdpType::kAnswer)
+          .ok());
+}
+
+// Test that a remote offer with the current negotiated role can be accepted.
+// This is allowed by dtls-sdp, though we'll never generate such an offer,
+// since JSEP requires generating "actpass".
+TEST_F(JsepTransport2Test, RemoteOfferWithCurrentNegotiatedDtlsRole) {
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+  bool rtcp_mux_enabled = true;
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+  jsep_transport_->SetLocalCertificate(certificate);
+
+  JsepTransportDescription remote_desc =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_ACTPASS);
+  JsepTransportDescription local_desc =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_ACTIVE);
+
+  // Normal initial offer/answer with "actpass" in the offer and "active" in
+  // the answer.
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer)
+          .ok());
+
+  // Sanity check that role was actually negotiated.
+  rtc::Optional<rtc::SSLRole> role = jsep_transport_->GetDtlsRole();
+  ASSERT_TRUE(role);
+  EXPECT_EQ(rtc::SSL_CLIENT, *role);
+
+  // Subsequent offer with current negotiated role of "passive".
+  remote_desc.transport_desc.connection_role = CONNECTIONROLE_PASSIVE;
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer)
+          .ok());
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer)
+          .ok());
+}
+
+// Test that a remote offer with the inverse of the current negotiated DTLS
+// role is rejected.
+TEST_F(JsepTransport2Test, RemoteOfferThatChangesNegotiatedDtlsRole) {
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+  bool rtcp_mux_enabled = true;
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+  jsep_transport_->SetLocalCertificate(certificate);
+
+  JsepTransportDescription remote_desc =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_ACTPASS);
+  JsepTransportDescription local_desc =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_ACTIVE);
+
+  // Normal initial offer/answer with "actpass" in the offer and "active" in
+  // the answer.
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer)
+          .ok());
+
+  // Sanity check that role was actually negotiated.
+  rtc::Optional<rtc::SSLRole> role = jsep_transport_->GetDtlsRole();
+  ASSERT_TRUE(role);
+  EXPECT_EQ(rtc::SSL_CLIENT, *role);
+
+  // Subsequent offer with current negotiated role of "passive".
+  remote_desc.transport_desc.connection_role = CONNECTIONROLE_ACTIVE;
+  EXPECT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kOffer)
+          .ok());
+  EXPECT_FALSE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kAnswer)
+          .ok());
+}
+
+// Testing that a legacy client that doesn't use the setup attribute will be
+// interpreted as having an active role.
+TEST_F(JsepTransport2Test, DtlsSetupWithLegacyAsAnswerer) {
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("testing", rtc::KT_ECDSA)));
+  bool rtcp_mux_enabled = true;
+  CreateJsepTransport2(rtcp_mux_enabled, SrtpMode::kDtlsSrtp);
+  jsep_transport_->SetLocalCertificate(certificate);
+
+  JsepTransportDescription remote_desc =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag1, kIcePwd1,
+                                   certificate, CONNECTIONROLE_ACTPASS);
+  JsepTransportDescription local_desc =
+      MakeJsepTransportDescription(rtcp_mux_enabled, kIceUfrag2, kIcePwd2,
+                                   certificate, CONNECTIONROLE_ACTIVE);
+
+  local_desc.transport_desc.connection_role = CONNECTIONROLE_ACTPASS;
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kOffer)
+          .ok());
+  // Use CONNECTIONROLE_NONE to simulate legacy endpoint.
+  remote_desc.transport_desc.connection_role = CONNECTIONROLE_NONE;
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kAnswer)
+          .ok());
+
+  rtc::Optional<rtc::SSLRole> role = jsep_transport_->GetDtlsRole();
+  ASSERT_TRUE(role);
+  // Since legacy answer ommitted setup atribute, and we offered actpass, we
+  // should act as passive (server).
+  EXPECT_EQ(rtc::SSL_SERVER, *role);
+}
+
+// Tests that when the RTCP mux is successfully negotiated, the RTCP transport
+// will be destroyed and the SignalRtpMuxActive will be fired.
+TEST_F(JsepTransport2Test, RtcpMuxNegotiation) {
+  CreateJsepTransport2(/*rtcp_mux_enabled=*/false, SrtpMode::kDtlsSrtp);
+  JsepTransportDescription local_desc;
+  local_desc.rtcp_mux_enabled = true;
+  EXPECT_NE(nullptr, jsep_transport_->rtcp_dtls_transport());
+  EXPECT_FALSE(signal_rtcp_mux_active_received_);
+
+  // The remote side supports RTCP-mux.
+  JsepTransportDescription remote_desc;
+  remote_desc.rtcp_mux_enabled = true;
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kAnswer)
+          .ok());
+
+  EXPECT_EQ(nullptr, jsep_transport_->rtcp_dtls_transport());
+  EXPECT_TRUE(signal_rtcp_mux_active_received_);
+
+  // The remote side doesn't support RTCP-mux.
+  CreateJsepTransport2(/*rtcp_mux_enabled=*/false, SrtpMode::kDtlsSrtp);
+  signal_rtcp_mux_active_received_ = false;
+  remote_desc.rtcp_mux_enabled = false;
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(local_desc, SdpType::kOffer)
+          .ok());
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(remote_desc, SdpType::kAnswer)
+          .ok());
+
+  EXPECT_NE(nullptr, jsep_transport_->rtcp_dtls_transport());
+  EXPECT_FALSE(signal_rtcp_mux_active_received_);
+}
+
+TEST_F(JsepTransport2Test, SdesNegotiation) {
+  CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes);
+  ASSERT_TRUE(sdes_transport_);
+  EXPECT_FALSE(sdes_transport_->IsActive());
+
+  JsepTransportDescription offer_desc;
+  offer_desc.cryptos.push_back(cricket::CryptoParams(
+      1, rtc::CS_AES_CM_128_HMAC_SHA1_32,
+      "inline:" + rtc::CreateRandomString(40), std::string()));
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer)
+          .ok());
+
+  JsepTransportDescription answer_desc;
+  answer_desc.cryptos.push_back(cricket::CryptoParams(
+      1, rtc::CS_AES_CM_128_HMAC_SHA1_32,
+      "inline:" + rtc::CreateRandomString(40), std::string()));
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer)
+          .ok());
+  EXPECT_TRUE(sdes_transport_->IsActive());
+}
+
+TEST_F(JsepTransport2Test, SdesNegotiationWithEmptyCryptosInAnswer) {
+  CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes);
+  ASSERT_TRUE(sdes_transport_);
+  EXPECT_FALSE(sdes_transport_->IsActive());
+
+  JsepTransportDescription offer_desc;
+  offer_desc.cryptos.push_back(cricket::CryptoParams(
+      1, rtc::CS_AES_CM_128_HMAC_SHA1_32,
+      "inline:" + rtc::CreateRandomString(40), std::string()));
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer)
+          .ok());
+
+  JsepTransportDescription answer_desc;
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer)
+          .ok());
+  // SRTP is not active because the crypto parameter is answer is empty.
+  EXPECT_FALSE(sdes_transport_->IsActive());
+}
+
+TEST_F(JsepTransport2Test, SdesNegotiationWithMismatchedCryptos) {
+  CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kSdes);
+  ASSERT_TRUE(sdes_transport_);
+  EXPECT_FALSE(sdes_transport_->IsActive());
+
+  JsepTransportDescription offer_desc;
+  offer_desc.cryptos.push_back(cricket::CryptoParams(
+      1, rtc::CS_AES_CM_128_HMAC_SHA1_32,
+      "inline:" + rtc::CreateRandomString(40), std::string()));
+  ASSERT_TRUE(
+      jsep_transport_
+          ->SetLocalJsepTransportDescription(offer_desc, SdpType::kOffer)
+          .ok());
+
+  JsepTransportDescription answer_desc;
+  answer_desc.cryptos.push_back(cricket::CryptoParams(
+      1, rtc::CS_AES_CM_128_HMAC_SHA1_80,
+      "inline:" + rtc::CreateRandomString(40), std::string()));
+  // Expected to fail because the crypto parameters don't match.
+  ASSERT_FALSE(
+      jsep_transport_
+          ->SetRemoteJsepTransportDescription(answer_desc, SdpType::kAnswer)
+          .ok());
+}
+
+// Tests that the remote candidates can be added to the transports after both
+// local and remote descriptions are set.
+TEST_F(JsepTransport2Test, AddRemoteCandidates) {
+  CreateJsepTransport2(/*rtcp_mux_enabled=*/true, SrtpMode::kDtlsSrtp);
+  auto fake_ice_transport = static_cast<FakeIceTransport*>(
+      jsep_transport_->rtp_dtls_transport()->ice_transport());
+
+  Candidates candidates;
+  candidates.push_back(CreateCandidate(/*COMPONENT_RTP*/ 1));
+  candidates.push_back(CreateCandidate(/*COMPONENT_RTP*/ 1));
+
+  JsepTransportDescription desc;
+  ASSERT_TRUE(
+      jsep_transport_->SetLocalJsepTransportDescription(desc, SdpType::kOffer)
+          .ok());
+  // Expected to fail because the remote description is unset.
+  EXPECT_FALSE(jsep_transport_->AddRemoteCandidates(candidates).ok());
+
+  ASSERT_TRUE(
+      jsep_transport_->SetRemoteJsepTransportDescription(desc, SdpType::kAnswer)
+          .ok());
+  EXPECT_EQ(0u, fake_ice_transport->remote_candidates().size());
+  EXPECT_TRUE(jsep_transport_->AddRemoteCandidates(candidates).ok());
+  EXPECT_EQ(candidates.size(), fake_ice_transport->remote_candidates().size());
+}
+
+}  // namespace cricket
diff --git a/pc/jseptransport_unittest.cc b/pc/jseptransport_unittest.cc
index 1111f36..a40312f 100644
--- a/pc/jseptransport_unittest.cc
+++ b/pc/jseptransport_unittest.cc
@@ -119,7 +119,7 @@
   // Verify that SSL role and remote fingerprint were set correctly based on
   // transport descriptions.
   rtc::SSLRole role;
-  EXPECT_TRUE(fake_dtls_transports_[0]->GetSslRole(&role));
+  EXPECT_TRUE(fake_dtls_transports_[0]->GetDtlsRole(&role));
   EXPECT_EQ(rtc::SSL_SERVER, role);  // Because remote description was "active".
   EXPECT_EQ(remote_desc.identity_fingerprint->ToString(),
             fake_dtls_transports_[0]->dtls_fingerprint().ToString());
@@ -152,7 +152,7 @@
   // Verify that SSL role and remote fingerprint were set correctly based on
   // transport descriptions.
   rtc::SSLRole role;
-  EXPECT_TRUE(fake_dtls_transports_[0]->GetSslRole(&role));
+  EXPECT_TRUE(fake_dtls_transports_[0]->GetDtlsRole(&role));
   EXPECT_EQ(rtc::SSL_CLIENT,
             role);  // Because remote description was "passive".
   EXPECT_EQ(remote_desc.identity_fingerprint->ToString(),
@@ -194,7 +194,7 @@
     EXPECT_EQ(kIcePwd2, fake_ice_transports_[i]->remote_ice_pwd());
     // Verify parameters of DTLS transports.
     rtc::SSLRole role;
-    EXPECT_TRUE(fake_dtls_transports_[i]->GetSslRole(&role));
+    EXPECT_TRUE(fake_dtls_transports_[i]->GetDtlsRole(&role));
     EXPECT_EQ(rtc::SSL_SERVER,
               role);  // Because remote description was "active".
     EXPECT_EQ(remote_desc.identity_fingerprint->ToString(),
diff --git a/pc/jseptransportcontroller.cc b/pc/jseptransportcontroller.cc
new file mode 100644
index 0000000..aac55c4
--- /dev/null
+++ b/pc/jseptransportcontroller.cc
@@ -0,0 +1,1024 @@
+/*
+ *  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 "pc/jseptransportcontroller.h"
+
+#include <algorithm>
+#include <memory>
+#include <utility>
+
+#include "p2p/base/port.h"
+#include "rtc_base/bind.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/ptr_util.h"
+#include "rtc_base/thread.h"
+
+using webrtc::SdpType;
+
+namespace {
+
+enum {
+  MSG_ICECONNECTIONSTATE,
+  MSG_ICEGATHERINGSTATE,
+  MSG_ICECANDIDATESGATHERED,
+};
+
+struct CandidatesData : public rtc::MessageData {
+  CandidatesData(const std::string& transport_name,
+                 const cricket::Candidates& candidates)
+      : transport_name(transport_name), candidates(candidates) {}
+
+  std::string transport_name;
+  cricket::Candidates candidates;
+};
+
+webrtc::RTCError VerifyCandidate(const cricket::Candidate& cand) {
+  // No address zero.
+  if (cand.address().IsNil() || cand.address().IsAnyIP()) {
+    return webrtc::RTCError(webrtc::RTCErrorType::INVALID_PARAMETER,
+                            "candidate has address of zero");
+  }
+
+  // Disallow all ports below 1024, except for 80 and 443 on public addresses.
+  int port = cand.address().port();
+  if (cand.protocol() == cricket::TCP_PROTOCOL_NAME &&
+      (cand.tcptype() == cricket::TCPTYPE_ACTIVE_STR || port == 0)) {
+    // Expected for active-only candidates per
+    // http://tools.ietf.org/html/rfc6544#section-4.5 so no error.
+    // Libjingle clients emit port 0, in "active" mode.
+    return webrtc::RTCError::OK();
+  }
+  if (port < 1024) {
+    if ((port != 80) && (port != 443)) {
+      return webrtc::RTCError(
+          webrtc::RTCErrorType::INVALID_PARAMETER,
+          "candidate has port below 1024, but not 80 or 443");
+    }
+
+    if (cand.address().IsPrivateIP()) {
+      return webrtc::RTCError(
+          webrtc::RTCErrorType::INVALID_PARAMETER,
+          "candidate has port of 80 or 443 with private IP address");
+    }
+  }
+
+  return webrtc::RTCError::OK();
+}
+
+webrtc::RTCError VerifyCandidates(const cricket::Candidates& candidates) {
+  for (const cricket::Candidate& candidate : candidates) {
+    webrtc::RTCError error = VerifyCandidate(candidate);
+    if (!error.ok()) {
+      return error;
+    }
+  }
+  return webrtc::RTCError::OK();
+}
+
+}  // namespace
+
+namespace webrtc {
+
+JsepTransportController::JsepTransportController(
+    rtc::Thread* signaling_thread,
+    rtc::Thread* network_thread,
+    cricket::PortAllocator* port_allocator,
+    Config config)
+    : signaling_thread_(signaling_thread),
+      network_thread_(network_thread),
+      port_allocator_(port_allocator),
+      config_(config) {}
+
+JsepTransportController::~JsepTransportController() {
+  // Channel destructors may try to send packets, so this needs to happen on
+  // the network thread.
+  network_thread_->Invoke<void>(
+      RTC_FROM_HERE,
+      rtc::Bind(&JsepTransportController::DestroyAllJsepTransports_n, this));
+}
+
+RTCError JsepTransportController::SetLocalDescription(
+    SdpType type,
+    const cricket::SessionDescription* description) {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<RTCError>(
+        RTC_FROM_HERE, [=] { return SetLocalDescription(type, description); });
+  }
+
+  if (!initial_offerer_.has_value()) {
+    initial_offerer_.emplace(type == SdpType::kOffer);
+    if (*initial_offerer_) {
+      SetIceRole_n(cricket::ICEROLE_CONTROLLING);
+    } else {
+      SetIceRole_n(cricket::ICEROLE_CONTROLLED);
+    }
+  }
+  return ApplyDescription_n(/*local=*/true, type, description);
+}
+
+RTCError JsepTransportController::SetRemoteDescription(
+    SdpType type,
+    const cricket::SessionDescription* description) {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<RTCError>(
+        RTC_FROM_HERE, [=] { return SetRemoteDescription(type, description); });
+  }
+
+  return ApplyDescription_n(/*local=*/false, type, description);
+}
+
+RtpTransportInternal* JsepTransportController::GetRtpTransport(
+    const std::string& mid) const {
+  auto jsep_transport = GetJsepTransport(mid);
+  if (!jsep_transport) {
+    return nullptr;
+  }
+  return jsep_transport->rtp_transport();
+}
+
+cricket::DtlsTransportInternal* JsepTransportController::GetDtlsTransport(
+    const std::string& mid) const {
+  auto jsep_transport = GetJsepTransport(mid);
+  if (!jsep_transport) {
+    return nullptr;
+  }
+  return jsep_transport->rtp_dtls_transport();
+}
+
+cricket::DtlsTransportInternal* JsepTransportController::GetRtcpDtlsTransport(
+    const std::string& mid) const {
+  auto jsep_transport = GetJsepTransport(mid);
+  if (!jsep_transport) {
+    return nullptr;
+  }
+  return jsep_transport->rtcp_dtls_transport();
+}
+
+void JsepTransportController::SetIceConfig(const cricket::IceConfig& config) {
+  if (!network_thread_->IsCurrent()) {
+    network_thread_->Invoke<void>(RTC_FROM_HERE, [&] { SetIceConfig(config); });
+    return;
+  }
+
+  ice_config_ = config;
+  for (auto& dtls : GetDtlsTransports()) {
+    dtls->ice_transport()->SetIceConfig(ice_config_);
+  }
+}
+
+void JsepTransportController::SetNeedsIceRestartFlag() {
+  for (auto& kv : jsep_transports_by_mid_) {
+    kv.second->SetNeedsIceRestartFlag();
+  }
+}
+
+bool JsepTransportController::NeedsIceRestart(
+    const std::string& transport_name) const {
+  const cricket::JsepTransport2* transport = GetJsepTransport(transport_name);
+  if (!transport) {
+    return false;
+  }
+  return transport->needs_ice_restart();
+}
+
+rtc::Optional<rtc::SSLRole> JsepTransportController::GetDtlsRole(
+    const std::string& transport_name) const {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<rtc::Optional<rtc::SSLRole>>(
+        RTC_FROM_HERE, [&] { return GetDtlsRole(transport_name); });
+  }
+
+  const cricket::JsepTransport2* t = GetJsepTransport(transport_name);
+  if (!t) {
+    return rtc::Optional<rtc::SSLRole>();
+  }
+  return t->GetDtlsRole();
+}
+
+bool JsepTransportController::SetLocalCertificate(
+    const rtc::scoped_refptr<rtc::RTCCertificate>& certificate) {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<bool>(
+        RTC_FROM_HERE, [&] { return SetLocalCertificate(certificate); });
+  }
+
+  // Can't change a certificate, or set a null certificate.
+  if (certificate_ || !certificate) {
+    return false;
+  }
+  certificate_ = certificate;
+
+  // Set certificate for JsepTransport, which verifies it matches the
+  // fingerprint in SDP, and DTLS transport.
+  // Fallback from DTLS to SDES is not supported.
+  for (auto& kv : jsep_transports_by_mid_) {
+    kv.second->SetLocalCertificate(certificate_);
+  }
+  for (auto& dtls : GetDtlsTransports()) {
+    bool set_cert_success = dtls->SetLocalCertificate(certificate_);
+    RTC_DCHECK(set_cert_success);
+  }
+  return true;
+}
+
+rtc::scoped_refptr<rtc::RTCCertificate>
+JsepTransportController::GetLocalCertificate(
+    const std::string& transport_name) const {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<rtc::scoped_refptr<rtc::RTCCertificate>>(
+        RTC_FROM_HERE, [&] { return GetLocalCertificate(transport_name); });
+  }
+
+  const cricket::JsepTransport2* t = GetJsepTransport(transport_name);
+  if (!t) {
+    return nullptr;
+  }
+  return t->GetLocalCertificate();
+}
+
+std::unique_ptr<rtc::SSLCertificate>
+JsepTransportController::GetRemoteSSLCertificate(
+    const std::string& transport_name) const {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<std::unique_ptr<rtc::SSLCertificate>>(
+        RTC_FROM_HERE, [&] { return GetRemoteSSLCertificate(transport_name); });
+  }
+
+  // Get the certificate from the RTP channel's DTLS handshake. Should be
+  // identical to the RTCP channel's, since they were given the same remote
+  // fingerprint.
+  auto dtls = GetDtlsTransport(transport_name);
+  if (!dtls) {
+    return nullptr;
+  }
+
+  return dtls->GetRemoteSSLCertificate();
+}
+
+void JsepTransportController::MaybeStartGathering() {
+  if (!network_thread_->IsCurrent()) {
+    network_thread_->Invoke<void>(RTC_FROM_HERE,
+                                  [&] { MaybeStartGathering(); });
+    return;
+  }
+
+  for (auto& dtls : GetDtlsTransports()) {
+    dtls->ice_transport()->MaybeStartGathering();
+  }
+}
+
+RTCError JsepTransportController::AddRemoteCandidates(
+    const std::string& transport_name,
+    const cricket::Candidates& candidates) {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<RTCError>(RTC_FROM_HERE, [&] {
+      return AddRemoteCandidates(transport_name, candidates);
+    });
+  }
+
+  // Verify each candidate before passing down to the transport layer.
+  RTCError error = VerifyCandidates(candidates);
+  if (!error.ok()) {
+    return error;
+  }
+
+  cricket::JsepTransport2* jsep_transport = GetJsepTransport(transport_name);
+  if (!jsep_transport) {
+    return RTCError(RTCErrorType::INVALID_PARAMETER,
+                    "The transport doesn't exist.");
+  }
+
+  return jsep_transport->AddRemoteCandidates(candidates);
+}
+
+RTCError JsepTransportController::RemoveRemoteCandidates(
+    const cricket::Candidates& candidates) {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<RTCError>(
+        RTC_FROM_HERE, [&] { return RemoveRemoteCandidates(candidates); });
+  }
+
+  // Verify each candidate before passing down to the transport layer.
+  RTCError error = VerifyCandidates(candidates);
+  if (!error.ok()) {
+    return error;
+  }
+
+  std::map<std::string, cricket::Candidates> candidates_by_transport_name;
+  for (const cricket::Candidate& cand : candidates) {
+    if (!cand.transport_name().empty()) {
+      candidates_by_transport_name[cand.transport_name()].push_back(cand);
+    } else {
+      RTC_LOG(LS_ERROR) << "Not removing candidate because it does not have a "
+                           "transport name set: "
+                        << cand.ToString();
+    }
+  }
+
+  for (const auto& kv : candidates_by_transport_name) {
+    const std::string& transport_name = kv.first;
+    const cricket::Candidates& candidates = kv.second;
+    cricket::JsepTransport2* jsep_transport = GetJsepTransport(transport_name);
+    if (!jsep_transport) {
+      return RTCError(RTCErrorType::INVALID_PARAMETER,
+                      "The transport doesn't exist.");
+    }
+    for (const cricket::Candidate& candidate : candidates) {
+      auto dtls = candidate.component() == cricket::ICE_CANDIDATE_COMPONENT_RTP
+                      ? jsep_transport->rtp_dtls_transport()
+                      : jsep_transport->rtcp_dtls_transport();
+      if (dtls) {
+        dtls->ice_transport()->RemoveRemoteCandidate(candidate);
+      }
+    }
+  }
+  return RTCError::OK();
+}
+
+bool JsepTransportController::GetStats(const std::string& transport_name,
+                                       cricket::TransportStats* stats) {
+  if (!network_thread_->IsCurrent()) {
+    return network_thread_->Invoke<bool>(
+        RTC_FROM_HERE, [=] { return GetStats(transport_name, stats); });
+  }
+
+  cricket::JsepTransport2* transport = GetJsepTransport(transport_name);
+  if (!transport) {
+    return false;
+  }
+  return transport->GetStats(stats);
+}
+
+void JsepTransportController::SetMetricsObserver(
+    webrtc::MetricsObserverInterface* metrics_observer) {
+  if (!network_thread_->IsCurrent()) {
+    network_thread_->Invoke<void>(
+        RTC_FROM_HERE, [=] { SetMetricsObserver(metrics_observer); });
+    return;
+  }
+
+  metrics_observer_ = metrics_observer;
+  for (auto& dtls : GetDtlsTransports()) {
+    dtls->ice_transport()->SetMetricsObserver(metrics_observer);
+  }
+}
+
+std::unique_ptr<cricket::DtlsTransportInternal>
+JsepTransportController::CreateDtlsTransport(const std::string& transport_name,
+                                             bool rtcp) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  int component = rtcp ? cricket::ICE_CANDIDATE_COMPONENT_RTCP
+                       : cricket::ICE_CANDIDATE_COMPONENT_RTP;
+
+  std::unique_ptr<cricket::DtlsTransportInternal> dtls;
+  if (config_.external_transport_factory) {
+    auto ice = config_.external_transport_factory->CreateIceTransport(
+        transport_name, component);
+    dtls = config_.external_transport_factory->CreateDtlsTransport(
+        std::move(ice), config_.crypto_options);
+  } else {
+    auto ice = rtc::MakeUnique<cricket::P2PTransportChannel>(
+        transport_name, component, port_allocator_);
+    dtls = rtc::MakeUnique<cricket::DtlsTransport>(std::move(ice),
+                                                   config_.crypto_options);
+  }
+
+  RTC_DCHECK(dtls);
+  dtls->SetSslMaxProtocolVersion(config_.ssl_max_version);
+  dtls->ice_transport()->SetMetricsObserver(metrics_observer_);
+  dtls->ice_transport()->SetIceRole(ice_role_);
+  dtls->ice_transport()->SetIceTiebreaker(ice_tiebreaker_);
+  dtls->ice_transport()->SetIceConfig(ice_config_);
+  if (certificate_) {
+    bool set_cert_success = dtls->SetLocalCertificate(certificate_);
+    RTC_DCHECK(set_cert_success);
+  }
+
+  // Connect to signals offered by the DTLS and ICE transport.
+  dtls->SignalWritableState.connect(
+      this, &JsepTransportController::OnTransportWritableState_n);
+  dtls->SignalReceivingState.connect(
+      this, &JsepTransportController::OnTransportReceivingState_n);
+  dtls->SignalDtlsHandshakeError.connect(
+      this, &JsepTransportController::OnDtlsHandshakeError);
+  dtls->ice_transport()->SignalGatheringState.connect(
+      this, &JsepTransportController::OnTransportGatheringState_n);
+  dtls->ice_transport()->SignalCandidateGathered.connect(
+      this, &JsepTransportController::OnTransportCandidateGathered_n);
+  dtls->ice_transport()->SignalCandidatesRemoved.connect(
+      this, &JsepTransportController::OnTransportCandidatesRemoved_n);
+  dtls->ice_transport()->SignalRoleConflict.connect(
+      this, &JsepTransportController::OnTransportRoleConflict_n);
+  dtls->ice_transport()->SignalStateChanged.connect(
+      this, &JsepTransportController::OnTransportStateChanged_n);
+  return dtls;
+}
+
+std::unique_ptr<webrtc::RtpTransport>
+JsepTransportController::CreateUnencryptedRtpTransport(
+    const std::string& transport_name,
+    rtc::PacketTransportInternal* rtp_packet_transport,
+    rtc::PacketTransportInternal* rtcp_packet_transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  // TODO(zhihuang): Add support of unencrypted RTP for testing.
+  return nullptr;
+}
+
+std::unique_ptr<webrtc::SrtpTransport>
+JsepTransportController::CreateSdesTransport(
+    const std::string& transport_name,
+    rtc::PacketTransportInternal* rtp_packet_transport,
+    rtc::PacketTransportInternal* rtcp_packet_transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  bool rtcp_mux_enabled = rtcp_packet_transport == nullptr;
+  auto srtp_transport =
+      rtc::MakeUnique<webrtc::SrtpTransport>(rtcp_mux_enabled);
+  RTC_DCHECK(rtp_packet_transport);
+  srtp_transport->SetRtpPacketTransport(rtp_packet_transport);
+  if (rtcp_packet_transport) {
+    srtp_transport->SetRtcpPacketTransport(rtp_packet_transport);
+  }
+  if (config_.enable_external_auth) {
+    srtp_transport->EnableExternalAuth();
+  }
+  return srtp_transport;
+}
+
+std::unique_ptr<webrtc::DtlsSrtpTransport>
+JsepTransportController::CreateDtlsSrtpTransport(
+    const std::string& transport_name,
+    cricket::DtlsTransportInternal* rtp_dtls_transport,
+    cricket::DtlsTransportInternal* rtcp_dtls_transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  bool rtcp_mux_enabled = rtcp_dtls_transport == nullptr;
+  auto srtp_transport =
+      rtc::MakeUnique<webrtc::SrtpTransport>(rtcp_mux_enabled);
+  if (config_.enable_external_auth) {
+    srtp_transport->EnableExternalAuth();
+  }
+
+  auto dtls_srtp_transport =
+      rtc::MakeUnique<webrtc::DtlsSrtpTransport>(std::move(srtp_transport));
+
+  dtls_srtp_transport->SetDtlsTransports(rtp_dtls_transport,
+                                         rtcp_dtls_transport);
+  return dtls_srtp_transport;
+}
+
+std::vector<cricket::DtlsTransportInternal*>
+JsepTransportController::GetDtlsTransports() {
+  std::vector<cricket::DtlsTransportInternal*> dtls_transports;
+  for (auto it = jsep_transports_by_mid_.begin();
+       it != jsep_transports_by_mid_.end(); ++it) {
+    auto jsep_transport = it->second.get();
+    RTC_DCHECK(jsep_transport);
+    if (jsep_transport->rtp_dtls_transport()) {
+      dtls_transports.push_back(jsep_transport->rtp_dtls_transport());
+    }
+
+    if (jsep_transport->rtcp_dtls_transport()) {
+      dtls_transports.push_back(jsep_transport->rtcp_dtls_transport());
+    }
+  }
+  return dtls_transports;
+}
+
+void JsepTransportController::OnMessage(rtc::Message* pmsg) {
+  RTC_DCHECK(signaling_thread_->IsCurrent());
+
+  switch (pmsg->message_id) {
+    case MSG_ICECONNECTIONSTATE: {
+      rtc::TypedMessageData<cricket::IceConnectionState>* data =
+          static_cast<rtc::TypedMessageData<cricket::IceConnectionState>*>(
+              pmsg->pdata);
+      SignalIceConnectionState(data->data());
+      delete data;
+      break;
+    }
+    case MSG_ICEGATHERINGSTATE: {
+      rtc::TypedMessageData<cricket::IceGatheringState>* data =
+          static_cast<rtc::TypedMessageData<cricket::IceGatheringState>*>(
+              pmsg->pdata);
+      SignalIceGatheringState(data->data());
+      delete data;
+      break;
+    }
+    case MSG_ICECANDIDATESGATHERED: {
+      CandidatesData* data = static_cast<CandidatesData*>(pmsg->pdata);
+      SignalIceCandidatesGathered(data->transport_name, data->candidates);
+      delete data;
+      break;
+    }
+    default:
+      RTC_NOTREACHED();
+  }
+}
+
+RTCError JsepTransportController::ApplyDescription_n(
+    bool local,
+    SdpType type,
+    const cricket::SessionDescription* description) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_DCHECK(description);
+
+  if (local) {
+    local_desc_ = description;
+  } else {
+    remote_desc_ = description;
+  }
+
+  if (ShouldUpdateBundleGroup(type, description)) {
+    bundle_group_ = *description->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  }
+
+  std::vector<int> merged_encrypted_extension_ids;
+  if (bundle_group_) {
+    merged_encrypted_extension_ids =
+        MergeEncryptedHeaderExtensionIdsForBundle(description);
+  }
+
+  for (const cricket::ContentInfo& content_info : description->contents()) {
+    // Don't create transports for rejected m-lines and bundled m-lines."
+    if (content_info.rejected ||
+        (IsBundled(content_info.name) && content_info.name != *bundled_mid())) {
+      continue;
+    }
+    MaybeCreateJsepTransport(content_info.name, content_info);
+  }
+
+  RTC_DCHECK(description->contents().size() ==
+             description->transport_infos().size());
+  for (size_t i = 0; i < description->contents().size(); ++i) {
+    const cricket::ContentInfo& content_info = description->contents()[i];
+    const cricket::TransportInfo& transport_info =
+        description->transport_infos()[i];
+    if (content_info.rejected) {
+      HandleRejectedContent(content_info);
+      continue;
+    }
+
+    if (IsBundled(content_info.name) && content_info.name != *bundled_mid()) {
+      HandleBundledContent(content_info);
+      continue;
+    }
+
+    std::vector<int> extension_ids;
+    if (bundle_group_ && content_info.name == *bundled_mid()) {
+      extension_ids = merged_encrypted_extension_ids;
+    } else {
+      extension_ids = GetEncryptedHeaderExtensionIds(content_info);
+    }
+
+    cricket::JsepTransport2* transport = GetJsepTransport(content_info.name);
+    RTC_DCHECK(transport);
+
+    SetIceRole_n(DetermineIceRole(transport, transport_info, type, local));
+
+    RTCError error;
+    cricket::JsepTransportDescription jsep_description =
+        CreateJsepTransportDescription(content_info, transport_info,
+                                       extension_ids);
+    if (local) {
+      error =
+          transport->SetLocalJsepTransportDescription(jsep_description, type);
+    } else {
+      error =
+          transport->SetRemoteJsepTransportDescription(jsep_description, type);
+    }
+
+    if (!error.ok()) {
+      LOG_AND_RETURN_ERROR(RTCErrorType::INVALID_PARAMETER,
+                           "Failed to apply the description for " +
+                               content_info.name + ": " + error.message());
+    }
+  }
+  return RTCError::OK();
+}
+
+void JsepTransportController::HandleRejectedContent(
+    const cricket::ContentInfo& content_info) {
+  // If the content is rejected, let the
+  // BaseChannel/SctpTransport change the RtpTransport/DtlsTransport first,
+  // then destroy the cricket::JsepTransport2.
+  if (content_info.type == cricket::MediaProtocolType::kRtp) {
+    SignalRtpTransportChanged(content_info.name, nullptr);
+  } else {
+    SignalDtlsTransportChanged(content_info.name, nullptr);
+  }
+  // Remove the rejected content from the |bundle_group_|.
+  if (IsBundled(content_info.name)) {
+    bundle_group_->RemoveContentName(content_info.name);
+  }
+  MaybeDestroyJsepTransport(content_info.name);
+}
+
+void JsepTransportController::HandleBundledContent(
+    const cricket::ContentInfo& content_info) {
+  // If the content is bundled, let the
+  // BaseChannel/SctpTransport change the RtpTransport/DtlsTransport first,
+  // then destroy the cricket::JsepTransport2.
+  if (content_info.type == cricket::MediaProtocolType::kRtp) {
+    auto rtp_transport =
+        jsep_transports_by_mid_[*bundled_mid()]->rtp_transport();
+    SignalRtpTransportChanged(content_info.name, rtp_transport);
+  } else {
+    auto dtls_transport =
+        jsep_transports_by_mid_[*bundled_mid()]->rtp_dtls_transport();
+    SignalDtlsTransportChanged(content_info.name, dtls_transport);
+  }
+  MaybeDestroyJsepTransport(content_info.name);
+}
+
+cricket::JsepTransportDescription
+JsepTransportController::CreateJsepTransportDescription(
+    cricket::ContentInfo content_info,
+    cricket::TransportInfo transport_info,
+    const std::vector<int>& encrypted_extension_ids) {
+  const cricket::MediaContentDescription* content_desc =
+      static_cast<const cricket::MediaContentDescription*>(
+          content_info.description);
+  RTC_DCHECK(content_desc);
+  bool rtcp_mux_enabled = content_info.type == cricket::MediaProtocolType::kSctp
+                              ? true
+                              : content_desc->rtcp_mux();
+
+  return cricket::JsepTransportDescription(
+      rtcp_mux_enabled, content_desc->cryptos(), encrypted_extension_ids,
+      transport_info.description);
+}
+
+bool JsepTransportController::ShouldUpdateBundleGroup(
+    SdpType type,
+    const cricket::SessionDescription* description) {
+  if (config_.bundle_policy ==
+      PeerConnectionInterface::kBundlePolicyMaxBundle) {
+    return true;
+  }
+
+  if (type != SdpType::kAnswer) {
+    return false;
+  }
+
+  RTC_DCHECK(local_desc_ && remote_desc_);
+  const cricket::ContentGroup* local_bundle =
+      local_desc_->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  const cricket::ContentGroup* remote_bundle =
+      remote_desc_->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  return local_bundle && remote_bundle;
+}
+
+std::vector<int> JsepTransportController::GetEncryptedHeaderExtensionIds(
+    const cricket::ContentInfo& content_info) {
+  const cricket::MediaContentDescription* content_desc =
+      static_cast<const cricket::MediaContentDescription*>(
+          content_info.description);
+
+  if (!config_.crypto_options.enable_encrypted_rtp_header_extensions) {
+    return std::vector<int>();
+  }
+
+  std::vector<int> encrypted_header_extension_ids;
+  for (auto extension : content_desc->rtp_header_extensions()) {
+    if (!extension.encrypt) {
+      continue;
+    }
+    auto it = std::find(encrypted_header_extension_ids.begin(),
+                        encrypted_header_extension_ids.end(), extension.id);
+    if (it == encrypted_header_extension_ids.end()) {
+      encrypted_header_extension_ids.push_back(extension.id);
+    }
+  }
+  return encrypted_header_extension_ids;
+}
+
+std::vector<int>
+JsepTransportController::MergeEncryptedHeaderExtensionIdsForBundle(
+    const cricket::SessionDescription* description) {
+  RTC_DCHECK(description);
+  RTC_DCHECK(bundle_group_);
+
+  std::vector<int> merged_ids;
+  // Union the encrypted header IDs in the group when bundle is enabled.
+  for (const cricket::ContentInfo& content_info : description->contents()) {
+    if (bundle_group_->HasContentName(content_info.name)) {
+      std::vector<int> extension_ids =
+          GetEncryptedHeaderExtensionIds(content_info);
+      for (int id : extension_ids) {
+        auto it = std::find(merged_ids.begin(), merged_ids.end(), id);
+        if (it == merged_ids.end()) {
+          merged_ids.push_back(id);
+        }
+      }
+    }
+  }
+  return merged_ids;
+}
+
+const cricket::JsepTransport2* JsepTransportController::GetJsepTransport(
+    const std::string& mid) const {
+  auto target_mid = mid;
+  if (IsBundled(mid)) {
+    target_mid = *bundled_mid();
+  }
+  auto it = jsep_transports_by_mid_.find(target_mid);
+  return (it == jsep_transports_by_mid_.end()) ? nullptr : it->second.get();
+}
+
+cricket::JsepTransport2* JsepTransportController::GetJsepTransport(
+    const std::string& mid) {
+  auto target_mid = mid;
+  if (IsBundled(mid)) {
+    target_mid = *bundled_mid();
+  }
+  auto it = jsep_transports_by_mid_.find(target_mid);
+  return (it == jsep_transports_by_mid_.end()) ? nullptr : it->second.get();
+}
+
+void JsepTransportController::MaybeCreateJsepTransport(
+    const std::string& mid,
+    const cricket::ContentInfo& content_info) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+
+  cricket::JsepTransport2* transport = GetJsepTransport(mid);
+  if (transport) {
+    return;
+  }
+
+  const cricket::MediaContentDescription* content_desc =
+      static_cast<const cricket::MediaContentDescription*>(
+          content_info.description);
+  bool rtcp_mux_enabled =
+      content_desc->rtcp_mux() ||
+      config_.rtcp_mux_policy == PeerConnectionInterface::kRtcpMuxPolicyRequire;
+  std::unique_ptr<cricket::DtlsTransportInternal> rtp_dtls_transport =
+      CreateDtlsTransport(mid, /*rtcp = */ false);
+  std::unique_ptr<cricket::DtlsTransportInternal> rtcp_dtls_transport;
+  if (!rtcp_mux_enabled) {
+    rtcp_dtls_transport = CreateDtlsTransport(mid, /*rtcp = */ true);
+  }
+
+  std::unique_ptr<RtpTransport> unencrypted_rtp_transport;
+  std::unique_ptr<SrtpTransport> sdes_transport;
+  std::unique_ptr<DtlsSrtpTransport> dtls_srtp_transport;
+  if (config_.disable_encryption) {
+    unencrypted_rtp_transport = CreateUnencryptedRtpTransport(
+        mid, rtp_dtls_transport.get(), rtcp_dtls_transport.get());
+  } else if (!content_desc->cryptos().empty()) {
+    sdes_transport = CreateSdesTransport(mid, rtp_dtls_transport.get(),
+                                         rtcp_dtls_transport.get());
+  } else {
+    dtls_srtp_transport = CreateDtlsSrtpTransport(mid, rtp_dtls_transport.get(),
+                                                  rtcp_dtls_transport.get());
+  }
+
+  std::unique_ptr<cricket::JsepTransport2> jsep_transport =
+      rtc::MakeUnique<cricket::JsepTransport2>(
+          mid, certificate_, std::move(unencrypted_rtp_transport),
+          std::move(sdes_transport), std::move(dtls_srtp_transport),
+          std::move(rtp_dtls_transport), std::move(rtcp_dtls_transport));
+  jsep_transport->SignalRtcpMuxActive.connect(
+      this, &JsepTransportController::UpdateAggregateStates_n);
+  jsep_transports_by_mid_[mid] = std::move(jsep_transport);
+  UpdateAggregateStates_n();
+}
+
+void JsepTransportController::MaybeDestroyJsepTransport(
+    const std::string& mid) {
+  jsep_transports_by_mid_.erase(mid);
+  UpdateAggregateStates_n();
+}
+
+void JsepTransportController::DestroyAllJsepTransports_n() {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  jsep_transports_by_mid_.clear();
+}
+
+void JsepTransportController::SetIceRole_n(cricket::IceRole ice_role) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+
+  ice_role_ = ice_role;
+  for (auto& dtls : GetDtlsTransports()) {
+    dtls->ice_transport()->SetIceRole(ice_role_);
+  }
+}
+
+cricket::IceRole JsepTransportController::DetermineIceRole(
+    cricket::JsepTransport2* jsep_transport,
+    const cricket::TransportInfo& transport_info,
+    SdpType type,
+    bool local) {
+  cricket::IceRole ice_role = ice_role_;
+  auto tdesc = transport_info.description;
+  if (local) {
+    // The initial offer side may use ICE Lite, in which case, per RFC5245
+    // Section 5.1.1, the answer side should take the controlling role if it is
+    // in the full ICE mode.
+    //
+    // When both sides use ICE Lite, the initial offer side must take the
+    // controlling role, and this is the default logic implemented in
+    // SetLocalDescription in JsepTransportController.
+    if (jsep_transport->remote_description() &&
+        jsep_transport->remote_description()->transport_desc.ice_mode ==
+            cricket::ICEMODE_LITE &&
+        ice_role_ == cricket::ICEROLE_CONTROLLED &&
+        tdesc.ice_mode == cricket::ICEMODE_FULL) {
+      ice_role = cricket::ICEROLE_CONTROLLING;
+    }
+
+    // Older versions of Chrome expect the ICE role to be re-determined when an
+    // ICE restart occurs, and also don't perform conflict resolution correctly,
+    // so for now we can't safely stop doing this, unless the application opts
+    // in by setting |config_.redetermine_role_on_ice_restart_| to false. See:
+    // https://bugs.chromium.org/p/chromium/issues/detail?id=628676
+    // TODO(deadbeef): Remove this when these old versions of Chrome reach a low
+    // enough population.
+    if (config_.redetermine_role_on_ice_restart &&
+        jsep_transport->local_description() &&
+        cricket::IceCredentialsChanged(
+            jsep_transport->local_description()->transport_desc.ice_ufrag,
+            jsep_transport->local_description()->transport_desc.ice_pwd,
+            tdesc.ice_ufrag, tdesc.ice_pwd) &&
+        // Don't change the ICE role if the remote endpoint is ICE lite; we
+        // should always be controlling in that case.
+        (!jsep_transport->remote_description() ||
+         jsep_transport->remote_description()->transport_desc.ice_mode !=
+             cricket::ICEMODE_LITE)) {
+      ice_role = (type == SdpType::kOffer) ? cricket::ICEROLE_CONTROLLING
+                                           : cricket::ICEROLE_CONTROLLED;
+    }
+  } else {
+    // If our role is cricket::ICEROLE_CONTROLLED and the remote endpoint
+    // supports only ice_lite, this local endpoint should take the CONTROLLING
+    // role.
+    // TODO(deadbeef): This is a session-level attribute, so it really shouldn't
+    // be in a TransportDescription in the first place...
+    if (ice_role_ == cricket::ICEROLE_CONTROLLED &&
+        tdesc.ice_mode == cricket::ICEMODE_LITE) {
+      ice_role = cricket::ICEROLE_CONTROLLING;
+    }
+
+    // If we use ICE Lite and the remote endpoint uses the full implementation
+    // of ICE, the local endpoint must take the controlled role, and the other
+    // side must be the controlling role.
+    if (jsep_transport->local_description() &&
+        jsep_transport->local_description()->transport_desc.ice_mode ==
+            cricket::ICEMODE_LITE &&
+        ice_role_ == cricket::ICEROLE_CONTROLLING &&
+        tdesc.ice_mode == cricket::ICEMODE_LITE) {
+      ice_role = cricket::ICEROLE_CONTROLLED;
+    }
+  }
+
+  return ice_role;
+}
+
+void JsepTransportController::OnTransportWritableState_n(
+    rtc::PacketTransportInternal* transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_LOG(LS_INFO) << " Transport " << transport->transport_name()
+                   << " writability changed to " << transport->writable()
+                   << ".";
+  UpdateAggregateStates_n();
+}
+
+void JsepTransportController::OnTransportReceivingState_n(
+    rtc::PacketTransportInternal* transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  UpdateAggregateStates_n();
+}
+
+void JsepTransportController::OnTransportGatheringState_n(
+    cricket::IceTransportInternal* transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  UpdateAggregateStates_n();
+}
+
+void JsepTransportController::OnTransportCandidateGathered_n(
+    cricket::IceTransportInternal* transport,
+    const cricket::Candidate& candidate) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+
+  // We should never signal peer-reflexive candidates.
+  if (candidate.type() == cricket::PRFLX_PORT_TYPE) {
+    RTC_NOTREACHED();
+    return;
+  }
+  std::vector<cricket::Candidate> candidates;
+  candidates.push_back(candidate);
+  CandidatesData* data =
+      new CandidatesData(transport->transport_name(), candidates);
+  signaling_thread_->Post(RTC_FROM_HERE, this, MSG_ICECANDIDATESGATHERED, data);
+}
+
+void JsepTransportController::OnTransportCandidatesRemoved_n(
+    cricket::IceTransportInternal* transport,
+    const cricket::Candidates& candidates) {
+  invoker_.AsyncInvoke<void>(
+      RTC_FROM_HERE, signaling_thread_,
+      rtc::Bind(&JsepTransportController::OnTransportCandidatesRemoved, this,
+                candidates));
+}
+
+void JsepTransportController::OnTransportCandidatesRemoved(
+    const cricket::Candidates& candidates) {
+  RTC_DCHECK(signaling_thread_->IsCurrent());
+  SignalIceCandidatesRemoved(candidates);
+}
+
+void JsepTransportController::OnTransportRoleConflict_n(
+    cricket::IceTransportInternal* transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  // Note: since the role conflict is handled entirely on the network thread,
+  // we don't need to worry about role conflicts occurring on two ports at
+  // once. The first one encountered should immediately reverse the role.
+  cricket::IceRole reversed_role = (ice_role_ == cricket::ICEROLE_CONTROLLING)
+                                       ? cricket::ICEROLE_CONTROLLED
+                                       : cricket::ICEROLE_CONTROLLING;
+  RTC_LOG(LS_INFO) << "Got role conflict; switching to "
+                   << (reversed_role == cricket::ICEROLE_CONTROLLING
+                           ? "controlling"
+                           : "controlled")
+                   << " role.";
+  SetIceRole_n(reversed_role);
+}
+
+void JsepTransportController::OnTransportStateChanged_n(
+    cricket::IceTransportInternal* transport) {
+  RTC_DCHECK(network_thread_->IsCurrent());
+  RTC_LOG(LS_INFO) << transport->transport_name() << " Transport "
+                   << transport->component()
+                   << " state changed. Check if state is complete.";
+  UpdateAggregateStates_n();
+}
+
+void JsepTransportController::UpdateAggregateStates_n() {
+  RTC_DCHECK(network_thread_->IsCurrent());
+
+  auto dtls_transports = GetDtlsTransports();
+  cricket::IceConnectionState new_connection_state =
+      cricket::kIceConnectionConnecting;
+  cricket::IceGatheringState new_gathering_state = cricket::kIceGatheringNew;
+  bool any_failed = false;
+  bool all_connected = !dtls_transports.empty();
+  bool all_completed = !dtls_transports.empty();
+  bool any_gathering = false;
+  bool all_done_gathering = !dtls_transports.empty();
+  for (const auto& dtls : dtls_transports) {
+    any_failed = any_failed || dtls->ice_transport()->GetState() ==
+                                   cricket::IceTransportState::STATE_FAILED;
+    all_connected = all_connected && dtls->writable();
+    all_completed =
+        all_completed && dtls->writable() &&
+        dtls->ice_transport()->GetState() ==
+            cricket::IceTransportState::STATE_COMPLETED &&
+        dtls->ice_transport()->GetIceRole() == cricket::ICEROLE_CONTROLLING &&
+        dtls->ice_transport()->gathering_state() ==
+            cricket::kIceGatheringComplete;
+    any_gathering = any_gathering || dtls->ice_transport()->gathering_state() !=
+                                         cricket::kIceGatheringNew;
+    all_done_gathering =
+        all_done_gathering && dtls->ice_transport()->gathering_state() ==
+                                  cricket::kIceGatheringComplete;
+  }
+  if (any_failed) {
+    new_connection_state = cricket::kIceConnectionFailed;
+  } else if (all_completed) {
+    new_connection_state = cricket::kIceConnectionCompleted;
+  } else if (all_connected) {
+    new_connection_state = cricket::kIceConnectionConnected;
+  }
+  if (ice_connection_state_ != new_connection_state) {
+    ice_connection_state_ = new_connection_state;
+    signaling_thread_->Post(
+        RTC_FROM_HERE, this, MSG_ICECONNECTIONSTATE,
+        new rtc::TypedMessageData<cricket::IceConnectionState>(
+            new_connection_state));
+  }
+
+  if (all_done_gathering) {
+    new_gathering_state = cricket::kIceGatheringComplete;
+  } else if (any_gathering) {
+    new_gathering_state = cricket::kIceGatheringGathering;
+  }
+  if (ice_gathering_state_ != new_gathering_state) {
+    ice_gathering_state_ = new_gathering_state;
+    signaling_thread_->Post(
+        RTC_FROM_HERE, this, MSG_ICEGATHERINGSTATE,
+        new rtc::TypedMessageData<cricket::IceGatheringState>(
+            new_gathering_state));
+  }
+}
+
+void JsepTransportController::OnDtlsHandshakeError(
+    rtc::SSLHandshakeError error) {
+  SignalDtlsHandshakeError(error);
+}
+
+}  // namespace webrtc
diff --git a/pc/jseptransportcontroller.h b/pc/jseptransportcontroller.h
new file mode 100644
index 0000000..9755f4c
--- /dev/null
+++ b/pc/jseptransportcontroller.h
@@ -0,0 +1,295 @@
+/*
+ *  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.
+ */
+
+#ifndef PC_JSEPTRANSPORTCONTROLLER_H_
+#define PC_JSEPTRANSPORTCONTROLLER_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "api/candidate.h"
+#include "api/peerconnectioninterface.h"
+#include "media/sctp/sctptransportinternal.h"
+#include "p2p/base/dtlstransport.h"
+#include "p2p/base/p2ptransportchannel.h"
+#include "p2p/base/transportfactoryinterface.h"
+#include "pc/channel.h"
+#include "pc/dtlssrtptransport.h"
+#include "pc/jseptransport2.h"
+#include "pc/rtptransport.h"
+#include "pc/srtptransport.h"
+#include "rtc_base/asyncinvoker.h"
+#include "rtc_base/constructormagic.h"
+#include "rtc_base/refcountedobject.h"
+#include "rtc_base/sigslot.h"
+#include "rtc_base/sslstreamadapter.h"
+
+namespace rtc {
+class Thread;
+class PacketTransportInternal;
+}  // namespace rtc
+
+namespace webrtc {
+
+class JsepTransportController : public sigslot::has_slots<>,
+                                public rtc::MessageHandler {
+ public:
+  struct Config {
+    // If |redetermine_role_on_ice_restart| is true, ICE role is redetermined
+    // upon setting a local transport description that indicates an ICE
+    // restart.
+    bool redetermine_role_on_ice_restart = true;
+    rtc::SSLProtocolVersion ssl_max_version = rtc::SSL_PROTOCOL_DTLS_12;
+    // |crypto_options| is used to determine if created DTLS transports
+    // negotiate GCM crypto suites or not.
+    rtc::CryptoOptions crypto_options;
+    PeerConnectionInterface::BundlePolicy bundle_policy =
+        PeerConnectionInterface::kBundlePolicyBalanced;
+    PeerConnectionInterface::RtcpMuxPolicy rtcp_mux_policy =
+        PeerConnectionInterface::kRtcpMuxPolicyRequire;
+    bool disable_encryption = false;
+    bool enable_external_auth = false;
+    // Used to inject the ICE/DTLS transports created externally.
+    cricket::TransportFactoryInterface* external_transport_factory = nullptr;
+  };
+
+  // The ICE related events are signaled on the |signaling_thread|.
+  // All the transport related methods are called on the |network_thread|.
+  JsepTransportController(rtc::Thread* signaling_thread,
+                          rtc::Thread* network_thread,
+                          cricket::PortAllocator* port_allocator,
+                          Config config);
+  virtual ~JsepTransportController();
+
+  // The main method to be called; applies a description at the transport
+  // level, creating/destroying transport objects as needed and updating their
+  // properties. This includes RTP, DTLS, and ICE (but not SCTP). At least not
+  // yet? May make sense to in the future.
+  RTCError SetLocalDescription(SdpType type,
+                               const cricket::SessionDescription* description);
+
+  RTCError SetRemoteDescription(SdpType type,
+                                const cricket::SessionDescription* description);
+
+  // Get transports to be used for the provided |mid|. If bundling is enabled,
+  // calling GetRtpTransport for multiple MIDs may yield the same object.
+  RtpTransportInternal* GetRtpTransport(const std::string& mid) const;
+  cricket::DtlsTransportInternal* GetDtlsTransport(
+      const std::string& mid) const;
+  cricket::DtlsTransportInternal* GetRtcpDtlsTransport(
+      const std::string& mid) const;
+
+  /*********************
+   * ICE-related methods
+   ********************/
+  // This method is public to allow PeerConnection to update it from
+  // SetConfiguration.
+  void SetIceConfig(const cricket::IceConfig& config);
+  // Set the "needs-ice-restart" flag as described in JSEP. After the flag is
+  // set, offers should generate new ufrags/passwords until an ICE restart
+  // occurs.
+  void SetNeedsIceRestartFlag();
+  // Returns true if the ICE restart flag above was set, and no ICE restart has
+  // occurred yet for this transport (by applying a local description with
+  // changed ufrag/password). If the transport has been deleted as a result of
+  // bundling, returns false.
+  bool NeedsIceRestart(const std::string& mid) const;
+  // Start gathering candidates for any new transports, or transports doing an
+  // ICE restart.
+  void MaybeStartGathering();
+  RTCError AddRemoteCandidates(
+      const std::string& mid,
+      const std::vector<cricket::Candidate>& candidates);
+  RTCError RemoveRemoteCandidates(
+      const std::vector<cricket::Candidate>& candidates);
+
+  /**********************
+   * DTLS-related methods
+   *********************/
+  // Specifies the identity to use in this session.
+  // Can only be called once.
+  bool SetLocalCertificate(
+      const rtc::scoped_refptr<rtc::RTCCertificate>& certificate);
+  rtc::scoped_refptr<rtc::RTCCertificate> GetLocalCertificate(
+      const std::string& mid) const;
+  // Caller owns returned certificate. This method mainly exists for stats
+  // reporting.
+  std::unique_ptr<rtc::SSLCertificate> GetRemoteSSLCertificate(
+      const std::string& mid) const;
+  // Get negotiated role, if one has been negotiated.
+  rtc::Optional<rtc::SSLRole> GetDtlsRole(const std::string& mid) const;
+
+  // TODO(deadbeef): GetStats isn't const because all the way down to
+  // OpenSSLStreamAdapter, GetSslCipherSuite and GetDtlsSrtpCryptoSuite are not
+  // const. Fix this.
+  bool GetStats(const std::string& mid, cricket::TransportStats* stats);
+  void SetMetricsObserver(webrtc::MetricsObserverInterface* metrics_observer);
+
+  // All of these signals are fired on the signaling thread.
+
+  // If any transport failed => failed,
+  // Else if all completed => completed,
+  // Else if all connected => connected,
+  // Else => connecting
+  sigslot::signal1<cricket::IceConnectionState> SignalIceConnectionState;
+
+  // If all transports done gathering => complete,
+  // Else if any are gathering => gathering,
+  // Else => new
+  sigslot::signal1<cricket::IceGatheringState> SignalIceGatheringState;
+
+  // (mid, candidates)
+  sigslot::signal2<const std::string&, const std::vector<cricket::Candidate>&>
+      SignalIceCandidatesGathered;
+
+  sigslot::signal1<const std::vector<cricket::Candidate>&>
+      SignalIceCandidatesRemoved;
+
+  sigslot::signal1<rtc::SSLHandshakeError> SignalDtlsHandshakeError;
+
+  // This will be fired when BUNDLE is enabled, the PeerConnection will handle
+  // the signal and set the RtpTransport for the BaseChannel.
+  // The first argument is the MID and the second is the new RtpTransport.
+  // Before firing this signal, the previous RtpTransport must no longer be
+  // referenced.
+  sigslot::signal2<const std::string&, RtpTransportInternal*>
+      SignalRtpTransportChanged;
+
+  // SCTP version of the signal above. PeerConnection will set a new
+  // DtlsTransport for the SctpTransport.
+  sigslot::signal2<const std::string&, cricket::DtlsTransportInternal*>
+      SignalDtlsTransportChanged;
+
+ private:
+  void OnMessage(rtc::Message* pmsg) override;
+
+  RTCError ApplyDescription_n(bool local,
+                              SdpType type,
+                              const cricket::SessionDescription* description);
+
+  void HandleRejectedContent(const cricket::ContentInfo& content_info);
+  void HandleBundledContent(const cricket::ContentInfo& content_info);
+
+  cricket::JsepTransportDescription CreateJsepTransportDescription(
+      cricket::ContentInfo content_info,
+      cricket::TransportInfo transport_info,
+      const std::vector<int>& encrypted_extension_ids);
+
+  rtc::Optional<std::string> bundled_mid() const {
+    rtc::Optional<std::string> bundled_mid;
+    if (bundle_group_) {
+      bundled_mid = std::move(*(bundle_group_->FirstContentName()));
+    }
+    return bundled_mid;
+  }
+
+  bool IsBundled(const std::string& mid) const {
+    return bundle_group_ && bundle_group_->HasContentName(mid);
+  }
+
+  bool ShouldUpdateBundleGroup(SdpType type,
+                               const cricket::SessionDescription* description);
+
+  std::vector<int> MergeEncryptedHeaderExtensionIdsForBundle(
+      const cricket::SessionDescription* description);
+
+  std::vector<int> GetEncryptedHeaderExtensionIds(
+      const cricket::ContentInfo& content_info);
+
+  const cricket::JsepTransport2* GetJsepTransport(const std::string& mid) const;
+  cricket::JsepTransport2* GetJsepTransport(const std::string& mid);
+
+  void MaybeCreateJsepTransport(const std::string& mid,
+                                const cricket::ContentInfo& content_info);
+  void MaybeDestroyJsepTransport(const std::string& mid);
+  void DestroyAllJsepTransports_n();
+
+  void SetIceRole_n(cricket::IceRole ice_role);
+
+  cricket::IceRole DetermineIceRole(
+      cricket::JsepTransport2* jsep_transport,
+      const cricket::TransportInfo& transport_info,
+      SdpType type,
+      bool local);
+
+  std::unique_ptr<cricket::DtlsTransportInternal> CreateDtlsTransport(
+      const std::string& transport_name,
+      bool rtcp);
+
+  std::unique_ptr<webrtc::RtpTransport> CreateUnencryptedRtpTransport(
+      const std::string& transport_name,
+      rtc::PacketTransportInternal* rtp_packet_transport,
+      rtc::PacketTransportInternal* rtcp_packet_transport);
+  std::unique_ptr<webrtc::SrtpTransport> CreateSdesTransport(
+      const std::string& transport_name,
+      rtc::PacketTransportInternal* rtp_packet_transport,
+      rtc::PacketTransportInternal* rtcp_packet_transport);
+  std::unique_ptr<webrtc::DtlsSrtpTransport> CreateDtlsSrtpTransport(
+      const std::string& transport_name,
+      cricket::DtlsTransportInternal* rtp_dtls_transport,
+      cricket::DtlsTransportInternal* rtcp_dtls_transport);
+
+  // Collect all the DtlsTransports, including RTP and RTCP, from the
+  // JsepTransports. JsepTransportController can iterate all the DtlsTransports
+  // and update the aggregate states.
+  std::vector<cricket::DtlsTransportInternal*> GetDtlsTransports();
+
+  // Handlers for signals from Transport.
+  void OnTransportWritableState_n(rtc::PacketTransportInternal* transport);
+  void OnTransportReceivingState_n(rtc::PacketTransportInternal* transport);
+  void OnTransportGatheringState_n(cricket::IceTransportInternal* transport);
+  void OnTransportCandidateGathered_n(cricket::IceTransportInternal* transport,
+                                      const cricket::Candidate& candidate);
+  void OnTransportCandidatesRemoved(const cricket::Candidates& candidates);
+  void OnTransportCandidatesRemoved_n(cricket::IceTransportInternal* transport,
+                                      const cricket::Candidates& candidates);
+  void OnTransportRoleConflict_n(cricket::IceTransportInternal* transport);
+  void OnTransportStateChanged_n(cricket::IceTransportInternal* transport);
+
+  void UpdateAggregateStates_n();
+
+  void OnDtlsHandshakeError(rtc::SSLHandshakeError error);
+
+  rtc::Thread* const signaling_thread_ = nullptr;
+  rtc::Thread* const network_thread_ = nullptr;
+  cricket::PortAllocator* const port_allocator_ = nullptr;
+
+  std::map<std::string, std::unique_ptr<cricket::JsepTransport2>>
+      jsep_transports_by_mid_;
+
+  // Aggregate state for TransportChannelImpls.
+  cricket::IceConnectionState ice_connection_state_ =
+      cricket::kIceConnectionConnecting;
+  cricket::IceGatheringState ice_gathering_state_ = cricket::kIceGatheringNew;
+
+  Config config_;
+  const cricket::SessionDescription* local_desc_ = nullptr;
+  const cricket::SessionDescription* remote_desc_ = nullptr;
+  rtc::Optional<bool> initial_offerer_;
+
+  rtc::Optional<cricket::ContentGroup> bundle_group_;
+
+  cricket::IceConfig ice_config_;
+  cricket::IceRole ice_role_ = cricket::ICEROLE_CONTROLLING;
+  uint64_t ice_tiebreaker_ = rtc::CreateRandomId64();
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate_;
+  rtc::AsyncInvoker invoker_;
+
+  webrtc::MetricsObserverInterface* metrics_observer_ = nullptr;
+
+  RTC_DISALLOW_COPY_AND_ASSIGN(JsepTransportController);
+};
+
+}  // namespace webrtc
+
+#endif  // PC_JSEPTRANSPORTCONTROLLER_H_
diff --git a/pc/jseptransportcontroller_unittest.cc b/pc/jseptransportcontroller_unittest.cc
new file mode 100644
index 0000000..887c4be
--- /dev/null
+++ b/pc/jseptransportcontroller_unittest.cc
@@ -0,0 +1,1210 @@
+/*
+ *  Copyright 2018 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 <map>
+#include <memory>
+
+#include "p2p/base/fakedtlstransport.h"
+#include "p2p/base/fakeicetransport.h"
+#include "p2p/base/transportfactoryinterface.h"
+#include "p2p/base/transportinfo.h"
+#include "pc/jseptransportcontroller.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/ptr_util.h"
+#include "rtc_base/thread.h"
+#include "test/gtest.h"
+
+using cricket::FakeDtlsTransport;
+using cricket::Candidate;
+using cricket::Candidates;
+using webrtc::SdpType;
+
+static const int kTimeout = 100;
+static const char kIceUfrag1[] = "u0001";
+static const char kIcePwd1[] = "TESTICEPWD00000000000001";
+static const char kIceUfrag2[] = "u0002";
+static const char kIcePwd2[] = "TESTICEPWD00000000000002";
+static const char kIceUfrag3[] = "u0003";
+static const char kIcePwd3[] = "TESTICEPWD00000000000003";
+static const char kAudioMid1[] = "audio1";
+static const char kAudioMid2[] = "audio2";
+static const char kVideoMid1[] = "video1";
+static const char kVideoMid2[] = "video2";
+static const char kDataMid1[] = "data1";
+
+namespace webrtc {
+
+class FakeTransportFactory : public cricket::TransportFactoryInterface {
+ public:
+  std::unique_ptr<cricket::IceTransportInternal> CreateIceTransport(
+      const std::string& transport_name,
+      int component) override {
+    return rtc::MakeUnique<cricket::FakeIceTransport>(transport_name,
+                                                      component);
+  }
+
+  std::unique_ptr<cricket::DtlsTransportInternal> CreateDtlsTransport(
+      std::unique_ptr<cricket::IceTransportInternal> ice,
+      const rtc::CryptoOptions& crypto_options) override {
+    std::unique_ptr<cricket::FakeIceTransport> fake_ice(
+        static_cast<cricket::FakeIceTransport*>(ice.release()));
+    return rtc::MakeUnique<FakeDtlsTransport>(std::move(fake_ice));
+  }
+};
+
+class JsepTransportControllerTest : public testing::Test,
+                                    public sigslot::has_slots<> {
+ public:
+  JsepTransportControllerTest() : signaling_thread_(rtc::Thread::Current()) {
+    fake_transport_factory_ = rtc::MakeUnique<FakeTransportFactory>();
+  }
+
+  void CreateJsepTransportController(
+      JsepTransportController::Config config,
+      rtc::Thread* signaling_thread = rtc::Thread::Current(),
+      rtc::Thread* network_thread = rtc::Thread::Current(),
+      cricket::PortAllocator* port_allocator = nullptr) {
+    // The tests only works with |fake_transport_factory|;
+    config.external_transport_factory = fake_transport_factory_.get();
+    transport_controller_ = rtc::MakeUnique<JsepTransportController>(
+        signaling_thread, network_thread, port_allocator, config);
+    ConnectTransportControllerSignals();
+  }
+
+  void ConnectTransportControllerSignals() {
+    transport_controller_->SignalIceConnectionState.connect(
+        this, &JsepTransportControllerTest::OnConnectionState);
+    transport_controller_->SignalIceGatheringState.connect(
+        this, &JsepTransportControllerTest::OnGatheringState);
+    transport_controller_->SignalIceCandidatesGathered.connect(
+        this, &JsepTransportControllerTest::OnCandidatesGathered);
+    transport_controller_->SignalRtpTransportChanged.connect(
+        this, &JsepTransportControllerTest::OnRtpTransportChanged);
+    transport_controller_->SignalDtlsTransportChanged.connect(
+        this, &JsepTransportControllerTest::OnDtlsTransportChanged);
+  }
+
+  std::unique_ptr<cricket::SessionDescription>
+  CreateSessionDescriptionWithoutBundle() {
+    auto description = rtc::MakeUnique<cricket::SessionDescription>();
+    AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                    cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                    nullptr);
+    AddVideoSection(description.get(), kVideoMid1, kIceUfrag1, kIcePwd1,
+                    cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                    nullptr);
+    return description;
+  }
+
+  std::unique_ptr<cricket::SessionDescription>
+  CreateSessionDescriptionWithBundleGroup() {
+    auto description = CreateSessionDescriptionWithoutBundle();
+    cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+    bundle_group.AddContentName(kAudioMid1);
+    bundle_group.AddContentName(kVideoMid1);
+    description->AddGroup(bundle_group);
+
+    return description;
+  }
+
+  void AddAudioSection(cricket::SessionDescription* description,
+                       const std::string& mid,
+                       const std::string& ufrag,
+                       const std::string& pwd,
+                       cricket::IceMode ice_mode,
+                       cricket::ConnectionRole conn_role,
+                       rtc::scoped_refptr<rtc::RTCCertificate> cert) {
+    std::unique_ptr<cricket::AudioContentDescription> audio(
+        new cricket::AudioContentDescription());
+    description->AddContent(mid, cricket::MediaProtocolType::kRtp,
+                            /*rejected=*/false, audio.release());
+    AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert);
+  }
+
+  void AddVideoSection(cricket::SessionDescription* description,
+                       const std::string& mid,
+                       const std::string& ufrag,
+                       const std::string& pwd,
+                       cricket::IceMode ice_mode,
+                       cricket::ConnectionRole conn_role,
+                       rtc::scoped_refptr<rtc::RTCCertificate> cert) {
+    std::unique_ptr<cricket::VideoContentDescription> video(
+        new cricket::VideoContentDescription());
+    description->AddContent(mid, cricket::MediaProtocolType::kRtp,
+                            /*rejected=*/false, video.release());
+    AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert);
+  }
+
+  void AddDataSection(cricket::SessionDescription* description,
+                      const std::string& mid,
+                      cricket::MediaProtocolType protocol_type,
+                      const std::string& ufrag,
+                      const std::string& pwd,
+                      cricket::IceMode ice_mode,
+                      cricket::ConnectionRole conn_role,
+                      rtc::scoped_refptr<rtc::RTCCertificate> cert) {
+    std::unique_ptr<cricket::DataContentDescription> data(
+        new cricket::DataContentDescription());
+    description->AddContent(mid, protocol_type,
+                            /*rejected=*/false, data.release());
+    AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert);
+  }
+
+  void AddTransportInfo(cricket::SessionDescription* description,
+                        const std::string& mid,
+                        const std::string& ufrag,
+                        const std::string& pwd,
+                        cricket::IceMode ice_mode,
+                        cricket::ConnectionRole conn_role,
+                        rtc::scoped_refptr<rtc::RTCCertificate> cert) {
+    std::unique_ptr<rtc::SSLFingerprint> fingerprint;
+    if (cert) {
+      fingerprint.reset(rtc::SSLFingerprint::CreateFromCertificate(cert));
+    }
+
+    cricket::TransportDescription transport_desc(std::vector<std::string>(),
+                                                 ufrag, pwd, ice_mode,
+                                                 conn_role, fingerprint.get());
+    description->AddTransportInfo(cricket::TransportInfo(mid, transport_desc));
+  }
+
+  cricket::IceConfig CreateIceConfig(
+      int receiving_timeout,
+      cricket::ContinualGatheringPolicy continual_gathering_policy) {
+    cricket::IceConfig config;
+    config.receiving_timeout = receiving_timeout;
+    config.continual_gathering_policy = continual_gathering_policy;
+    return config;
+  }
+
+  Candidate CreateCandidate(const std::string& transport_name, int component) {
+    Candidate c;
+    c.set_transport_name(transport_name);
+    c.set_address(rtc::SocketAddress("192.168.1.1", 8000));
+    c.set_component(component);
+    c.set_protocol(cricket::UDP_PROTOCOL_NAME);
+    c.set_priority(1);
+    return c;
+  }
+
+  void CreateLocalDescriptionAndCompleteConnectionOnNetworkThread() {
+    if (!network_thread_->IsCurrent()) {
+      network_thread_->Invoke<void>(RTC_FROM_HERE, [&] {
+        CreateLocalDescriptionAndCompleteConnectionOnNetworkThread();
+      });
+      return;
+    }
+
+    auto description = CreateSessionDescriptionWithBundleGroup();
+    EXPECT_TRUE(transport_controller_
+                    ->SetLocalDescription(SdpType::kOffer, description.get())
+                    .ok());
+
+    transport_controller_->MaybeStartGathering();
+    auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+        transport_controller_->GetDtlsTransport(kAudioMid1));
+    auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+        transport_controller_->GetDtlsTransport(kVideoMid1));
+    fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered(
+        fake_audio_dtls->fake_ice_transport(),
+        CreateCandidate(kAudioMid1, /*component=*/1));
+    fake_video_dtls->fake_ice_transport()->SignalCandidateGathered(
+        fake_video_dtls->fake_ice_transport(),
+        CreateCandidate(kVideoMid1, /*component=*/1));
+    fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+    fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+    fake_audio_dtls->fake_ice_transport()->SetConnectionCount(2);
+    fake_video_dtls->fake_ice_transport()->SetConnectionCount(2);
+    fake_audio_dtls->SetReceiving(true);
+    fake_video_dtls->SetReceiving(true);
+    fake_audio_dtls->SetWritable(true);
+    fake_video_dtls->SetWritable(true);
+    fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1);
+    fake_video_dtls->fake_ice_transport()->SetConnectionCount(1);
+  }
+
+ protected:
+  void OnConnectionState(cricket::IceConnectionState state) {
+    if (!signaling_thread_->IsCurrent()) {
+      signaled_on_non_signaling_thread_ = true;
+    }
+    connection_state_ = state;
+    ++connection_state_signal_count_;
+  }
+
+  void OnGatheringState(cricket::IceGatheringState state) {
+    if (!signaling_thread_->IsCurrent()) {
+      signaled_on_non_signaling_thread_ = true;
+    }
+    gathering_state_ = state;
+    ++gathering_state_signal_count_;
+  }
+
+  void OnCandidatesGathered(const std::string& transport_name,
+                            const Candidates& candidates) {
+    if (!signaling_thread_->IsCurrent()) {
+      signaled_on_non_signaling_thread_ = true;
+    }
+    candidates_[transport_name].insert(candidates_[transport_name].end(),
+                                       candidates.begin(), candidates.end());
+    ++candidates_signal_count_;
+  }
+
+  void OnRtpTransportChanged(const std::string& mid,
+                             RtpTransportInternal* rtp_transport) {
+    changed_rtp_transport_by_mid_[mid] = rtp_transport;
+  }
+
+  void OnDtlsTransportChanged(const std::string& mid,
+                              cricket::DtlsTransportInternal* dtls_transport) {
+    changed_dtls_transport_by_mid_[mid] = dtls_transport;
+  }
+
+  // Information received from signals from transport controller.
+  cricket::IceConnectionState connection_state_ =
+      cricket::kIceConnectionConnecting;
+  bool receiving_ = false;
+  cricket::IceGatheringState gathering_state_ = cricket::kIceGatheringNew;
+  // transport_name => candidates
+  std::map<std::string, Candidates> candidates_;
+  // Counts of each signal emitted.
+  int connection_state_signal_count_ = 0;
+  int receiving_signal_count_ = 0;
+  int gathering_state_signal_count_ = 0;
+  int candidates_signal_count_ = 0;
+
+  // |network_thread_| should be destroyed after |transport_controller_|
+  std::unique_ptr<rtc::Thread> network_thread_;
+  std::unique_ptr<JsepTransportController> transport_controller_;
+  std::unique_ptr<FakeTransportFactory> fake_transport_factory_;
+  rtc::Thread* const signaling_thread_ = nullptr;
+  bool signaled_on_non_signaling_thread_ = false;
+  // Used to verify the SignalRtpTransportChanged/SignalDtlsTransportChanged are
+  // signaled correctly.
+  std::map<std::string, RtpTransportInternal*> changed_rtp_transport_by_mid_;
+  std::map<std::string, cricket::DtlsTransportInternal*>
+      changed_dtls_transport_by_mid_;
+};
+
+TEST_F(JsepTransportControllerTest, GetRtpTransport) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  auto audio_rtp_transport = transport_controller_->GetRtpTransport(kAudioMid1);
+  auto video_rtp_transport = transport_controller_->GetRtpTransport(kVideoMid1);
+  EXPECT_NE(nullptr, audio_rtp_transport);
+  EXPECT_NE(nullptr, video_rtp_transport);
+  EXPECT_NE(audio_rtp_transport, video_rtp_transport);
+  // Return nullptr for non-existing ones.
+  EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kAudioMid2));
+}
+
+TEST_F(JsepTransportControllerTest, GetDtlsTransport) {
+  JsepTransportController::Config config;
+  config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyNegotiate;
+  CreateJsepTransportController(config);
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kAudioMid1));
+  EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kAudioMid1));
+  EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kVideoMid1));
+  EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid1));
+  // Return nullptr for non-existing ones.
+  EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kVideoMid2));
+  EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid2));
+}
+
+TEST_F(JsepTransportControllerTest, SetIceConfig) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  transport_controller_->SetIceConfig(
+      CreateIceConfig(kTimeout, cricket::GATHER_CONTINUALLY));
+  FakeDtlsTransport* fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  ASSERT_NE(nullptr, fake_audio_dtls);
+  EXPECT_EQ(kTimeout,
+            fake_audio_dtls->fake_ice_transport()->receiving_timeout());
+  EXPECT_TRUE(fake_audio_dtls->fake_ice_transport()->gather_continually());
+
+  // Test that value stored in controller is applied to new transports.
+  AddAudioSection(description.get(), kAudioMid2, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid2));
+  ASSERT_NE(nullptr, fake_audio_dtls);
+  EXPECT_EQ(kTimeout,
+            fake_audio_dtls->fake_ice_transport()->receiving_timeout());
+  EXPECT_TRUE(fake_audio_dtls->fake_ice_transport()->gather_continually());
+}
+
+// Tests the getter and setter of the ICE restart flag.
+TEST_F(JsepTransportControllerTest, NeedIceRestart) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, description.get())
+                  .ok());
+
+  // Initially NeedsIceRestart should return false.
+  EXPECT_FALSE(transport_controller_->NeedsIceRestart(kAudioMid1));
+  EXPECT_FALSE(transport_controller_->NeedsIceRestart(kVideoMid1));
+  // Set the needs-ice-restart flag and verify NeedsIceRestart starts returning
+  // true.
+  transport_controller_->SetNeedsIceRestartFlag();
+  EXPECT_TRUE(transport_controller_->NeedsIceRestart(kAudioMid1));
+  EXPECT_TRUE(transport_controller_->NeedsIceRestart(kVideoMid1));
+  // For a nonexistent transport, false should be returned.
+  EXPECT_FALSE(transport_controller_->NeedsIceRestart(kVideoMid2));
+
+  // Reset the ice_ufrag/ice_pwd for audio.
+  auto audio_transport_info = description->GetTransportInfoByName(kAudioMid1);
+  audio_transport_info->description.ice_ufrag = kIceUfrag2;
+  audio_transport_info->description.ice_pwd = kIcePwd2;
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  // Because the ICE is only restarted for audio, NeedsIceRestart is expected to
+  // return false for audio and true for video.
+  EXPECT_FALSE(transport_controller_->NeedsIceRestart(kAudioMid1));
+  EXPECT_TRUE(transport_controller_->NeedsIceRestart(kVideoMid1));
+}
+
+TEST_F(JsepTransportControllerTest, MaybeStartGathering) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithBundleGroup();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  // After setting the local description, we should be able to start gathering
+  // candidates.
+  transport_controller_->MaybeStartGathering();
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest, AddRemoveRemoteCandidates) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  transport_controller_->SetLocalDescription(SdpType::kOffer,
+                                             description.get());
+  transport_controller_->SetRemoteDescription(SdpType::kAnswer,
+                                              description.get());
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  ASSERT_NE(nullptr, fake_audio_dtls);
+  Candidates candidates;
+  candidates.push_back(
+      CreateCandidate(kAudioMid1, cricket::ICE_CANDIDATE_COMPONENT_RTP));
+  EXPECT_TRUE(
+      transport_controller_->AddRemoteCandidates(kAudioMid1, candidates).ok());
+  EXPECT_EQ(1U,
+            fake_audio_dtls->fake_ice_transport()->remote_candidates().size());
+
+  EXPECT_TRUE(transport_controller_->RemoveRemoteCandidates(candidates).ok());
+  EXPECT_EQ(0U,
+            fake_audio_dtls->fake_ice_transport()->remote_candidates().size());
+}
+
+TEST_F(JsepTransportControllerTest, SetAndGetLocalCertificate) {
+  CreateJsepTransportController(JsepTransportController::Config());
+
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate1 =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("session1", rtc::KT_DEFAULT)));
+  rtc::scoped_refptr<rtc::RTCCertificate> returned_certificate;
+
+  auto description = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  certificate1);
+
+  // Apply the local certificate.
+  EXPECT_TRUE(transport_controller_->SetLocalCertificate(certificate1));
+  // Apply the local description.
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  returned_certificate = transport_controller_->GetLocalCertificate(kAudioMid1);
+  EXPECT_TRUE(returned_certificate);
+  EXPECT_EQ(certificate1->identity()->certificate().ToPEMString(),
+            returned_certificate->identity()->certificate().ToPEMString());
+
+  // Should fail if called for a nonexistant transport.
+  EXPECT_EQ(nullptr, transport_controller_->GetLocalCertificate(kVideoMid1));
+
+  // Shouldn't be able to change the identity once set.
+  rtc::scoped_refptr<rtc::RTCCertificate> certificate2 =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("session2", rtc::KT_DEFAULT)));
+  EXPECT_FALSE(transport_controller_->SetLocalCertificate(certificate2));
+}
+
+TEST_F(JsepTransportControllerTest, GetRemoteSSLCertificate) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithBundleGroup();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  rtc::FakeSSLCertificate fake_certificate("fake_data");
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  fake_audio_dtls->SetRemoteSSLCertificate(&fake_certificate);
+  std::unique_ptr<rtc::SSLCertificate> returned_certificate =
+      transport_controller_->GetRemoteSSLCertificate(kAudioMid1);
+  EXPECT_TRUE(returned_certificate);
+  EXPECT_EQ(fake_certificate.ToPEMString(),
+            returned_certificate->ToPEMString());
+
+  // Should fail if called for a nonexistant transport.
+  EXPECT_FALSE(transport_controller_->GetRemoteSSLCertificate(kAudioMid2));
+}
+
+TEST_F(JsepTransportControllerTest, GetDtlsRole) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto offer_certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("offer", rtc::KT_DEFAULT)));
+  auto answer_certificate =
+      rtc::RTCCertificate::Create(std::unique_ptr<rtc::SSLIdentity>(
+          rtc::SSLIdentity::Generate("answer", rtc::KT_DEFAULT)));
+  transport_controller_->SetLocalCertificate(offer_certificate);
+
+  auto offer_desc = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(offer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  offer_certificate);
+  auto answer_desc = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(answer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  answer_certificate);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, offer_desc.get())
+                  .ok());
+
+  rtc::Optional<rtc::SSLRole> role =
+      transport_controller_->GetDtlsRole(kAudioMid1);
+  // The DTLS role is not decided yet.
+  EXPECT_FALSE(role);
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, answer_desc.get())
+                  .ok());
+  role = transport_controller_->GetDtlsRole(kAudioMid1);
+
+  ASSERT_TRUE(role);
+  EXPECT_EQ(rtc::SSL_CLIENT, *role);
+}
+
+TEST_F(JsepTransportControllerTest, GetStats) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithBundleGroup();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  cricket::TransportStats stats;
+  EXPECT_TRUE(transport_controller_->GetStats(kAudioMid1, &stats));
+  EXPECT_EQ(kAudioMid1, stats.transport_name);
+  EXPECT_EQ(1u, stats.channel_stats.size());
+  // Return false for non-existing transport.
+  EXPECT_FALSE(transport_controller_->GetStats(kAudioMid2, &stats));
+}
+
+TEST_F(JsepTransportControllerTest, SignalConnectionStateFailed) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_ice = static_cast<cricket::FakeIceTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1)->ice_transport());
+  fake_ice->SetCandidatesGatheringComplete();
+  fake_ice->SetConnectionCount(1);
+  // The connection stats will be failed if there is no active connection.
+  fake_ice->SetConnectionCount(0);
+  EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout);
+  EXPECT_EQ(1, connection_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest, SignalConnectionStateConnected) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+
+  // First, have one transport connect, and another fail, to ensure that
+  // the first transport connecting didn't trigger a "connected" state signal.
+  // We should only get a signal when all are connected.
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_audio_dtls->SetWritable(true);
+  fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+  // Decrease the number of the connection to trigger the signal.
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(0);
+  fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+
+  EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout);
+  EXPECT_EQ(1, connection_state_signal_count_);
+
+  // Set the connection count to be 2 and the cricket::FakeIceTransport will set
+  // the transport state to be STATE_CONNECTING.
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(2);
+  fake_video_dtls->SetWritable(true);
+  EXPECT_EQ_WAIT(cricket::kIceConnectionConnected, connection_state_, kTimeout);
+  EXPECT_EQ(2, connection_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest, SignalConnectionStateComplete) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+
+  // First, have one transport connect, and another fail, to ensure that
+  // the first transport connecting didn't trigger a "connected" state signal.
+  // We should only get a signal when all are connected.
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_audio_dtls->SetWritable(true);
+  fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+  // Decrease the number of the connection to trigger the signal.
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(0);
+  fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+
+  EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout);
+  EXPECT_EQ(1, connection_state_signal_count_);
+
+  // Set the connection count to be 1 and the cricket::FakeIceTransport will set
+  // the transport state to be STATE_COMPLETED.
+  fake_video_dtls->fake_ice_transport()->SetConnectionCount(1);
+  fake_video_dtls->SetWritable(true);
+  EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout);
+  EXPECT_EQ(2, connection_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest, SignalIceGatheringStateGathering) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  fake_audio_dtls->fake_ice_transport()->MaybeStartGathering();
+  // Should be in the gathering state as soon as any transport starts gathering.
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest, SignalIceGatheringStateComplete) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithoutBundle();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+
+  fake_audio_dtls->fake_ice_transport()->MaybeStartGathering();
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+
+  // Have one transport finish gathering, to make sure gathering
+  // completion wasn't signalled if only one transport finished gathering.
+  fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+  EXPECT_EQ(1, gathering_state_signal_count_);
+
+  fake_video_dtls->fake_ice_transport()->MaybeStartGathering();
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+
+  fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+  EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout);
+  EXPECT_EQ(2, gathering_state_signal_count_);
+}
+
+// Test that when the last transport that hasn't finished connecting and/or
+// gathering is destroyed, the aggregate state jumps to "completed". This can
+// happen if, for example, we have an audio and video transport, the audio
+// transport completes, then we start bundling video on the audio transport.
+TEST_F(JsepTransportControllerTest,
+       SignalingWhenLastIncompleteTransportDestroyed) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithBundleGroup();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  auto fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+  EXPECT_NE(fake_audio_dtls, fake_video_dtls);
+
+  fake_audio_dtls->fake_ice_transport()->MaybeStartGathering();
+  EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+
+  // Let the audio transport complete.
+  fake_audio_dtls->SetWritable(true);
+  fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+  fake_audio_dtls->fake_ice_transport()->SetConnectionCount(1);
+  EXPECT_EQ(1, gathering_state_signal_count_);
+
+  // Set the remote description and enable the bundle.
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, description.get())
+                  .ok());
+  // The BUNDLE should be enabled, the incomplete video transport should be
+  // deleted and the states shoud be updated.
+  fake_video_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kVideoMid1));
+  EXPECT_EQ(fake_audio_dtls, fake_video_dtls);
+  EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout);
+  EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout);
+  EXPECT_EQ(2, gathering_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest, SignalCandidatesGathered) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto description = CreateSessionDescriptionWithBundleGroup();
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, description.get())
+                  .ok());
+  transport_controller_->MaybeStartGathering();
+
+  auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered(
+      fake_audio_dtls->fake_ice_transport(), CreateCandidate(kAudioMid1, 1));
+  EXPECT_EQ_WAIT(1, candidates_signal_count_, kTimeout);
+  EXPECT_EQ(1u, candidates_[kAudioMid1].size());
+}
+
+TEST_F(JsepTransportControllerTest, IceSignalingOccursOnSignalingThread) {
+  network_thread_ = rtc::Thread::CreateWithSocketServer();
+  network_thread_->Start();
+  CreateJsepTransportController(JsepTransportController::Config(),
+                                signaling_thread_, network_thread_.get(),
+                                /*PortAllocator=*/nullptr);
+  CreateLocalDescriptionAndCompleteConnectionOnNetworkThread();
+
+  // connecting --> connected --> completed
+  EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout);
+  EXPECT_EQ(2, connection_state_signal_count_);
+
+  // new --> gathering --> complete
+  EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout);
+  EXPECT_EQ(2, gathering_state_signal_count_);
+
+  EXPECT_EQ_WAIT(1u, candidates_[kAudioMid1].size(), kTimeout);
+  EXPECT_EQ_WAIT(1u, candidates_[kVideoMid1].size(), kTimeout);
+  EXPECT_EQ(2, candidates_signal_count_);
+
+  EXPECT_TRUE(!signaled_on_non_signaling_thread_);
+}
+
+// Older versions of Chrome expect the ICE role to be re-determined when an
+// ICE restart occurs, and also don't perform conflict resolution correctly,
+// so for now we can't safely stop doing this.
+// See: https://bugs.chromium.org/p/chromium/issues/detail?id=628676
+// TODO(deadbeef): Remove this when these old versions of Chrome reach a low
+// enough population.
+TEST_F(JsepTransportControllerTest, IceRoleRedeterminedOnIceRestartByDefault) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  // Let the |transport_controller_| be the controlled side initially.
+  auto remote_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  auto local_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kOffer, remote_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kAnswer, local_answer.get())
+                  .ok());
+
+  auto fake_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLED,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+
+  // New offer will trigger the ICE restart.
+  auto restart_local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(restart_local_offer.get(), kAudioMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  EXPECT_TRUE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, restart_local_offer.get())
+          .ok());
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLING,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+}
+
+// Test that if the TransportController was created with the
+// |redetermine_role_on_ice_restart| parameter set to false, the role is *not*
+// redetermined on an ICE restart.
+TEST_F(JsepTransportControllerTest, IceRoleNotRedetermined) {
+  JsepTransportController::Config config;
+  config.redetermine_role_on_ice_restart = false;
+
+  CreateJsepTransportController(config);
+  // Let the |transport_controller_| be the controlled side initially.
+  auto remote_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  auto local_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kOffer, remote_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kAnswer, local_answer.get())
+                  .ok());
+
+  auto fake_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLED,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+
+  // New offer will trigger the ICE restart.
+  auto restart_local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(restart_local_offer.get(), kAudioMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  EXPECT_TRUE(
+      transport_controller_
+          ->SetLocalDescription(SdpType::kOffer, restart_local_offer.get())
+          .ok());
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLED,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+}
+
+// Tests ICE-Lite mode in remote answer.
+TEST_F(JsepTransportControllerTest, SetIceRoleWhenIceLiteInRemoteAnswer) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  auto fake_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLING,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+  EXPECT_EQ(cricket::ICEMODE_FULL,
+            fake_dtls->fake_ice_transport()->remote_ice_mode());
+
+  auto remote_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLING,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+  EXPECT_EQ(cricket::ICEMODE_LITE,
+            fake_dtls->fake_ice_transport()->remote_ice_mode());
+}
+
+// Tests that the ICE role remains "controlling" if a subsequent offer that
+// does an ICE restart is received from an ICE lite endpoint. Regression test
+// for: https://crbug.com/710760
+TEST_F(JsepTransportControllerTest,
+       IceRoleIsControllingAfterIceRestartFromIceLiteEndpoint) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  auto remote_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  auto local_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  // Initial Offer/Answer exchange. If the remote offerer is ICE-Lite, then the
+  // local side is the controlling.
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kOffer, remote_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kAnswer, local_answer.get())
+                  .ok());
+  auto fake_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLING,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+
+  // In the subsequence remote offer triggers an ICE restart.
+  auto remote_offer2 = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_offer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  auto local_answer2 = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_answer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kOffer, remote_offer2.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kAnswer, local_answer2.get())
+                  .ok());
+  fake_dtls = static_cast<FakeDtlsTransport*>(
+      transport_controller_->GetDtlsTransport(kAudioMid1));
+  // The local side is still the controlling role since the remote side is using
+  // ICE-Lite.
+  EXPECT_EQ(cricket::ICEROLE_CONTROLLING,
+            fake_dtls->fake_ice_transport()->GetIceRole());
+}
+
+// Tests that the SDP has more than one audio/video m= sections.
+TEST_F(JsepTransportControllerTest, MultipleMediaSectionsOfSameTypeWithBundle) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kAudioMid1);
+  bundle_group.AddContentName(kAudioMid2);
+  bundle_group.AddContentName(kVideoMid1);
+  bundle_group.AddContentName(kDataMid1);
+
+  auto local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddDataSection(local_offer.get(), kDataMid1,
+                 cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+                 cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                 nullptr);
+
+  auto remote_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddDataSection(remote_answer.get(), kDataMid1,
+                 cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+                 cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                 nullptr);
+
+  local_offer->AddGroup(bundle_group);
+  remote_answer->AddGroup(bundle_group);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+  // Verify that all the sections are bundled on kAudio1.
+  auto transport1 = transport_controller_->GetRtpTransport(kAudioMid1);
+  auto transport2 = transport_controller_->GetRtpTransport(kAudioMid2);
+  auto transport3 = transport_controller_->GetRtpTransport(kVideoMid1);
+  auto transport4 = transport_controller_->GetRtpTransport(kDataMid1);
+  EXPECT_EQ(transport1, transport2);
+  EXPECT_EQ(transport1, transport3);
+  EXPECT_EQ(transport1, transport4);
+
+  // Verify the OnRtpTransport/DtlsTransportChanged signals are fired correctly.
+  auto it = changed_rtp_transport_by_mid_.find(kAudioMid2);
+  ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+  EXPECT_EQ(transport1, it->second);
+  it = changed_rtp_transport_by_mid_.find(kAudioMid2);
+  ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+  EXPECT_EQ(transport1, it->second);
+  it = changed_rtp_transport_by_mid_.find(kVideoMid1);
+  ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+  EXPECT_EQ(transport1, it->second);
+  // Verify the DtlsTransport for the SCTP data channel is reset correctly.
+  auto it2 = changed_dtls_transport_by_mid_.find(kDataMid1);
+  ASSERT_TRUE(it2 != changed_dtls_transport_by_mid_.end());
+  EXPECT_EQ(transport1->rtp_packet_transport(), it2->second);
+}
+
+// Tests that only a subset of all the m= sections are bundled.
+TEST_F(JsepTransportControllerTest, BundleSubsetOfMediaSections) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kAudioMid1);
+  bundle_group.AddContentName(kVideoMid1);
+
+  auto local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+
+  auto remote_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+
+  local_offer->AddGroup(bundle_group);
+  remote_answer->AddGroup(bundle_group);
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Verifiy that only |kAudio1| and |kVideo1| are bundled.
+  auto transport1 = transport_controller_->GetRtpTransport(kAudioMid1);
+  auto transport2 = transport_controller_->GetRtpTransport(kAudioMid2);
+  auto transport3 = transport_controller_->GetRtpTransport(kVideoMid1);
+  EXPECT_NE(transport1, transport2);
+  EXPECT_EQ(transport1, transport3);
+
+  auto it = changed_rtp_transport_by_mid_.find(kVideoMid1);
+  ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+  EXPECT_EQ(transport1, it->second);
+  it = changed_rtp_transport_by_mid_.find(kAudioMid2);
+  EXPECT_TRUE(it == changed_rtp_transport_by_mid_.end());
+}
+
+// Tests that the initial offer/answer only have data section and audio/video
+// sections are added in the subsequent offer.
+TEST_F(JsepTransportControllerTest, BundleOnDataSectionInSubsequentOffer) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kDataMid1);
+
+  auto local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddDataSection(local_offer.get(), kDataMid1,
+                 cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+                 cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                 nullptr);
+  auto remote_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddDataSection(remote_answer.get(), kDataMid1,
+                 cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+                 cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                 nullptr);
+  local_offer->AddGroup(bundle_group);
+  remote_answer->AddGroup(bundle_group);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+  auto data_transport = transport_controller_->GetRtpTransport(kDataMid1);
+
+  // Add audio/video sections in subsequent offer.
+  AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+
+  // Reset the bundle group and do another offer/answer exchange.
+  bundle_group.AddContentName(kAudioMid1);
+  bundle_group.AddContentName(kVideoMid1);
+  local_offer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  remote_answer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  local_offer->AddGroup(bundle_group);
+  remote_answer->AddGroup(bundle_group);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  auto audio_transport = transport_controller_->GetRtpTransport(kAudioMid1);
+  auto video_transport = transport_controller_->GetRtpTransport(kVideoMid1);
+  EXPECT_EQ(data_transport, audio_transport);
+  EXPECT_EQ(data_transport, video_transport);
+}
+
+TEST_F(JsepTransportControllerTest, VideoDataRejectedInAnswer) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kAudioMid1);
+  bundle_group.AddContentName(kVideoMid1);
+  bundle_group.AddContentName(kDataMid1);
+
+  auto local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddDataSection(local_offer.get(), kDataMid1,
+                 cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3,
+                 cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                 nullptr);
+
+  auto remote_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddDataSection(remote_answer.get(), kDataMid1,
+                 cricket::MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3,
+                 cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                 nullptr);
+  // Reject video and data section.
+  remote_answer->contents()[1].rejected = true;
+  remote_answer->contents()[2].rejected = true;
+
+  local_offer->AddGroup(bundle_group);
+  remote_answer->AddGroup(bundle_group);
+
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+
+  // Verify the RtpTransport/DtlsTransport is destroyed correctly.
+  EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kVideoMid1));
+  EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kDataMid1));
+  // Verify the signals are fired correctly.
+  auto it = changed_rtp_transport_by_mid_.find(kVideoMid1);
+  ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+  EXPECT_EQ(nullptr, it->second);
+  auto it2 = changed_dtls_transport_by_mid_.find(kDataMid1);
+  ASSERT_TRUE(it2 != changed_dtls_transport_by_mid_.end());
+  EXPECT_EQ(nullptr, it2->second);
+}
+
+// Tests that changing the bundled MID in subsequent offer/answer exchange is
+// not supported.
+// TODO(bugs.webrtc.org/6704): Change this test to expect success once issue is
+// fixed
+TEST_F(JsepTransportControllerTest, ChangeBundledMidNotSupported) {
+  CreateJsepTransportController(JsepTransportController::Config());
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  bundle_group.AddContentName(kAudioMid1);
+  bundle_group.AddContentName(kVideoMid1);
+
+  auto local_offer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+  AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+                  nullptr);
+
+  auto remote_answer = rtc::MakeUnique<cricket::SessionDescription>();
+  AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+  AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2,
+                  cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+                  nullptr);
+
+  local_offer->AddGroup(bundle_group);
+  remote_answer->AddGroup(bundle_group);
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_TRUE(transport_controller_
+                  ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                  .ok());
+  EXPECT_EQ(transport_controller_->GetRtpTransport(kAudioMid1),
+            transport_controller_->GetRtpTransport(kVideoMid1));
+
+  // Reorder the bundle group.
+  EXPECT_TRUE(bundle_group.RemoveContentName(kAudioMid1));
+  bundle_group.AddContentName(kAudioMid1);
+  // The answerer uses the new bundle group and now the bundle mid is changed to
+  // |kVideo1|.
+  remote_answer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  remote_answer->AddGroup(bundle_group);
+  EXPECT_TRUE(transport_controller_
+                  ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+                  .ok());
+  EXPECT_FALSE(transport_controller_
+                   ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+                   .ok());
+}
+
+}  // namespace webrtc
diff --git a/pc/srtpfilter.cc b/pc/srtpfilter.cc
index 4fe9dae..5561ada 100644
--- a/pc/srtpfilter.cc
+++ b/pc/srtpfilter.cc
@@ -11,8 +11,8 @@
 #include "pc/srtpfilter.h"
 
 #include <string.h>
-
 #include <algorithm>
+#include <utility>
 
 #include "media/base/rtputils.h"
 #include "pc/srtpsession.h"
@@ -35,6 +35,31 @@
   return state_ >= ST_ACTIVE;
 }
 
+bool SrtpFilter::Process(const std::vector<CryptoParams>& cryptos,
+                         webrtc::SdpType type,
+                         ContentSource source) {
+  bool ret = false;
+  switch (type) {
+    case webrtc::SdpType::kOffer:
+      ret = SetOffer(cryptos, source);
+      break;
+    case webrtc::SdpType::kPrAnswer:
+      ret = SetProvisionalAnswer(cryptos, source);
+      break;
+    case webrtc::SdpType::kAnswer:
+      ret = SetAnswer(cryptos, source);
+      break;
+    default:
+      break;
+  }
+
+  if (!ret) {
+    return false;
+  }
+
+  return true;
+}
+
 bool SrtpFilter::SetOffer(const std::vector<CryptoParams>& offer_params,
                           ContentSource source) {
   if (!ExpectOffer(source)) {
diff --git a/pc/srtpfilter.h b/pc/srtpfilter.h
index e89a68a..dc0b37e 100644
--- a/pc/srtpfilter.h
+++ b/pc/srtpfilter.h
@@ -18,6 +18,7 @@
 #include <vector>
 
 #include "api/cryptoparams.h"
+#include "api/jsep.h"
 #include "api/optional.h"
 #include "pc/sessiondescription.h"
 #include "rtc_base/basictypes.h"
@@ -54,6 +55,13 @@
   // Whether the filter is active (i.e. crypto has been properly negotiated).
   bool IsActive() const;
 
+  // Handle the offer/answer negotiation of the crypto parameters internally.
+  // TODO(zhihuang): Make SetOffer/ProvisionalAnswer/Answer private as helper
+  // methods once start using Process.
+  bool Process(const std::vector<CryptoParams>& cryptos,
+               webrtc::SdpType type,
+               ContentSource source);
+
   // Indicates which crypto algorithms and keys were contained in the offer.
   // offer_params should contain a list of available parameters to use, or none,
   // if crypto is not desired. This must be called before SetAnswer.
diff --git a/pc/transportstats.cc b/pc/transportstats.cc
new file mode 100644
index 0000000..df2df58
--- /dev/null
+++ b/pc/transportstats.cc
@@ -0,0 +1,25 @@
+/*
+ *  Copyright 2018 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 "pc/transportstats.h"
+
+namespace cricket {
+
+TransportChannelStats::TransportChannelStats() = default;
+
+TransportChannelStats::TransportChannelStats(const TransportChannelStats&) =
+    default;
+
+TransportChannelStats::~TransportChannelStats() = default;
+
+TransportStats::TransportStats() = default;
+
+TransportStats::~TransportStats() = default;
+
+}  // namespace cricket
diff --git a/pc/transportstats.h b/pc/transportstats.h
new file mode 100644
index 0000000..cff608a
--- /dev/null
+++ b/pc/transportstats.h
@@ -0,0 +1,50 @@
+/*
+ *  Copyright 2018 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.
+ */
+
+#ifndef PC_TRANSPORTSTATS_H_
+#define PC_TRANSPORTSTATS_H_
+
+#include <string>
+#include <vector>
+
+#include "p2p/base/dtlstransport.h"
+#include "p2p/base/port.h"
+
+namespace cricket {
+
+struct TransportChannelStats {
+  TransportChannelStats();
+  TransportChannelStats(const TransportChannelStats&);
+  ~TransportChannelStats();
+
+  int component = 0;
+  CandidateStatsList candidate_stats_list;
+  ConnectionInfos connection_infos;
+  int srtp_crypto_suite = rtc::SRTP_INVALID_CRYPTO_SUITE;
+  int ssl_cipher_suite = rtc::TLS_NULL_WITH_NULL_NULL;
+  DtlsTransportState dtls_state = DTLS_TRANSPORT_NEW;
+};
+
+// Information about all the channels of a transport.
+// TODO(hta): Consider if a simple vector is as good as a map.
+typedef std::vector<TransportChannelStats> TransportChannelStatsList;
+
+// Information about the stats of a transport.
+struct TransportStats {
+  TransportStats();
+  ~TransportStats();
+
+  std::string transport_name;
+  TransportChannelStatsList channel_stats;
+};
+
+}  // namespace cricket
+
+#endif  // PC_TRANSPORTSTATS_H_