/* * 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 #include #include #include #include #include #include #include "api/candidate.h" #include "api/crypto/crypto_options.h" #include "api/dtls_transport_interface.h" #include "api/environment/environment.h" #include "api/environment/environment_factory.h" #include "api/field_trials.h" #include "api/ice_transport_interface.h" #include "api/jsep.h" #include "api/make_ref_counted.h" #include "api/peer_connection_interface.h" #include "api/rtc_error.h" #include "api/scoped_refptr.h" #include "api/test/rtc_error_matchers.h" #include "api/transport/data_channel_transport_interface.h" #include "api/transport/enums.h" #include "api/units/time_delta.h" #include "call/payload_type.h" #include "call/payload_type_picker.h" #include "media/base/codec.h" #include "media/base/media_constants.h" #include "p2p/base/ice_transport_internal.h" #include "p2p/base/p2p_constants.h" #include "p2p/base/port_allocator.h" #include "p2p/base/transport_description.h" #include "p2p/base/transport_info.h" #include "p2p/dtls/dtls_transport_factory.h" #include "p2p/dtls/dtls_transport_internal.h" #include "p2p/dtls/fake_dtls_transport.h" #include "p2p/test/fake_ice_transport.h" #include "pc/dtls_transport.h" #include "pc/rtp_transport_internal.h" #include "pc/session_description.h" #include "pc/transport_stats.h" #include "rtc_base/checks.h" #include "rtc_base/copy_on_write_buffer.h" #include "rtc_base/fake_ssl_identity.h" #include "rtc_base/logging.h" #include "rtc_base/net_helper.h" #include "rtc_base/rtc_certificate.h" #include "rtc_base/socket_address.h" #include "rtc_base/ssl_certificate.h" #include "rtc_base/ssl_fingerprint.h" #include "rtc_base/ssl_identity.h" #include "rtc_base/ssl_stream_adapter.h" #include "rtc_base/task_queue_for_test.h" #include "rtc_base/third_party/sigslot/sigslot.h" #include "rtc_base/thread.h" #include "test/create_test_field_trials.h" #include "test/gmock.h" #include "test/gtest.h" #include "test/wait_until.h" using webrtc::Candidate; using ::webrtc::Candidates; using ::webrtc::FakeDtlsTransport; 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 IceTransportFactory { public: ~FakeIceTransportFactory() override = default; scoped_refptr CreateIceTransport( const std::string& transport_name, int component, IceTransportInit init) override { return make_ref_counted( std::make_unique(transport_name, component)); } }; class FakeDtlsTransportFactory : public DtlsTransportFactory { public: std::unique_ptr CreateDtlsTransport( IceTransportInternal* ice, const CryptoOptions& crypto_options, SSLProtocolVersion max_version) override { return std::make_unique( static_cast(ice)); } }; class JsepTransportControllerTest : public JsepTransportController::Observer, public ::testing::Test, public sigslot::has_slots<> { public: JsepTransportControllerTest() : env_(CreateEnvironment(&field_trials_)), signaling_thread_(Thread::Current()) { fake_ice_transport_factory_ = std::make_unique(); fake_dtls_transport_factory_ = std::make_unique(); } void CreateJsepTransportController(JsepTransportController::Config config, Thread* network_thread = Thread::Current(), PortAllocator* port_allocator = nullptr) { config.transport_observer = this; config.rtcp_handler = [](const 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_ = [](SSLHandshakeError s) {}; transport_controller_ = std::make_unique( env_, network_thread, port_allocator, /*async_resolver_factory=*/nullptr, /*lna_permission_factory=*/nullptr, payload_type_picker_, std::move(config)); SendTask(network_thread, [&] { ConnectTransportControllerSignals(); }); } void ConnectTransportControllerSignals() { transport_controller_->SubscribeIceConnectionState( [this](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](IceGatheringState s) { JsepTransportControllerTest::OnGatheringState(s); }); transport_controller_->SubscribeIceCandidateGathered( [this](const std::string& transport, const std::vector& candidates) { JsepTransportControllerTest::OnCandidatesGathered(transport, candidates); }); } std::unique_ptr CreateSessionDescriptionWithoutBundle() { auto description = std::make_unique(); AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(description.get(), kVideoMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); return description; } std::unique_ptr CreateSessionDescriptionWithBundleGroup() { auto description = CreateSessionDescriptionWithoutBundle(); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); description->AddGroup(bundle_group); return description; } std::unique_ptr CreateSessionDescriptionWithBundledData() { auto description = CreateSessionDescriptionWithoutBundle(); AddDataSection(description.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); bundle_group.AddContentName(kDataMid1); description->AddGroup(bundle_group); return description; } void AddAudioSection(SessionDescription* description, const std::string& mid, const std::string& ufrag, const std::string& pwd, IceMode ice_mode, ConnectionRole conn_role, scoped_refptr cert) { std::unique_ptr audio( new AudioContentDescription()); // Set RTCP-mux to be true because the default policy is "mux required". audio->set_rtcp_mux(true); description->AddContent(mid, MediaProtocolType::kRtp, /*rejected=*/false, std::move(audio)); AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); } void AddVideoSection(SessionDescription* description, const std::string& mid, const std::string& ufrag, const std::string& pwd, IceMode ice_mode, ConnectionRole conn_role, scoped_refptr cert) { std::unique_ptr video( new VideoContentDescription()); // Set RTCP-mux to be true because the default policy is "mux required". video->set_rtcp_mux(true); description->AddContent(mid, MediaProtocolType::kRtp, /*rejected=*/false, std::move(video)); AddTransportInfo(description, mid, ufrag, pwd, ice_mode, conn_role, cert); } void AddDataSection(SessionDescription* description, const std::string& mid, MediaProtocolType protocol_type, const std::string& ufrag, const std::string& pwd, IceMode ice_mode, ConnectionRole conn_role, scoped_refptr cert) { RTC_CHECK(protocol_type == MediaProtocolType::kSctp); std::unique_ptr data( new 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(SessionDescription* description, const std::string& mid, const std::string& ufrag, const std::string& pwd, IceMode ice_mode, ConnectionRole conn_role, scoped_refptr cert) { std::unique_ptr fingerprint; if (cert) { fingerprint = SSLFingerprint::CreateFromCertificate(*cert); } TransportDescription transport_desc(std::vector(), ufrag, pwd, ice_mode, conn_role, fingerprint.get()); description->AddTransportInfo(TransportInfo(mid, transport_desc)); } IceConfig CreateIceConfig( int receiving_timeout, ContinualGatheringPolicy continual_gathering_policy) { IceConfig config; config.receiving_timeout = receiving_timeout; config.continual_gathering_policy = continual_gathering_policy; return config; } Candidate CreateCandidate(int component = ICE_CANDIDATE_COMPONENT_RTP) { Candidate c; c.set_address(SocketAddress("192.168.1.1", 8000)); c.set_component(component); c.set_protocol(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(), nullptr) .ok()); transport_controller_->MaybeStartGathering(); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); auto fake_video_dtls = static_cast( transport_controller_->GetDtlsTransport(kVideoMid1)); fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered( fake_audio_dtls->fake_ice_transport(), CreateCandidate()); fake_video_dtls->fake_ice_transport()->SignalCandidateGathered( fake_video_dtls->fake_ice_transport(), CreateCandidate()); 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(IceConnectionState state) { ice_signaled_on_thread_ = Thread::Current(); connection_state_ = state; ++connection_state_signal_count_; } void OnStandardizedIceConnectionState( PeerConnectionInterface::IceConnectionState state) { ice_signaled_on_thread_ = Thread::Current(); ice_connection_state_ = state; ++ice_connection_state_signal_count_; } void OnCombinedConnectionState( PeerConnectionInterface::PeerConnectionState state) { RTC_LOG(LS_INFO) << "OnCombinedConnectionState: " << static_cast(state); ice_signaled_on_thread_ = Thread::Current(); combined_connection_state_ = state; ++combined_connection_state_signal_count_; } void OnGatheringState(IceGatheringState state) { ice_signaled_on_thread_ = Thread::Current(); gathering_state_ = state; ++gathering_state_signal_count_; } void OnCandidatesGathered(const std::string& transport_name, const Candidates& candidates) { ice_signaled_on_thread_ = 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, scoped_refptr 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; } FieldTrials field_trials_ = CreateTestFieldTrials(); Environment env_; AutoThread main_thread_; // Information received from signals from transport controller. IceConnectionState connection_state_ = kIceConnectionConnecting; PeerConnectionInterface::IceConnectionState ice_connection_state_ = PeerConnectionInterface::kIceConnectionNew; PeerConnectionInterface::PeerConnectionState combined_connection_state_ = PeerConnectionInterface::PeerConnectionState::kNew; bool receiving_ = false; IceGatheringState gathering_state_ = kIceGatheringNew; // transport_name => candidates std::map 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 network_thread_; std::unique_ptr fake_ice_transport_factory_; std::unique_ptr fake_dtls_transport_factory_; Thread* const signaling_thread_ = nullptr; Thread* ice_signaled_on_thread_ = nullptr; // Used to verify the SignalRtpTransportChanged/SignalDtlsTransportChanged are // signaled correctly. std::map changed_rtp_transport_by_mid_; std::map changed_dtls_transport_by_mid_; PayloadTypePicker payload_type_picker_; // Transport controller needs to be destroyed first, because it may issue // callbacks that modify the changed_*_by_mid in the destructor. std::unique_ptr transport_controller_; }; TEST_F(JsepTransportControllerTest, GetRtpTransport) { CreateJsepTransportController(JsepTransportController::Config()); auto description = CreateSessionDescriptionWithoutBundle(); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, description.get(), nullptr) .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(std::move(config)); auto description = CreateSessionDescriptionWithoutBundle(); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, description.get(), nullptr) .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); DtlsTransport* my_transport = static_cast(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(std::move(config)); auto description = CreateSessionDescriptionWithoutBundle(); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, description.get(), nullptr) .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(), nullptr) .ok()); transport_controller_->SetIceConfig( CreateIceConfig(kTimeout, GATHER_CONTINUALLY)); FakeDtlsTransport* fake_audio_dtls = static_cast( 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, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, description.get(), nullptr) .ok()); fake_audio_dtls = static_cast( 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(), nullptr) .ok()); // TODO(tommi): Note that _now_ we set `remote`. (was not set before). EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, description.get(), 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(), 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(), nullptr) .ok()); // After setting the local description, we should be able to start gathering // candidates. transport_controller_->MaybeStartGathering(); EXPECT_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, gathering_state_signal_count_); } TEST_F(JsepTransportControllerTest, AddRemoveRemoteCandidates) { CreateJsepTransportController(JsepTransportController::Config()); auto description = CreateSessionDescriptionWithoutBundle(); transport_controller_->SetLocalDescription(SdpType::kOffer, description.get(), nullptr); transport_controller_->SetRemoteDescription( SdpType::kAnswer, description.get(), description.get()); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); ASSERT_NE(nullptr, fake_audio_dtls); Candidates candidates; candidates.push_back(CreateCandidate()); EXPECT_TRUE( transport_controller_->AddRemoteCandidates(kAudioMid1, candidates).ok()); EXPECT_EQ(1U, fake_audio_dtls->fake_ice_transport()->remote_candidates().size()); IceCandidate ice_candidate(kAudioMid1, -1, candidates[0]); EXPECT_TRUE(transport_controller_->RemoveRemoteCandidate(&ice_candidate)); EXPECT_EQ(0U, fake_audio_dtls->fake_ice_transport()->remote_candidates().size()); } TEST_F(JsepTransportControllerTest, SetAndGetLocalCertificate) { CreateJsepTransportController(JsepTransportController::Config()); scoped_refptr certificate1 = RTCCertificate::Create(SSLIdentity::Create("session1", KT_DEFAULT)); scoped_refptr returned_certificate; auto description = std::make_unique(); AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, 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(), nullptr) .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. scoped_refptr certificate2 = RTCCertificate::Create(SSLIdentity::Create("session2", 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(), nullptr) .ok()); FakeSSLCertificate fake_certificate("fake_data"); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); fake_audio_dtls->SetRemoteSSLCertificate(&fake_certificate); std::unique_ptr 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 = RTCCertificate::Create(SSLIdentity::Create("offer", KT_DEFAULT)); auto answer_certificate = RTCCertificate::Create(SSLIdentity::Create("answer", KT_DEFAULT)); transport_controller_->SetLocalCertificate(offer_certificate); auto offer_desc = std::make_unique(); AddAudioSection(offer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, offer_certificate); auto answer_desc = std::make_unique(); AddAudioSection(answer_desc.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, answer_certificate); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, offer_desc.get(), nullptr) .ok()); std::optional role = transport_controller_->GetDtlsRole(kAudioMid1); // The DTLS role is not decided yet. EXPECT_FALSE(role); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, offer_desc.get(), answer_desc.get()) .ok()); role = transport_controller_->GetDtlsRole(kAudioMid1); ASSERT_TRUE(role); EXPECT_EQ(SSL_CLIENT, *role); } TEST_F(JsepTransportControllerTest, GetStats) { CreateJsepTransportController(JsepTransportController::Config()); auto description = CreateSessionDescriptionWithBundleGroup(); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, description.get(), nullptr) .ok()); 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(), nullptr) .ok()); auto fake_ice = static_cast( 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_THAT(WaitUntil([&] { return kIceConnectionFailed; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, connection_state_signal_count_); EXPECT_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionFailed; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kFailed; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), nullptr) .ok()); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); auto fake_video_dtls = static_cast( 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_THAT(WaitUntil([&] { return kIceConnectionFailed; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, connection_state_signal_count_); EXPECT_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionFailed; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kFailed; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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 webrtc::FakeIceTransport will set // the transport state to be STATE_CONNECTING. fake_video_dtls->fake_ice_transport()->SetConnectionCount(2); fake_video_dtls->SetWritable(true); EXPECT_THAT(WaitUntil([&] { return kIceConnectionConnected; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::kIceConnectionConnected; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(3, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kConnected; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), nullptr) .ok()); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); auto fake_video_dtls = static_cast( 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, IceTransportStateInternal::STATE_COMPLETED); fake_audio_dtls->SetWritable(true); fake_audio_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); EXPECT_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionChecking; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kConnecting; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, combined_connection_state_signal_count_); fake_video_dtls->fake_ice_transport()->SetTransportState( IceTransportState::kFailed, IceTransportStateInternal::STATE_FAILED); fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); EXPECT_THAT(WaitUntil([&] { return kIceConnectionFailed; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, connection_state_signal_count_); EXPECT_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionFailed; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kFailed; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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 webrtc::FakeIceTransport will set // the transport state to be STATE_COMPLETED. fake_video_dtls->fake_ice_transport()->SetTransportState( IceTransportState::kCompleted, IceTransportStateInternal::STATE_COMPLETED); fake_video_dtls->SetWritable(true); EXPECT_THAT(WaitUntil([&] { return kIceConnectionCompleted; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(3, connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::kIceConnectionCompleted; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(3, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kConnected; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), nullptr) .ok()); auto fake_audio_dtls = static_cast( 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_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), nullptr) .ok()); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); auto fake_video_dtls = static_cast( transport_controller_->GetDtlsTransport(kVideoMid1)); fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); EXPECT_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, gathering_state_signal_count_); fake_video_dtls->fake_ice_transport()->SetCandidatesGatheringComplete(); EXPECT_THAT(WaitUntil([&] { return kIceGatheringComplete; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), nullptr) .ok()); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); auto fake_video_dtls = static_cast( transport_controller_->GetDtlsTransport(kVideoMid1)); EXPECT_NE(fake_audio_dtls, fake_video_dtls); fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); EXPECT_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), description.get()) .ok()); // The BUNDLE should be enabled, the incomplete video transport should be // deleted and the states should be updated. fake_video_dtls = static_cast( transport_controller_->GetDtlsTransport(kVideoMid1)); EXPECT_EQ(fake_audio_dtls, fake_video_dtls); EXPECT_THAT(WaitUntil([&] { return kIceConnectionCompleted; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(PeerConnectionInterface::kIceConnectionCompleted, ice_connection_state_); EXPECT_EQ(PeerConnectionInterface::PeerConnectionState::kConnected, combined_connection_state_); EXPECT_THAT(WaitUntil([&] { return kIceGatheringComplete; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(); AddAudioSection(description.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, description.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, description.get(), description.get()) .ok()); // Trigger and verify initial non-new states. auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); fake_audio_dtls->fake_ice_transport()->MaybeStartGathering(); fake_audio_dtls->fake_ice_transport()->SetTransportState( IceTransportState::kChecking, IceTransportStateInternal::STATE_CONNECTING); EXPECT_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionChecking; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kConnecting; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1, combined_connection_state_signal_count_); EXPECT_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), description.get()) .ok()); EXPECT_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionNew; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kNew; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, combined_connection_state_signal_count_); EXPECT_THAT(WaitUntil([&] { return kIceGatheringNew; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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_THAT( WaitUntil([&] { return PeerConnectionInterface::kIceConnectionChecking; }, ::testing::Eq(ice_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(3, ice_connection_state_signal_count_); EXPECT_THAT( WaitUntil( [&] { return PeerConnectionInterface::PeerConnectionState::kConnecting; }, ::testing::Eq(combined_connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(3, combined_connection_state_signal_count_); EXPECT_THAT(WaitUntil([&] { return kIceGatheringGathering; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(), nullptr) .ok()); transport_controller_->MaybeStartGathering(); auto fake_audio_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); fake_audio_dtls->fake_ice_transport()->SignalCandidateGathered( fake_audio_dtls->fake_ice_transport(), CreateCandidate()); EXPECT_THAT( WaitUntil([&] { return 1; }, ::testing::Eq(candidates_signal_count_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(1u, candidates_[kAudioMid1].size()); } TEST_F(JsepTransportControllerTest, IceSignalingOccursOnNetworkThread) { network_thread_ = 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_THAT(WaitUntil([&] { return kIceConnectionCompleted; }, ::testing::Eq(connection_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, connection_state_signal_count_); // new --> gathering --> complete EXPECT_THAT(WaitUntil([&] { return kIceGatheringComplete; }, ::testing::Eq(gathering_state_), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_EQ(2, gathering_state_signal_count_); EXPECT_THAT( WaitUntil([&] { return candidates_[kAudioMid1].size(); }, ::testing::Eq(1u), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); EXPECT_THAT( WaitUntil([&] { return candidates_[kVideoMid1].size(); }, ::testing::Eq(1u), {.timeout = TimeDelta::Millis(kTimeout)}), IsRtcOk()); 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(std::move(config)); // Let the `transport_controller_` be the controlled side initially. auto remote_offer = std::make_unique(); AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto local_answer = std::make_unique(); AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); EXPECT_TRUE( transport_controller_ ->SetRemoteDescription(SdpType::kOffer, nullptr, remote_offer.get()) .ok()); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kAnswer, local_answer.get(), remote_offer.get()) .ok()); auto fake_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); EXPECT_EQ(ICEROLE_CONTROLLED, fake_dtls->fake_ice_transport()->GetIceRole()); // New offer will trigger the ICE restart. auto restart_local_offer = std::make_unique(); AddAudioSection(restart_local_offer.get(), kAudioMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, restart_local_offer.get(), remote_offer.get()) .ok()); EXPECT_EQ(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(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); auto fake_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); EXPECT_EQ(ICEROLE_CONTROLLING, fake_dtls->fake_ice_transport()->GetIceRole()); EXPECT_EQ(ICEMODE_FULL, fake_dtls->fake_ice_transport()->remote_ice_mode()); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, ICEMODE_LITE, CONNECTIONROLE_PASSIVE, nullptr); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); EXPECT_EQ(ICEROLE_CONTROLLING, fake_dtls->fake_ice_transport()->GetIceRole()); EXPECT_EQ(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(); AddAudioSection(remote_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_LITE, CONNECTIONROLE_ACTPASS, nullptr); auto local_answer = std::make_unique(); AddAudioSection(local_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, 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, nullptr, remote_offer.get()) .ok()); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kAnswer, local_answer.get(), remote_offer.get()) .ok()); auto fake_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); EXPECT_EQ(ICEROLE_CONTROLLING, fake_dtls->fake_ice_transport()->GetIceRole()); // In the subsequence remote offer triggers an ICE restart. auto remote_offer2 = std::make_unique(); AddAudioSection(remote_offer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2, ICEMODE_LITE, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kOffer, local_answer.get(), remote_offer2.get()) .ok()); auto local_answer2 = std::make_unique(); AddAudioSection(local_answer2.get(), kAudioMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kAnswer, local_answer2.get(), remote_offer2.get()) .ok()); fake_dtls = static_cast( transport_controller_->GetDtlsTransport(kAudioMid1)); // The local side is still the controlling role since the remote side is using // ICE-Lite. EXPECT_EQ(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()); ContentGroup bundle_group(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(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddDataSection(local_offer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddDataSection(remote_answer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); local_offer->AddGroup(bundle_group); remote_answer->AddGroup(bundle_group); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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()); ContentGroup bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid1Audio); bundle_group1.AddContentName(kMid2Video); ContentGroup bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid3Audio); bundle_group2.AddContentName(kMid4Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group1); local_offer->AddGroup(bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); remote_answer->AddGroup(bundle_group1); remote_answer->AddGroup(bundle_group2); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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()); ContentGroup bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid1Audio); bundle_group1.AddContentName(kMid2Video); ContentGroup bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid3Audio); bundle_group2.AddContentName(kMid4Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); // The offer has both groups. local_offer->AddGroup(bundle_group1); local_offer->AddGroup(bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, 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(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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). ContentGroup offer_bundle_group1(GROUP_TYPE_BUNDLE); offer_bundle_group1.AddContentName(kMid1Audio); offer_bundle_group1.AddContentName(kMid2Video); ContentGroup offer_bundle_group2(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. ContentGroup answer_bundle_group1(GROUP_TYPE_BUNDLE); answer_bundle_group1.AddContentName(kMid1Audio); answer_bundle_group1.AddContentName(kMid4Video); ContentGroup answer_bundle_group2(GROUP_TYPE_BUNDLE); answer_bundle_group2.AddContentName(kMid3Audio); answer_bundle_group2.AddContentName(kMid2Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(offer_bundle_group1); local_offer->AddGroup(offer_bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, 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(), nullptr) .ok()); // Reject answer! EXPECT_FALSE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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). ContentGroup offer_bundle_group1(GROUP_TYPE_BUNDLE); offer_bundle_group1.AddContentName(kMid1Audio); offer_bundle_group1.AddContentName(kMid2Video); ContentGroup offer_bundle_group2(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. ContentGroup answer_bundle_group1(GROUP_TYPE_BUNDLE); answer_bundle_group1.AddContentName(kMid1Audio); ContentGroup answer_bundle_group2(GROUP_TYPE_BUNDLE); answer_bundle_group2.AddContentName(kMid2Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(offer_bundle_group1); local_offer->AddGroup(offer_bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag4, kIcePwd4, ICEMODE_FULL, 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(), nullptr) .ok()); // Reject answer! EXPECT_FALSE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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. ContentGroup offer_bundle_group1(GROUP_TYPE_BUNDLE); offer_bundle_group1.AddContentName(kMid1Audio); offer_bundle_group1.AddContentName(kMid3Audio); ContentGroup offer_bundle_group2(GROUP_TYPE_BUNDLE); offer_bundle_group2.AddContentName(kMid2Video); offer_bundle_group2.AddContentName(kMid3Audio); auto offer = std::make_unique(); AddAudioSection(offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(offer.get(), kMid2Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, 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(), nullptr) .ok()); EXPECT_FALSE( transport_controller_ ->SetRemoteDescription(SdpType::kOffer, offer.get(), 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). ContentGroup offer_bundle_group1(GROUP_TYPE_BUNDLE); offer_bundle_group1.AddContentName(kMid1Audio); offer_bundle_group1.AddContentName(kMid2Audio); offer_bundle_group1.AddContentName(kMid3Audio); ContentGroup offer_bundle_group2(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. ContentGroup answer_bundle_group1(GROUP_TYPE_BUNDLE); answer_bundle_group1.AddContentName(kMid2Audio); answer_bundle_group1.AddContentName(kMid3Audio); ContentGroup answer_bundle_group2(GROUP_TYPE_BUNDLE); answer_bundle_group2.AddContentName(kMid5Video); answer_bundle_group2.AddContentName(kMid6Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(offer_bundle_group1); local_offer->AddGroup(offer_bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid6Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, 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(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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). ContentGroup offer_bundle_group1(GROUP_TYPE_BUNDLE); offer_bundle_group1.AddContentName(kMid1Audio); offer_bundle_group1.AddContentName(kMid2Audio); offer_bundle_group1.AddContentName(kMid3Audio); ContentGroup offer_bundle_group2(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. ContentGroup answer_bundle_group1(GROUP_TYPE_BUNDLE); answer_bundle_group1.AddContentName(kMid2Audio); answer_bundle_group1.AddContentName(kMid1Audio); answer_bundle_group1.AddContentName(kMid3Audio); ContentGroup answer_bundle_group2(GROUP_TYPE_BUNDLE); answer_bundle_group2.AddContentName(kMid5Video); answer_bundle_group2.AddContentName(kMid6Video); answer_bundle_group2.AddContentName(kMid4Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(offer_bundle_group1); local_offer->AddGroup(offer_bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid6Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, 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(), nullptr) .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, local_offer.get(), 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). ContentGroup bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid1Audio); bundle_group1.AddContentName(kMid2Audio); ContentGroup bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid4Video); bundle_group2.AddContentName(kMid5Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group1); local_offer->AddGroup(bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); remote_answer->AddGroup(bundle_group1); remote_answer->AddGroup(bundle_group2); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); // Add kMid3Audio and kMid6Video to the respective audio/video bundle groups. ContentGroup new_bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid3Audio); ContentGroup new_bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid6Video); auto subsequent_offer = std::make_unique(); AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(subsequent_offer.get(), kMid3Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid5Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid6Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); subsequent_offer->AddGroup(bundle_group1); subsequent_offer->AddGroup(bundle_group2); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get(), 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, 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). ContentGroup bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid1Audio); bundle_group1.AddContentName(kMid2Audio); ContentGroup bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid3Video); bundle_group2.AddContentName(kMid4Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group1); local_offer->AddGroup(bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); remote_answer->AddGroup(bundle_group1); remote_answer->AddGroup(bundle_group2); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); // Switch to grouping (kMid1Audio,kMid2Audio,kMid3Video,kMid4Video). // This is a illegal without first removing m= sections from their groups. ContentGroup new_bundle_group(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(); AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); subsequent_offer->AddGroup(new_bundle_group); EXPECT_FALSE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, subsequent_offer.get(), remote_answer.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). ContentGroup bundle_group(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(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); remote_answer->AddGroup(bundle_group); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); // Switch to grouping (kMid1Audio,kMid2Audio) and (kMid3Video,kMid4Video). // This is a illegal without first removing m= sections from their groups. ContentGroup new_bundle_group1(GROUP_TYPE_BUNDLE); new_bundle_group1.AddContentName(kMid1Audio); new_bundle_group1.AddContentName(kMid2Audio); ContentGroup new_bundle_group2(GROUP_TYPE_BUNDLE); new_bundle_group2.AddContentName(kMid3Video); new_bundle_group2.AddContentName(kMid4Video); auto subsequent_offer = std::make_unique(); AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, 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(), remote_answer.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). ContentGroup bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid1Audio); bundle_group1.AddContentName(kMid2Audio); ContentGroup bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid3Video); bundle_group2.AddContentName(kMid4Video); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group1); local_offer->AddGroup(bundle_group2); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(remote_answer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); remote_answer->AddGroup(bundle_group1); remote_answer->AddGroup(bundle_group2); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); // Switch to grouping (kMid1Audio,kMid3Video) and (kMid2Audio,kMid3Video). // This is a illegal without first removing m= sections from their groups. ContentGroup new_bundle_group1(GROUP_TYPE_BUNDLE); new_bundle_group1.AddContentName(kMid1Audio); new_bundle_group1.AddContentName(kMid3Video); ContentGroup new_bundle_group2(GROUP_TYPE_BUNDLE); new_bundle_group2.AddContentName(kMid2Audio); new_bundle_group2.AddContentName(kMid4Video); auto subsequent_offer = std::make_unique(); AddAudioSection(subsequent_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(subsequent_offer.get(), kMid2Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid3Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer.get(), kMid4Video, kIceUfrag2, kIcePwd2, ICEMODE_FULL, 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(), remote_answer.get()) .ok()); } // Tests that only a subset of all the m= sections are bundled. TEST_F(JsepTransportControllerTest, BundleSubsetOfMediaSections) { CreateJsepTransportController(JsepTransportController::Config()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddAudioSection(remote_answer.get(), kAudioMid2, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); local_offer->AddGroup(bundle_group); remote_answer->AddGroup(bundle_group); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kDataMid1); auto local_offer = std::make_unique(); AddDataSection(local_offer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto remote_answer = std::make_unique(); AddDataSection(remote_answer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); local_offer->AddGroup(bundle_group); remote_answer->AddGroup(bundle_group); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); // Reset the bundle group and do another offer/answer exchange. bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); local_offer->RemoveGroupByName(GROUP_TYPE_BUNDLE); local_offer->AddGroup(bundle_group); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), remote_answer.get()) .ok()); remote_answer->RemoveGroupByName(GROUP_TYPE_BUNDLE); remote_answer->AddGroup(bundle_group); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); bundle_group.AddContentName(kDataMid1); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddDataSection(local_offer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddDataSection(remote_answer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, ICEMODE_FULL, 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(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); local_offer->AddGroup(bundle_group); remote_answer->AddGroup(bundle_group); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), remote_answer.get()) .ok()); // The answerer uses the new bundle group and now the bundle mid is changed to // `kVideo1`. remote_answer->RemoveGroupByName(GROUP_TYPE_BUNDLE); remote_answer->AddGroup(bundle_group); EXPECT_FALSE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); bundle_group.AddContentName(kDataMid1); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddDataSection(local_offer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddVideoSection(remote_answer.get(), kVideoMid1, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_PASSIVE, nullptr); AddDataSection(remote_answer.get(), kDataMid1, MediaProtocolType::kSctp, kIceUfrag3, kIcePwd3, ICEMODE_FULL, 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(), nullptr) .ok()); EXPECT_FALSE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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, local_offer.get(), 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(std::move(config)); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, 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(), nullptr) .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(std::move(config)); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); auto remote_answer = std::make_unique(); AddAudioSection(remote_answer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, 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, local_offer.get(), 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(); ContentGroup offer_bundle_group(GROUP_TYPE_BUNDLE); offer_bundle_group.AddContentName(kAudioMid1); local_offer->AddGroup(offer_bundle_group); ContentGroup answer_bundle_group(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(), nullptr) .ok()); EXPECT_FALSE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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(); ContentGroup invalid_bundle_group(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(), nullptr) .ok()); EXPECT_FALSE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); // Do an re-offer/answer. EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), remote_answer.get()) .ok()); auto new_answer = CreateSessionDescriptionWithoutBundle(); ContentGroup new_bundle_group(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, local_offer.get(), 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, local_offer.get(), 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(); AddAudioSection(local_offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); local_offer->AddGroup(bundle_group); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); std::unique_ptr remote_answer(local_offer->Clone()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); std::unique_ptr local_reoffer(local_offer->Clone()); local_reoffer->contents()[0].rejected = true; AddVideoSection(local_reoffer.get(), kVideoMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_reoffer->RemoveGroupByName(GROUP_TYPE_BUNDLE); ContentGroup new_bundle_group(GROUP_TYPE_BUNDLE); new_bundle_group.AddContentName(kVideoMid1); local_reoffer->AddGroup(new_bundle_group); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_reoffer.get(), remote_answer.get()) .ok()); std::unique_ptr remote_reanswer(local_reoffer->Clone()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_reoffer.get(), 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(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); std::unique_ptr remote_answer(local_offer->Clone()); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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(); AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_reoffer->contents()[0].rejected = true; EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_reoffer.get(), remote_answer.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()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kMid1Audio); bundle_group.AddContentName(kMid2Audio); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_offer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group); std::unique_ptr remote_answer(local_offer->Clone()); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), 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(); AddAudioSection(local_reoffer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_reoffer.get(), kMid2Audio, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddAudioSection(local_reoffer.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_reoffer->AddGroup(bundle_group); EXPECT_TRUE(transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_reoffer.get(), remote_answer.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()); ContentGroup bundle_group1(GROUP_TYPE_BUNDLE); bundle_group1.AddContentName(kMid1Audio); ContentGroup bundle_group2(GROUP_TYPE_BUNDLE); bundle_group2.AddContentName(kMid2Audio); auto local_offer = std::make_unique(); AddAudioSection(local_offer.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(local_offer.get(), kMid2Audio, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); local_offer->AddGroup(bundle_group1); local_offer->AddGroup(bundle_group2); std::unique_ptr remote_answer(local_offer->Clone()); EXPECT_TRUE( transport_controller_ ->SetLocalDescription(SdpType::kOffer, local_offer.get(), nullptr) .ok()); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kAnswer, local_offer.get(), remote_answer.get()) .ok()); // Apply an offer that adds kMid3Audio to the first BUNDLE group., ContentGroup modified_bundle_group1(GROUP_TYPE_BUNDLE); modified_bundle_group1.AddContentName(kMid1Audio); modified_bundle_group1.AddContentName(kMid3Audio); auto subsequent_offer_1 = std::make_unique(); AddAudioSection(subsequent_offer_1.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer_1.get(), kMid2Audio, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer_1.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, 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(), 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_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., ContentGroup modified_bundle_group2(GROUP_TYPE_BUNDLE); modified_bundle_group2.AddContentName(kMid2Audio); modified_bundle_group2.AddContentName(kMid3Audio); auto subsequent_offer_2 = std::make_unique(); AddAudioSection(subsequent_offer_2.get(), kMid1Audio, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer_2.get(), kMid2Audio, kIceUfrag2, kIcePwd2, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(subsequent_offer_2.get(), kMid3Audio, kIceUfrag3, kIcePwd3, ICEMODE_FULL, 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(), remote_answer.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); } // Test that a bundle-only offer without rtcp-mux in the bundle-only section // is accepted. TEST_F(JsepTransportControllerTest, BundleOnlySectionDoesNotNeedRtcpMux) { CreateJsepTransportController(JsepTransportController::Config()); ContentGroup bundle_group(GROUP_TYPE_BUNDLE); bundle_group.AddContentName(kAudioMid1); bundle_group.AddContentName(kVideoMid1); auto offer = std::make_unique(); AddAudioSection(offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); AddVideoSection(offer.get(), kVideoMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); offer->AddGroup(bundle_group); // Remove rtcp-mux and set bundle-only on the second content. offer->contents()[1].media_description()->set_rtcp_mux(false); offer->contents()[1].bundle_only = true; EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kOffer, nullptr, offer.get()) .ok()); } // Test that with max-bundle a single unbundled m-line is accepted. TEST_F(JsepTransportControllerTest, MaxBundleDoesNotRequireBundleForFirstMline) { auto config = JsepTransportController::Config(); config.bundle_policy = PeerConnectionInterface::kBundlePolicyMaxBundle; CreateJsepTransportController(std::move(config)); auto offer = std::make_unique(); AddAudioSection(offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kOffer, nullptr, offer.get()) .ok()); } TEST_F(JsepTransportControllerTest, SuggestPayloadTypeBasic) { auto config = JsepTransportController::Config(); CreateJsepTransportController(std::move(config)); Codec pcmu_codec = CreateAudioCodec(-1, kPcmuCodecName, 8000, 1); RTCErrorOr pcmu_pt = transport_controller_->SuggestPayloadType("mid", pcmu_codec); ASSERT_TRUE(pcmu_pt.ok()); EXPECT_EQ(pcmu_pt.value(), PayloadType(0)); } TEST_F(JsepTransportControllerTest, SuggestPayloadTypeReusesRemotePayloadType) { auto config = JsepTransportController::Config(); CreateJsepTransportController(std::move(config)); const PayloadType remote_lyra_pt(99); Codec remote_lyra_codec = CreateAudioCodec(remote_lyra_pt, "lyra", 8000, 1); auto offer = std::make_unique(); AddAudioSection(offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); offer->contents()[0].media_description()->set_codecs({remote_lyra_codec}); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kOffer, nullptr, offer.get()) .ok()); Codec local_lyra_codec = CreateAudioCodec(-1, "lyra", 8000, 1); RTCErrorOr lyra_pt = transport_controller_->SuggestPayloadType(kAudioMid1, local_lyra_codec); ASSERT_TRUE(lyra_pt.ok()); EXPECT_EQ(lyra_pt.value(), remote_lyra_pt); } TEST_F(JsepTransportControllerTest, SuggestPayloadTypeAvoidsRemoteLocalConflict) { auto config = JsepTransportController::Config(); CreateJsepTransportController(std::move(config)); // libwebrtc will normally allocate 110 to DTMF/48000 const PayloadType remote_opus_pt(110); Codec remote_opus_codec = CreateAudioCodec(remote_opus_pt, "opus", 48000, 2); auto offer = std::make_unique(); AddAudioSection(offer.get(), kAudioMid1, kIceUfrag1, kIcePwd1, ICEMODE_FULL, CONNECTIONROLE_ACTPASS, nullptr); offer->contents()[0].media_description()->set_codecs({remote_opus_codec}); EXPECT_TRUE(transport_controller_ ->SetRemoteDescription(SdpType::kOffer, nullptr, offer.get()) .ok()); // Check that we get the Opus codec back with the remote PT Codec local_opus_codec = CreateAudioCodec(-1, "opus", 48000, 2); RTCErrorOr local_opus_pt = transport_controller_->SuggestPayloadType(kAudioMid1, local_opus_codec); EXPECT_EQ(local_opus_pt.value(), remote_opus_pt); // Check that we don't get 110 allocated for DTMF, since it's in use for opus Codec local_other_codec = CreateAudioCodec(-1, kDtmfCodecName, 48000, 1); RTCErrorOr other_pt = transport_controller_->SuggestPayloadType(kAudioMid1, local_other_codec); ASSERT_TRUE(other_pt.ok()); EXPECT_NE(other_pt.value(), remote_opus_pt); } } // namespace webrtc