summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc')
-rw-r--r--third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc2704
1 files changed, 2704 insertions, 0 deletions
diff --git a/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc b/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc
new file mode 100644
index 0000000000..c30f381971
--- /dev/null
+++ b/third_party/libwebrtc/pc/jsep_transport_controller_unittest.cc
@@ -0,0 +1,2704 @@
+/*
+ * 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/jsep_transport_controller.h"
+
+#include <map>
+#include <string>
+#include <utility>
+
+#include "api/dtls_transport_interface.h"
+#include "api/transport/enums.h"
+#include "p2p/base/candidate_pair_interface.h"
+#include "p2p/base/dtls_transport_factory.h"
+#include "p2p/base/fake_dtls_transport.h"
+#include "p2p/base/fake_ice_transport.h"
+#include "p2p/base/p2p_constants.h"
+#include "p2p/base/transport_info.h"
+#include "rtc_base/fake_ssl_identity.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/net_helper.h"
+#include "rtc_base/socket_address.h"
+#include "rtc_base/ssl_fingerprint.h"
+#include "rtc_base/ssl_identity.h"
+#include "rtc_base/task_queue_for_test.h"
+#include "rtc_base/thread.h"
+#include "test/gtest.h"
+#include "test/scoped_key_value_config.h"
+
+using cricket::Candidate;
+using cricket::Candidates;
+using cricket::FakeDtlsTransport;
+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 kIceUfrag4[] = "u0004";
+static const char kIcePwd4[] = "TESTICEPWD00000000000004";
+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 FakeIceTransportFactory : public webrtc::IceTransportFactory {
+ public:
+ ~FakeIceTransportFactory() override = default;
+ rtc::scoped_refptr<IceTransportInterface> CreateIceTransport(
+ const std::string& transport_name,
+ int component,
+ IceTransportInit init) override {
+ return rtc::make_ref_counted<cricket::FakeIceTransportWrapper>(
+ std::make_unique<cricket::FakeIceTransport>(transport_name, component));
+ }
+};
+
+class FakeDtlsTransportFactory : public cricket::DtlsTransportFactory {
+ public:
+ std::unique_ptr<cricket::DtlsTransportInternal> CreateDtlsTransport(
+ cricket::IceTransportInternal* ice,
+ const webrtc::CryptoOptions& crypto_options,
+ rtc::SSLProtocolVersion max_version) override {
+ return std::make_unique<FakeDtlsTransport>(
+ static_cast<cricket::FakeIceTransport*>(ice));
+ }
+};
+
+class JsepTransportControllerTest : public JsepTransportController::Observer,
+ public ::testing::Test,
+ public sigslot::has_slots<> {
+ public:
+ JsepTransportControllerTest() : signaling_thread_(rtc::Thread::Current()) {
+ fake_ice_transport_factory_ = std::make_unique<FakeIceTransportFactory>();
+ fake_dtls_transport_factory_ = std::make_unique<FakeDtlsTransportFactory>();
+ }
+
+ void CreateJsepTransportController(
+ JsepTransportController::Config config,
+ rtc::Thread* network_thread = rtc::Thread::Current(),
+ cricket::PortAllocator* port_allocator = nullptr) {
+ config.transport_observer = this;
+ config.rtcp_handler = [](const rtc::CopyOnWriteBuffer& packet,
+ int64_t packet_time_us) {
+ RTC_DCHECK_NOTREACHED();
+ };
+ config.ice_transport_factory = fake_ice_transport_factory_.get();
+ config.dtls_transport_factory = fake_dtls_transport_factory_.get();
+ config.on_dtls_handshake_error_ = [](rtc::SSLHandshakeError s) {};
+ config.field_trials = &field_trials_;
+ transport_controller_ = std::make_unique<JsepTransportController>(
+ network_thread, port_allocator, nullptr /* async_resolver_factory */,
+ config);
+ SendTask(network_thread, [&] { ConnectTransportControllerSignals(); });
+ }
+
+ void ConnectTransportControllerSignals() {
+ transport_controller_->SubscribeIceConnectionState(
+ [this](cricket::IceConnectionState s) {
+ JsepTransportControllerTest::OnConnectionState(s);
+ });
+ transport_controller_->SubscribeConnectionState(
+ [this](PeerConnectionInterface::PeerConnectionState s) {
+ JsepTransportControllerTest::OnCombinedConnectionState(s);
+ });
+ transport_controller_->SubscribeStandardizedIceConnectionState(
+ [this](PeerConnectionInterface::IceConnectionState s) {
+ JsepTransportControllerTest::OnStandardizedIceConnectionState(s);
+ });
+ transport_controller_->SubscribeIceGatheringState(
+ [this](cricket::IceGatheringState s) {
+ JsepTransportControllerTest::OnGatheringState(s);
+ });
+ transport_controller_->SubscribeIceCandidateGathered(
+ [this](const std::string& transport,
+ const std::vector<cricket::Candidate>& candidates) {
+ JsepTransportControllerTest::OnCandidatesGathered(transport,
+ candidates);
+ });
+ }
+
+ std::unique_ptr<cricket::SessionDescription>
+ CreateSessionDescriptionWithoutBundle() {
+ auto description = std::make_unique<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;
+ }
+
+ std::unique_ptr<cricket::SessionDescription>
+ CreateSessionDescriptionWithBundledData() {
+ auto description = CreateSessionDescriptionWithoutBundle();
+ AddDataSection(description.get(), kDataMid1,
+ cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group.AddContentName(kAudioMid1);
+ bundle_group.AddContentName(kVideoMid1);
+ bundle_group.AddContentName(kDataMid1);
+ 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());
+ // Set RTCP-mux to be true because the default policy is "mux required".
+ audio->set_rtcp_mux(true);
+ description->AddContent(mid, cricket::MediaProtocolType::kRtp,
+ /*rejected=*/false, std::move(audio));
+ 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());
+ // Set RTCP-mux to be true because the default policy is "mux required".
+ video->set_rtcp_mux(true);
+ description->AddContent(mid, cricket::MediaProtocolType::kRtp,
+ /*rejected=*/false, std::move(video));
+ 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) {
+ RTC_CHECK(protocol_type == cricket::MediaProtocolType::kSctp);
+ std::unique_ptr<cricket::SctpDataContentDescription> data(
+ new cricket::SctpDataContentDescription());
+ data->set_rtcp_mux(true);
+ description->AddContent(mid, protocol_type,
+ /*rejected=*/false, std::move(data));
+ 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 = 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()) {
+ SendTask(network_thread_.get(), [&] {
+ 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) {
+ ice_signaled_on_thread_ = rtc::Thread::Current();
+ connection_state_ = state;
+ ++connection_state_signal_count_;
+ }
+
+ void OnStandardizedIceConnectionState(
+ PeerConnectionInterface::IceConnectionState state) {
+ ice_signaled_on_thread_ = rtc::Thread::Current();
+ ice_connection_state_ = state;
+ ++ice_connection_state_signal_count_;
+ }
+
+ void OnCombinedConnectionState(
+ PeerConnectionInterface::PeerConnectionState state) {
+ RTC_LOG(LS_INFO) << "OnCombinedConnectionState: "
+ << static_cast<int>(state);
+ ice_signaled_on_thread_ = rtc::Thread::Current();
+ combined_connection_state_ = state;
+ ++combined_connection_state_signal_count_;
+ }
+
+ void OnGatheringState(cricket::IceGatheringState state) {
+ ice_signaled_on_thread_ = rtc::Thread::Current();
+ gathering_state_ = state;
+ ++gathering_state_signal_count_;
+ }
+
+ void OnCandidatesGathered(const std::string& transport_name,
+ const Candidates& candidates) {
+ ice_signaled_on_thread_ = rtc::Thread::Current();
+ candidates_[transport_name].insert(candidates_[transport_name].end(),
+ candidates.begin(), candidates.end());
+ ++candidates_signal_count_;
+ }
+
+ // JsepTransportController::Observer overrides.
+ bool OnTransportChanged(
+ const std::string& mid,
+ RtpTransportInternal* rtp_transport,
+ rtc::scoped_refptr<DtlsTransport> dtls_transport,
+ DataChannelTransportInterface* data_channel_transport) override {
+ changed_rtp_transport_by_mid_[mid] = rtp_transport;
+ if (dtls_transport) {
+ changed_dtls_transport_by_mid_[mid] = dtls_transport->internal();
+ } else {
+ changed_dtls_transport_by_mid_[mid] = nullptr;
+ }
+ return true;
+ }
+
+ rtc::AutoThread main_thread_;
+ // Information received from signals from transport controller.
+ cricket::IceConnectionState connection_state_ =
+ cricket::kIceConnectionConnecting;
+ PeerConnectionInterface::IceConnectionState ice_connection_state_ =
+ PeerConnectionInterface::kIceConnectionNew;
+ PeerConnectionInterface::PeerConnectionState combined_connection_state_ =
+ PeerConnectionInterface::PeerConnectionState::kNew;
+ 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 ice_connection_state_signal_count_ = 0;
+ int combined_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<FakeIceTransportFactory> fake_ice_transport_factory_;
+ std::unique_ptr<FakeDtlsTransportFactory> fake_dtls_transport_factory_;
+ rtc::Thread* const signaling_thread_ = nullptr;
+ rtc::Thread* ice_signaled_on_thread_ = nullptr;
+ // 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_;
+
+ // Transport controller needs to be destroyed first, because it may issue
+ // callbacks that modify the changed_*_by_mid in the destructor.
+ std::unique_ptr<JsepTransportController> transport_controller_;
+ webrtc::test::ScopedKeyValueConfig field_trials_;
+};
+
+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_->LookupDtlsTransportByMid(kAudioMid1));
+ EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kVideoMid1));
+ EXPECT_NE(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid1));
+ EXPECT_NE(nullptr,
+ transport_controller_->LookupDtlsTransportByMid(kVideoMid1));
+ // Lookup for all MIDs should return different transports (no bundle)
+ EXPECT_NE(transport_controller_->LookupDtlsTransportByMid(kAudioMid1),
+ transport_controller_->LookupDtlsTransportByMid(kVideoMid1));
+ // Return nullptr for non-existing ones.
+ EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kVideoMid2));
+ EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid2));
+ EXPECT_EQ(nullptr,
+ transport_controller_->LookupDtlsTransportByMid(kVideoMid2));
+ // Take a pointer to a transport, shut down the transport controller,
+ // and verify that the resulting container is empty.
+ auto dtls_transport =
+ transport_controller_->LookupDtlsTransportByMid(kVideoMid1);
+ webrtc::DtlsTransport* my_transport =
+ static_cast<DtlsTransport*>(dtls_transport.get());
+ EXPECT_NE(nullptr, my_transport->internal());
+ transport_controller_.reset();
+ EXPECT_EQ(nullptr, my_transport->internal());
+}
+
+TEST_F(JsepTransportControllerTest, GetDtlsTransportWithRtcpMux) {
+ JsepTransportController::Config config;
+ config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire;
+ CreateJsepTransportController(config);
+ auto description = CreateSessionDescriptionWithoutBundle();
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, description.get())
+ .ok());
+ EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kAudioMid1));
+ EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kAudioMid1));
+ EXPECT_NE(nullptr, transport_controller_->GetDtlsTransport(kVideoMid1));
+ EXPECT_EQ(nullptr, transport_controller_->GetRtcpDtlsTransport(kVideoMid1));
+}
+
+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(
+ rtc::SSLIdentity::Create("session1", rtc::KT_DEFAULT));
+ rtc::scoped_refptr<rtc::RTCCertificate> returned_certificate;
+
+ auto description = std::make_unique<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(
+ rtc::SSLIdentity::Create("session2", rtc::KT_DEFAULT));
+ EXPECT_FALSE(transport_controller_->SetLocalCertificate(certificate2));
+}
+
+TEST_F(JsepTransportControllerTest, GetRemoteSSLCertChain) {
+ 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::SSLCertChain> returned_cert_chain =
+ transport_controller_->GetRemoteSSLCertChain(kAudioMid1);
+ ASSERT_TRUE(returned_cert_chain);
+ ASSERT_EQ(1u, returned_cert_chain->GetSize());
+ EXPECT_EQ(fake_certificate.ToPEMString(),
+ returned_cert_chain->Get(0).ToPEMString());
+
+ // Should fail if called for a nonexistant transport.
+ EXPECT_FALSE(transport_controller_->GetRemoteSSLCertChain(kAudioMid2));
+}
+
+TEST_F(JsepTransportControllerTest, GetDtlsRole) {
+ CreateJsepTransportController(JsepTransportController::Config());
+ auto offer_certificate = rtc::RTCCertificate::Create(
+ rtc::SSLIdentity::Create("offer", rtc::KT_DEFAULT));
+ auto answer_certificate = rtc::RTCCertificate::Create(
+ rtc::SSLIdentity::Create("answer", rtc::KT_DEFAULT));
+ transport_controller_->SetLocalCertificate(offer_certificate);
+
+ auto offer_desc = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(offer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ offer_certificate);
+ auto answer_desc = std::make_unique<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());
+
+ absl::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_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionFailed,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(1, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kFailed,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(1, combined_connection_state_signal_count_);
+}
+
+TEST_F(JsepTransportControllerTest,
+ SignalConnectionStateConnectedNoMediaTransport) {
+ 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_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionFailed,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(2, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kFailed,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(2, combined_connection_state_signal_count_);
+
+ fake_audio_dtls->SetDtlsState(DtlsTransportState::kConnected);
+ fake_video_dtls->SetDtlsState(DtlsTransportState::kConnected);
+ // 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_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionConnected,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(3, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnected,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(3, combined_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()->SetTransportState(
+ IceTransportState::kCompleted,
+ cricket::IceTransportState::STATE_COMPLETED);
+ fake_audio_dtls->SetWritable(true);
+ fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(1, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(1, combined_connection_state_signal_count_);
+
+ fake_video_dtls->fake_ice_transport()->SetTransportState(
+ IceTransportState::kFailed, cricket::IceTransportState::STATE_FAILED);
+ fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete();
+
+ EXPECT_EQ_WAIT(cricket::kIceConnectionFailed, connection_state_, kTimeout);
+ EXPECT_EQ(1, connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionFailed,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(2, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kFailed,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(2, combined_connection_state_signal_count_);
+
+ fake_audio_dtls->SetDtlsState(DtlsTransportState::kConnected);
+ fake_video_dtls->SetDtlsState(DtlsTransportState::kConnected);
+ // 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()->SetTransportState(
+ IceTransportState::kCompleted,
+ cricket::IceTransportState::STATE_COMPLETED);
+ fake_video_dtls->SetWritable(true);
+ EXPECT_EQ_WAIT(cricket::kIceConnectionCompleted, connection_state_, kTimeout);
+ EXPECT_EQ(3, connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionCompleted,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(3, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnected,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(3, combined_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);
+ fake_audio_dtls->SetDtlsState(DtlsTransportState::kConnected);
+ 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(PeerConnectionInterface::kIceConnectionCompleted,
+ ice_connection_state_);
+ EXPECT_EQ(PeerConnectionInterface::PeerConnectionState::kConnected,
+ combined_connection_state_);
+ EXPECT_EQ_WAIT(cricket::kIceGatheringComplete, gathering_state_, kTimeout);
+ EXPECT_EQ(2, gathering_state_signal_count_);
+}
+
+// Test that states immediately return to "new" if all transports are
+// discarded. This should happen at offer time, even though the transport
+// controller may keep the transport alive in case of rollback.
+TEST_F(JsepTransportControllerTest,
+ IceStatesReturnToNewWhenTransportsDiscarded) {
+ CreateJsepTransportController(JsepTransportController::Config());
+ auto description = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, description.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, description.get())
+ .ok());
+
+ // Trigger and verify initial non-new states.
+ auto fake_audio_dtls = static_cast<FakeDtlsTransport*>(
+ transport_controller_->GetDtlsTransport(kAudioMid1));
+ fake_audio_dtls->fake_ice_transport()->MaybeStartGathering();
+ fake_audio_dtls->fake_ice_transport()->SetTransportState(
+ webrtc::IceTransportState::kChecking,
+ cricket::IceTransportState::STATE_CONNECTING);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(1, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(1, combined_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+ EXPECT_EQ(1, gathering_state_signal_count_);
+
+ // Reject m= section which should disconnect the transport and return states
+ // to "new".
+ description->contents()[0].rejected = true;
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kOffer, description.get())
+ .ok());
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionNew,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(2, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kNew,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(2, combined_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(cricket::kIceGatheringNew, gathering_state_, kTimeout);
+ EXPECT_EQ(2, gathering_state_signal_count_);
+
+ // For good measure, rollback the offer and verify that states return to
+ // their previous values.
+ EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+ EXPECT_EQ_WAIT(PeerConnectionInterface::kIceConnectionChecking,
+ ice_connection_state_, kTimeout);
+ EXPECT_EQ(3, ice_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(PeerConnectionInterface::PeerConnectionState::kConnecting,
+ combined_connection_state_, kTimeout);
+ EXPECT_EQ(3, combined_connection_state_signal_count_);
+ EXPECT_EQ_WAIT(cricket::kIceGatheringGathering, gathering_state_, kTimeout);
+ EXPECT_EQ(3, 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, IceSignalingOccursOnNetworkThread) {
+ network_thread_ = rtc::Thread::CreateWithSocketServer();
+ network_thread_->Start();
+ EXPECT_EQ(ice_signaled_on_thread_, nullptr);
+ CreateJsepTransportController(JsepTransportController::Config(),
+ network_thread_.get(),
+ /*port_allocator=*/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_EQ(ice_signaled_on_thread_, network_thread_.get());
+
+ SendTask(network_thread_.get(), [&] { transport_controller_.reset(); });
+}
+
+// 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 = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ auto local_answer = std::make_unique<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 = std::make_unique<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 = std::make_unique<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 = std::make_unique<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 = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ auto local_answer = std::make_unique<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 = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_offer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_LITE, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ auto local_answer2 = std::make_unique<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 = std::make_unique<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 = std::make_unique<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);
+
+ EXPECT_EQ(transport_controller_->LookupDtlsTransportByMid(kAudioMid1),
+ transport_controller_->LookupDtlsTransportByMid(kVideoMid1));
+
+ // 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());
+}
+
+TEST_F(JsepTransportControllerTest, MultipleBundleGroups) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Video[] = "2_video";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid1Audio);
+ bundle_group1.AddContentName(kMid2Video);
+ cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid3Audio);
+ bundle_group2.AddContentName(kMid4Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group1);
+ local_offer->AddGroup(bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(bundle_group1);
+ remote_answer->AddGroup(bundle_group2);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Verify that (kMid1Audio,kMid2Video) and (kMid3Audio,kMid4Video) form two
+ // distinct bundled groups.
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Video);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video);
+ EXPECT_EQ(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid3_transport, mid4_transport);
+ EXPECT_NE(mid1_transport, mid3_transport);
+
+ auto it = changed_rtp_transport_by_mid_.find(kMid1Audio);
+ ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+ EXPECT_EQ(it->second, mid1_transport);
+
+ it = changed_rtp_transport_by_mid_.find(kMid2Video);
+ ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+ EXPECT_EQ(it->second, mid2_transport);
+
+ it = changed_rtp_transport_by_mid_.find(kMid3Audio);
+ ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+ EXPECT_EQ(it->second, mid3_transport);
+
+ it = changed_rtp_transport_by_mid_.find(kMid4Video);
+ ASSERT_TRUE(it != changed_rtp_transport_by_mid_.end());
+ EXPECT_EQ(it->second, mid4_transport);
+}
+
+TEST_F(JsepTransportControllerTest,
+ MultipleBundleGroupsInOfferButOnlyASingleGroupInAnswer) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Video[] = "2_video";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid1Audio);
+ bundle_group1.AddContentName(kMid2Video);
+ cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid3Audio);
+ bundle_group2.AddContentName(kMid4Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ // The offer has both groups.
+ local_offer->AddGroup(bundle_group1);
+ local_offer->AddGroup(bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ // The answer only has a single group! This is what happens when talking to an
+ // endpoint that does not have support for multiple BUNDLE groups.
+ remote_answer->AddGroup(bundle_group1);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Verify that (kMid1Audio,kMid2Video) form a bundle group, but that
+ // kMid3Audio and kMid4Video are unbundled.
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Video);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video);
+ EXPECT_EQ(mid1_transport, mid2_transport);
+ EXPECT_NE(mid3_transport, mid4_transport);
+ EXPECT_NE(mid1_transport, mid3_transport);
+ EXPECT_NE(mid1_transport, mid4_transport);
+}
+
+TEST_F(JsepTransportControllerTest, MultipleBundleGroupsIllegallyChangeGroup) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Video[] = "2_video";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Offer groups (kMid1Audio,kMid2Video) and (kMid3Audio,kMid4Video).
+ cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group1.AddContentName(kMid1Audio);
+ offer_bundle_group1.AddContentName(kMid2Video);
+ cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group2.AddContentName(kMid3Audio);
+ offer_bundle_group2.AddContentName(kMid4Video);
+ // Answer groups (kMid1Audio,kMid4Video) and (kMid3Audio,kMid2Video), i.e. the
+ // second group members have switched places. This should get rejected.
+ cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group1.AddContentName(kMid1Audio);
+ answer_bundle_group1.AddContentName(kMid4Video);
+ cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group2.AddContentName(kMid3Audio);
+ answer_bundle_group2.AddContentName(kMid2Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(offer_bundle_group1);
+ local_offer->AddGroup(offer_bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(answer_bundle_group1);
+ remote_answer->AddGroup(answer_bundle_group2);
+
+ // Accept offer.
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ // Reject answer!
+ EXPECT_FALSE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+}
+
+TEST_F(JsepTransportControllerTest, MultipleBundleGroupsInvalidSubsets) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Video[] = "2_video";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Offer groups (kMid1Audio,kMid2Video) and (kMid3Audio,kMid4Video).
+ cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group1.AddContentName(kMid1Audio);
+ offer_bundle_group1.AddContentName(kMid2Video);
+ cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group2.AddContentName(kMid3Audio);
+ offer_bundle_group2.AddContentName(kMid4Video);
+ // Answer groups (kMid1Audio) and (kMid2Video), i.e. the second group was
+ // moved from the first group. This should get rejected.
+ cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group1.AddContentName(kMid1Audio);
+ cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group2.AddContentName(kMid2Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(offer_bundle_group1);
+ local_offer->AddGroup(offer_bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(answer_bundle_group1);
+ remote_answer->AddGroup(answer_bundle_group2);
+
+ // Accept offer.
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ // Reject answer!
+ EXPECT_FALSE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+}
+
+TEST_F(JsepTransportControllerTest, MultipleBundleGroupsInvalidOverlap) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Video[] = "2_video";
+ static const char kMid3Audio[] = "3_audio";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Offer groups (kMid1Audio,kMid3Audio) and (kMid2Video,kMid3Audio), i.e.
+ // kMid3Audio is in both groups - this is illegal.
+ cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group1.AddContentName(kMid1Audio);
+ offer_bundle_group1.AddContentName(kMid3Audio);
+ cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group2.AddContentName(kMid2Video);
+ offer_bundle_group2.AddContentName(kMid3Audio);
+
+ auto offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(offer.get(), kMid2Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ offer->AddGroup(offer_bundle_group1);
+ offer->AddGroup(offer_bundle_group2);
+
+ // Reject offer, both if set as local or remote.
+ EXPECT_FALSE(
+ transport_controller_->SetLocalDescription(SdpType::kOffer, offer.get())
+ .ok());
+ EXPECT_FALSE(
+ transport_controller_->SetRemoteDescription(SdpType::kOffer, offer.get())
+ .ok());
+}
+
+TEST_F(JsepTransportControllerTest, MultipleBundleGroupsUnbundleFirstMid) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+ static const char kMid5Video[] = "5_video";
+ static const char kMid6Video[] = "6_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Offer groups (kMid1Audio,kMid2Audio,kMid3Audio) and
+ // (kMid4Video,kMid5Video,kMid6Video).
+ cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group1.AddContentName(kMid1Audio);
+ offer_bundle_group1.AddContentName(kMid2Audio);
+ offer_bundle_group1.AddContentName(kMid3Audio);
+ cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group2.AddContentName(kMid4Video);
+ offer_bundle_group2.AddContentName(kMid5Video);
+ offer_bundle_group2.AddContentName(kMid6Video);
+ // Answer groups (kMid2Audio,kMid3Audio) and (kMid5Video,kMid6Video), i.e.
+ // we've moved the first MIDs out of the groups.
+ cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group1.AddContentName(kMid2Audio);
+ answer_bundle_group1.AddContentName(kMid3Audio);
+ cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group2.AddContentName(kMid5Video);
+ answer_bundle_group2.AddContentName(kMid6Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(offer_bundle_group1);
+ local_offer->AddGroup(offer_bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid6Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(answer_bundle_group1);
+ remote_answer->AddGroup(answer_bundle_group2);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video);
+ auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video);
+ auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video);
+ EXPECT_NE(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid2_transport, mid3_transport);
+ EXPECT_NE(mid4_transport, mid5_transport);
+ EXPECT_EQ(mid5_transport, mid6_transport);
+ EXPECT_NE(mid1_transport, mid4_transport);
+ EXPECT_NE(mid2_transport, mid5_transport);
+}
+
+TEST_F(JsepTransportControllerTest, MultipleBundleGroupsChangeFirstMid) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+ static const char kMid5Video[] = "5_video";
+ static const char kMid6Video[] = "6_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Offer groups (kMid1Audio,kMid2Audio,kMid3Audio) and
+ // (kMid4Video,kMid5Video,kMid6Video).
+ cricket::ContentGroup offer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group1.AddContentName(kMid1Audio);
+ offer_bundle_group1.AddContentName(kMid2Audio);
+ offer_bundle_group1.AddContentName(kMid3Audio);
+ cricket::ContentGroup offer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group2.AddContentName(kMid4Video);
+ offer_bundle_group2.AddContentName(kMid5Video);
+ offer_bundle_group2.AddContentName(kMid6Video);
+ // Answer groups (kMid2Audio,kMid1Audio,kMid3Audio) and
+ // (kMid5Video,kMid6Video,kMid4Video), i.e. we've changed which MID is first
+ // but accept the whole group.
+ cricket::ContentGroup answer_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group1.AddContentName(kMid2Audio);
+ answer_bundle_group1.AddContentName(kMid1Audio);
+ answer_bundle_group1.AddContentName(kMid3Audio);
+ cricket::ContentGroup answer_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group2.AddContentName(kMid5Video);
+ answer_bundle_group2.AddContentName(kMid6Video);
+ answer_bundle_group2.AddContentName(kMid4Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(offer_bundle_group1);
+ local_offer->AddGroup(offer_bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid6Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(answer_bundle_group1);
+ remote_answer->AddGroup(answer_bundle_group2);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+
+ // The fact that we accept this answer is actually a bug. If we accept the
+ // first MID to be in the group, we should also accept that it is the tagged
+ // one.
+ // TODO(https://crbug.com/webrtc/12699): When this issue is fixed, change this
+ // to EXPECT_FALSE and remove the below expectations about transports.
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video);
+ auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video);
+ auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video);
+ EXPECT_NE(mid1_transport, mid4_transport);
+ EXPECT_EQ(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid2_transport, mid3_transport);
+ EXPECT_EQ(mid4_transport, mid5_transport);
+ EXPECT_EQ(mid5_transport, mid6_transport);
+}
+
+TEST_F(JsepTransportControllerTest,
+ MultipleBundleGroupsSectionsAddedInSubsequentOffer) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Audio[] = "3_audio";
+ static const char kMid4Video[] = "4_video";
+ static const char kMid5Video[] = "5_video";
+ static const char kMid6Video[] = "6_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Start by grouping (kMid1Audio,kMid2Audio) and (kMid4Video,kMid4f5Video).
+ cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid1Audio);
+ bundle_group1.AddContentName(kMid2Audio);
+ cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid4Video);
+ bundle_group2.AddContentName(kMid5Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group1);
+ local_offer->AddGroup(bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(bundle_group1);
+ remote_answer->AddGroup(bundle_group2);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Add kMid3Audio and kMid6Video to the respective audio/video bundle groups.
+ cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid3Audio);
+ cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid6Video);
+
+ auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(subsequent_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ subsequent_offer->AddGroup(bundle_group1);
+ subsequent_offer->AddGroup(bundle_group2);
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+ .ok());
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ auto mid4_transport = transport_controller_->GetRtpTransport(kMid4Video);
+ auto mid5_transport = transport_controller_->GetRtpTransport(kMid5Video);
+ auto mid6_transport = transport_controller_->GetRtpTransport(kMid6Video);
+ EXPECT_NE(mid1_transport, mid4_transport);
+ EXPECT_EQ(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid2_transport, mid3_transport);
+ EXPECT_EQ(mid4_transport, mid5_transport);
+ EXPECT_EQ(mid5_transport, mid6_transport);
+}
+
+TEST_F(JsepTransportControllerTest,
+ MultipleBundleGroupsCombinedInSubsequentOffer) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Video[] = "3_video";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Start by grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video).
+ cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid1Audio);
+ bundle_group1.AddContentName(kMid2Audio);
+ cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid3Video);
+ bundle_group2.AddContentName(kMid4Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group1);
+ local_offer->AddGroup(bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(bundle_group1);
+ remote_answer->AddGroup(bundle_group2);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Switch to grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video).
+ // This is a illegal without first removing m= sections from their groups.
+ cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ new_bundle_group.AddContentName(kMid1Audio);
+ new_bundle_group.AddContentName(kMid2Audio);
+ new_bundle_group.AddContentName(kMid3Video);
+ new_bundle_group.AddContentName(kMid4Video);
+
+ auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ subsequent_offer->AddGroup(new_bundle_group);
+ EXPECT_FALSE(
+ transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+ .ok());
+}
+
+TEST_F(JsepTransportControllerTest,
+ MultipleBundleGroupsSplitInSubsequentOffer) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Video[] = "3_video";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Start by grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video).
+ cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group.AddContentName(kMid1Audio);
+ bundle_group.AddContentName(kMid2Audio);
+ bundle_group.AddContentName(kMid3Video);
+ bundle_group.AddContentName(kMid4Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ 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());
+
+ // Switch to grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video).
+ // This is a illegal without first removing m= sections from their groups.
+ cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ new_bundle_group1.AddContentName(kMid1Audio);
+ new_bundle_group1.AddContentName(kMid2Audio);
+ cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ new_bundle_group2.AddContentName(kMid3Video);
+ new_bundle_group2.AddContentName(kMid4Video);
+
+ auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ subsequent_offer->AddGroup(new_bundle_group1);
+ subsequent_offer->AddGroup(new_bundle_group2);
+ EXPECT_FALSE(
+ transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+ .ok());
+}
+
+TEST_F(JsepTransportControllerTest,
+ MultipleBundleGroupsShuffledInSubsequentOffer) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Video[] = "3_video";
+ static const char kMid4Video[] = "4_video";
+
+ CreateJsepTransportController(JsepTransportController::Config());
+ // Start by grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video).
+ cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid1Audio);
+ bundle_group1.AddContentName(kMid2Audio);
+ cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid3Video);
+ bundle_group2.AddContentName(kMid4Video);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group1);
+ local_offer->AddGroup(bundle_group2);
+
+ auto remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ remote_answer->AddGroup(bundle_group1);
+ remote_answer->AddGroup(bundle_group2);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Switch to grouping (kMid1Audio,kMid3Video) and (kMid2Audio,kMid3Video).
+ // This is a illegal without first removing m= sections from their groups.
+ cricket::ContentGroup new_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ new_bundle_group1.AddContentName(kMid1Audio);
+ new_bundle_group1.AddContentName(kMid3Video);
+ cricket::ContentGroup new_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ new_bundle_group2.AddContentName(kMid2Audio);
+ new_bundle_group2.AddContentName(kMid4Video);
+
+ auto subsequent_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ subsequent_offer->AddGroup(new_bundle_group1);
+ subsequent_offer->AddGroup(new_bundle_group2);
+ EXPECT_FALSE(
+ transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get())
+ .ok());
+}
+
+// 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 = std::make_unique<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 = std::make_unique<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(transport2 == it->second);
+}
+
+// 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 = std::make_unique<cricket::SessionDescription>();
+ AddDataSection(local_offer.get(), kDataMid1,
+ cricket::MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ auto remote_answer = std::make_unique<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 = std::make_unique<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 = std::make_unique<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 = std::make_unique<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 = std::make_unique<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());
+}
+// Test that rejecting only the first m= section of a BUNDLE group is treated as
+// an error, but rejecting all of them works as expected.
+TEST_F(JsepTransportControllerTest, RejectFirstContentInBundleGroup) {
+ 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 = std::make_unique<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 = std::make_unique<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 audio content in answer.
+ remote_answer->contents()[0].rejected = true;
+
+ local_offer->AddGroup(bundle_group);
+ 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());
+
+ // Reject all the contents.
+ remote_answer->contents()[1].rejected = true;
+ remote_answer->contents()[2].rejected = true;
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+ EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kAudioMid1));
+ EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kVideoMid1));
+ EXPECT_EQ(nullptr, transport_controller_->GetDtlsTransport(kDataMid1));
+}
+
+// Tests that applying non-RTCP-mux offer would fail when kRtcpMuxPolicyRequire
+// is used.
+TEST_F(JsepTransportControllerTest, ApplyNonRtcpMuxOfferWhenMuxingRequired) {
+ JsepTransportController::Config config;
+ config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire;
+ CreateJsepTransportController(config);
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+
+ local_offer->contents()[0].media_description()->set_rtcp_mux(false);
+ // Applying a non-RTCP-mux offer is expected to fail.
+ EXPECT_FALSE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+}
+
+// Tests that applying non-RTCP-mux answer would fail when kRtcpMuxPolicyRequire
+// is used.
+TEST_F(JsepTransportControllerTest, ApplyNonRtcpMuxAnswerWhenMuxingRequired) {
+ JsepTransportController::Config config;
+ config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyRequire;
+ CreateJsepTransportController(config);
+ auto local_offer = std::make_unique<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 remote_answer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_PASSIVE,
+ nullptr);
+ // Applying a non-RTCP-mux answer is expected to fail.
+ remote_answer->contents()[0].media_description()->set_rtcp_mux(false);
+ EXPECT_FALSE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+}
+
+// This tests that the BUNDLE group in answer should be a subset of the offered
+// group.
+TEST_F(JsepTransportControllerTest,
+ AddContentToBundleGroupInAnswerNotSupported) {
+ CreateJsepTransportController(JsepTransportController::Config());
+ auto local_offer = CreateSessionDescriptionWithoutBundle();
+ auto remote_answer = CreateSessionDescriptionWithoutBundle();
+
+ cricket::ContentGroup offer_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ offer_bundle_group.AddContentName(kAudioMid1);
+ local_offer->AddGroup(offer_bundle_group);
+
+ cricket::ContentGroup answer_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ answer_bundle_group.AddContentName(kAudioMid1);
+ answer_bundle_group.AddContentName(kVideoMid1);
+ remote_answer->AddGroup(answer_bundle_group);
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_FALSE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+}
+
+// This tests that the BUNDLE group with non-existing MID should be rejectd.
+TEST_F(JsepTransportControllerTest, RejectBundleGroupWithNonExistingMid) {
+ CreateJsepTransportController(JsepTransportController::Config());
+ auto local_offer = CreateSessionDescriptionWithoutBundle();
+ auto remote_answer = CreateSessionDescriptionWithoutBundle();
+
+ cricket::ContentGroup invalid_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ // The BUNDLE group is invalid because there is no data section in the
+ // description.
+ invalid_bundle_group.AddContentName(kDataMid1);
+ local_offer->AddGroup(invalid_bundle_group);
+ remote_answer->AddGroup(invalid_bundle_group);
+
+ EXPECT_FALSE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_FALSE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+}
+
+// This tests that an answer shouldn't be able to remove an m= section from an
+// established group without rejecting it.
+TEST_F(JsepTransportControllerTest, RemoveContentFromBundleGroup) {
+ CreateJsepTransportController(JsepTransportController::Config());
+
+ auto local_offer = CreateSessionDescriptionWithBundleGroup();
+ auto remote_answer = CreateSessionDescriptionWithBundleGroup();
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Do an re-offer/answer.
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ auto new_answer = CreateSessionDescriptionWithoutBundle();
+ cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ // The answer removes video from the BUNDLE group without rejecting it is
+ // invalid.
+ new_bundle_group.AddContentName(kAudioMid1);
+ new_answer->AddGroup(new_bundle_group);
+
+ // Applying invalid answer is expected to fail.
+ EXPECT_FALSE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, new_answer.get())
+ .ok());
+
+ // Rejected the video content.
+ auto video_content = new_answer->GetContentByName(kVideoMid1);
+ ASSERT_TRUE(video_content);
+ video_content->rejected = true;
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, new_answer.get())
+ .ok());
+}
+
+// Test that the JsepTransportController can process a new local and remote
+// description that changes the tagged BUNDLE group with the max-bundle policy
+// specified.
+// This is a regression test for bugs.webrtc.org/9954
+TEST_F(JsepTransportControllerTest, ChangeTaggedMediaSectionMaxBundle) {
+ CreateJsepTransportController(JsepTransportController::Config());
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group.AddContentName(kAudioMid1);
+ local_offer->AddGroup(bundle_group);
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+
+ std::unique_ptr<cricket::SessionDescription> remote_answer(
+ local_offer->Clone());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ std::unique_ptr<cricket::SessionDescription> local_reoffer(
+ local_offer->Clone());
+ local_reoffer->contents()[0].rejected = true;
+ AddVideoSection(local_reoffer.get(), kVideoMid1, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_reoffer->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+ cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ new_bundle_group.AddContentName(kVideoMid1);
+ local_reoffer->AddGroup(new_bundle_group);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_reoffer.get())
+ .ok());
+ std::unique_ptr<cricket::SessionDescription> remote_reanswer(
+ local_reoffer->Clone());
+ EXPECT_TRUE(
+ transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_reanswer.get())
+ .ok());
+}
+
+TEST_F(JsepTransportControllerTest, RollbackRestoresRejectedTransport) {
+ static const char kMid1Audio[] = "1_audio";
+
+ // Perform initial offer/answer.
+ CreateJsepTransportController(JsepTransportController::Config());
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ std::unique_ptr<cricket::SessionDescription> remote_answer(
+ local_offer->Clone());
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+
+ // Apply a reoffer which rejects the m= section, causing the transport to be
+ // set to null.
+ auto local_reoffer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_reoffer->contents()[0].rejected = true;
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_reoffer.get())
+ .ok());
+ auto old_mid1_transport = mid1_transport;
+ mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ EXPECT_EQ(nullptr, mid1_transport);
+
+ // Rolling back shouldn't just create a new transport for MID 1, it should
+ // restore the old transport.
+ EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+ mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ EXPECT_EQ(old_mid1_transport, mid1_transport);
+}
+
+// If an offer with a modified BUNDLE group causes a MID->transport mapping to
+// change, rollback should restore the previous mapping.
+TEST_F(JsepTransportControllerTest, RollbackRestoresPreviousTransportMapping) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Audio[] = "3_audio";
+
+ // Perform an initial offer/answer to establish a (kMid1Audio,kMid2Audio)
+ // group.
+ CreateJsepTransportController(JsepTransportController::Config());
+ cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group.AddContentName(kMid1Audio);
+ bundle_group.AddContentName(kMid2Audio);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group);
+
+ std::unique_ptr<cricket::SessionDescription> remote_answer(
+ local_offer->Clone());
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ EXPECT_EQ(mid1_transport, mid2_transport);
+ EXPECT_NE(mid1_transport, mid3_transport);
+
+ // Apply a reoffer adding kMid3Audio to the group; transport mapping should
+ // change, even without an answer, since this is an existing group.
+ bundle_group.AddContentName(kMid3Audio);
+ auto local_reoffer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_reoffer.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddAudioSection(local_reoffer.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_reoffer->AddGroup(bundle_group);
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_reoffer.get())
+ .ok());
+
+ // Store the old transport pointer and verify that the offer actually changed
+ // transports.
+ auto old_mid3_transport = mid3_transport;
+ mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ EXPECT_EQ(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid1_transport, mid3_transport);
+
+ // Rolling back shouldn't just create a new transport for MID 3, it should
+ // restore the old transport.
+ EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+ mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ EXPECT_EQ(old_mid3_transport, mid3_transport);
+}
+
+// Test that if an offer adds a MID to a specific BUNDLE group and is then
+// rolled back, it can be added to a different BUNDLE group in a new offer.
+// This is effectively testing that rollback resets the BundleManager state.
+TEST_F(JsepTransportControllerTest, RollbackAndAddToDifferentBundleGroup) {
+ static const char kMid1Audio[] = "1_audio";
+ static const char kMid2Audio[] = "2_audio";
+ static const char kMid3Audio[] = "3_audio";
+
+ // Perform an initial offer/answer to establish two bundle groups, each with
+ // one MID.
+ CreateJsepTransportController(JsepTransportController::Config());
+ cricket::ContentGroup bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group1.AddContentName(kMid1Audio);
+ cricket::ContentGroup bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ bundle_group2.AddContentName(kMid2Audio);
+
+ auto local_offer = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ local_offer->AddGroup(bundle_group1);
+ local_offer->AddGroup(bundle_group2);
+
+ std::unique_ptr<cricket::SessionDescription> remote_answer(
+ local_offer->Clone());
+
+ EXPECT_TRUE(transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, local_offer.get())
+ .ok());
+ EXPECT_TRUE(transport_controller_
+ ->SetRemoteDescription(SdpType::kAnswer, remote_answer.get())
+ .ok());
+
+ // Apply an offer that adds kMid3Audio to the first BUNDLE group.,
+ cricket::ContentGroup modified_bundle_group1(cricket::GROUP_TYPE_BUNDLE);
+ modified_bundle_group1.AddContentName(kMid1Audio);
+ modified_bundle_group1.AddContentName(kMid3Audio);
+ auto subsequent_offer_1 = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(subsequent_offer_1.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer_1.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer_1.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ subsequent_offer_1->AddGroup(modified_bundle_group1);
+ subsequent_offer_1->AddGroup(bundle_group2);
+
+ EXPECT_TRUE(
+ transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, subsequent_offer_1.get())
+ .ok());
+
+ auto mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ auto mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ auto mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ EXPECT_NE(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid1_transport, mid3_transport);
+
+ // Rollback and expect the transport to be reset.
+ EXPECT_TRUE(transport_controller_->RollbackTransports().ok());
+ EXPECT_EQ(nullptr, transport_controller_->GetRtpTransport(kMid3Audio));
+
+ // Apply an offer that adds kMid3Audio to the second BUNDLE group.,
+ cricket::ContentGroup modified_bundle_group2(cricket::GROUP_TYPE_BUNDLE);
+ modified_bundle_group2.AddContentName(kMid2Audio);
+ modified_bundle_group2.AddContentName(kMid3Audio);
+ auto subsequent_offer_2 = std::make_unique<cricket::SessionDescription>();
+ AddAudioSection(subsequent_offer_2.get(), kMid1Audio, kIceUfrag1, kIcePwd1,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer_2.get(), kMid2Audio, kIceUfrag2, kIcePwd2,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ AddVideoSection(subsequent_offer_2.get(), kMid3Audio, kIceUfrag3, kIcePwd3,
+ cricket::ICEMODE_FULL, cricket::CONNECTIONROLE_ACTPASS,
+ nullptr);
+ subsequent_offer_2->AddGroup(bundle_group1);
+ subsequent_offer_2->AddGroup(modified_bundle_group2);
+
+ EXPECT_TRUE(
+ transport_controller_
+ ->SetLocalDescription(SdpType::kOffer, subsequent_offer_2.get())
+ .ok());
+
+ mid1_transport = transport_controller_->GetRtpTransport(kMid1Audio);
+ mid2_transport = transport_controller_->GetRtpTransport(kMid2Audio);
+ mid3_transport = transport_controller_->GetRtpTransport(kMid3Audio);
+ EXPECT_NE(mid1_transport, mid2_transport);
+ EXPECT_EQ(mid2_transport, mid3_transport);
+}
+
+} // namespace webrtc