You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
891 lines
36 KiB
891 lines
36 KiB
/*
|
|
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
#include <memory>
|
|
|
|
#include "api/audio_codecs/builtin_audio_decoder_factory.h"
|
|
#include "api/audio_codecs/builtin_audio_encoder_factory.h"
|
|
#include "api/create_peerconnection_factory.h"
|
|
#include "api/peer_connection_proxy.h"
|
|
#include "api/video_codecs/builtin_video_decoder_factory.h"
|
|
#include "api/video_codecs/builtin_video_encoder_factory.h"
|
|
#include "p2p/base/fake_port_allocator.h"
|
|
#include "p2p/base/test_stun_server.h"
|
|
#include "p2p/client/basic_port_allocator.h"
|
|
#include "pc/media_session.h"
|
|
#include "pc/peer_connection.h"
|
|
#include "pc/peer_connection_wrapper.h"
|
|
#include "pc/sdp_utils.h"
|
|
#ifdef WEBRTC_ANDROID
|
|
#include "pc/test/android_test_initializer.h"
|
|
#endif
|
|
#include "pc/test/fake_audio_capture_module.h"
|
|
#include "rtc_base/fake_network.h"
|
|
#include "rtc_base/gunit.h"
|
|
#include "rtc_base/virtual_socket_server.h"
|
|
#include "test/gmock.h"
|
|
|
|
namespace webrtc {
|
|
|
|
using BundlePolicy = PeerConnectionInterface::BundlePolicy;
|
|
using RTCConfiguration = PeerConnectionInterface::RTCConfiguration;
|
|
using RTCOfferAnswerOptions = PeerConnectionInterface::RTCOfferAnswerOptions;
|
|
using RtcpMuxPolicy = PeerConnectionInterface::RtcpMuxPolicy;
|
|
using rtc::SocketAddress;
|
|
using ::testing::Combine;
|
|
using ::testing::ElementsAre;
|
|
using ::testing::UnorderedElementsAre;
|
|
using ::testing::Values;
|
|
|
|
constexpr int kDefaultTimeout = 10000;
|
|
|
|
// TODO(steveanton): These tests should be rewritten to use the standard
|
|
// RtpSenderInterface/DtlsTransportInterface objects once they're available in
|
|
// the API. The RtpSender can be used to determine which transport a given media
|
|
// will use: https://www.w3.org/TR/webrtc/#dom-rtcrtpsender-transport
|
|
// Should also be able to remove GetTransceiversForTesting at that point.
|
|
|
|
class FakeNetworkManagerWithNoAnyNetwork : public rtc::FakeNetworkManager {
|
|
public:
|
|
void GetAnyAddressNetworks(NetworkList* networks) override {
|
|
// This function allocates networks that are owned by the
|
|
// NetworkManager. But some tests assume that they can release
|
|
// all networks independent of the network manager.
|
|
// In order to prevent use-after-free issues, don't allow this
|
|
// function to have any effect when run in tests.
|
|
RTC_LOG(LS_INFO) << "FakeNetworkManager::GetAnyAddressNetworks ignored";
|
|
}
|
|
};
|
|
|
|
class PeerConnectionWrapperForBundleTest : public PeerConnectionWrapper {
|
|
public:
|
|
using PeerConnectionWrapper::PeerConnectionWrapper;
|
|
|
|
bool AddIceCandidateToMedia(cricket::Candidate* candidate,
|
|
cricket::MediaType media_type) {
|
|
auto* desc = pc()->remote_description()->description();
|
|
for (size_t i = 0; i < desc->contents().size(); i++) {
|
|
const auto& content = desc->contents()[i];
|
|
if (content.media_description()->type() == media_type) {
|
|
candidate->set_transport_name(content.name);
|
|
std::unique_ptr<IceCandidateInterface> jsep_candidate =
|
|
CreateIceCandidate(content.name, i, *candidate);
|
|
return pc()->AddIceCandidate(jsep_candidate.get());
|
|
}
|
|
}
|
|
RTC_NOTREACHED();
|
|
return false;
|
|
}
|
|
|
|
RtpTransportInternal* voice_rtp_transport() {
|
|
return (voice_channel() ? voice_channel()->rtp_transport() : nullptr);
|
|
}
|
|
|
|
cricket::VoiceChannel* voice_channel() {
|
|
auto transceivers = GetInternalPeerConnection()->GetTransceiversInternal();
|
|
for (const auto& transceiver : transceivers) {
|
|
if (transceiver->media_type() == cricket::MEDIA_TYPE_AUDIO) {
|
|
return static_cast<cricket::VoiceChannel*>(
|
|
transceiver->internal()->channel());
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
RtpTransportInternal* video_rtp_transport() {
|
|
return (video_channel() ? video_channel()->rtp_transport() : nullptr);
|
|
}
|
|
|
|
cricket::VideoChannel* video_channel() {
|
|
auto transceivers = GetInternalPeerConnection()->GetTransceiversInternal();
|
|
for (const auto& transceiver : transceivers) {
|
|
if (transceiver->media_type() == cricket::MEDIA_TYPE_VIDEO) {
|
|
return static_cast<cricket::VideoChannel*>(
|
|
transceiver->internal()->channel());
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
PeerConnection* GetInternalPeerConnection() {
|
|
auto* pci =
|
|
static_cast<PeerConnectionProxyWithInternal<PeerConnectionInterface>*>(
|
|
pc());
|
|
return static_cast<PeerConnection*>(pci->internal());
|
|
}
|
|
|
|
// Returns true if the stats indicate that an ICE connection is either in
|
|
// progress or established with the given remote address.
|
|
bool HasConnectionWithRemoteAddress(const SocketAddress& address) {
|
|
auto report = GetStats();
|
|
if (!report) {
|
|
return false;
|
|
}
|
|
std::string matching_candidate_id;
|
|
for (auto* ice_candidate_stats :
|
|
report->GetStatsOfType<RTCRemoteIceCandidateStats>()) {
|
|
if (*ice_candidate_stats->ip == address.HostAsURIString() &&
|
|
*ice_candidate_stats->port == address.port()) {
|
|
matching_candidate_id = ice_candidate_stats->id();
|
|
break;
|
|
}
|
|
}
|
|
if (matching_candidate_id.empty()) {
|
|
return false;
|
|
}
|
|
for (auto* pair_stats :
|
|
report->GetStatsOfType<RTCIceCandidatePairStats>()) {
|
|
if (*pair_stats->remote_candidate_id == matching_candidate_id) {
|
|
if (*pair_stats->state == RTCStatsIceCandidatePairState::kInProgress ||
|
|
*pair_stats->state == RTCStatsIceCandidatePairState::kSucceeded) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
rtc::FakeNetworkManager* network() { return network_; }
|
|
|
|
void set_network(rtc::FakeNetworkManager* network) { network_ = network; }
|
|
|
|
private:
|
|
rtc::FakeNetworkManager* network_;
|
|
};
|
|
|
|
class PeerConnectionBundleBaseTest : public ::testing::Test {
|
|
protected:
|
|
typedef std::unique_ptr<PeerConnectionWrapperForBundleTest> WrapperPtr;
|
|
|
|
explicit PeerConnectionBundleBaseTest(SdpSemantics sdp_semantics)
|
|
: vss_(new rtc::VirtualSocketServer()),
|
|
main_(vss_.get()),
|
|
sdp_semantics_(sdp_semantics) {
|
|
#ifdef WEBRTC_ANDROID
|
|
InitializeAndroidObjects();
|
|
#endif
|
|
pc_factory_ = CreatePeerConnectionFactory(
|
|
rtc::Thread::Current(), rtc::Thread::Current(), rtc::Thread::Current(),
|
|
rtc::scoped_refptr<AudioDeviceModule>(FakeAudioCaptureModule::Create()),
|
|
CreateBuiltinAudioEncoderFactory(), CreateBuiltinAudioDecoderFactory(),
|
|
CreateBuiltinVideoEncoderFactory(), CreateBuiltinVideoDecoderFactory(),
|
|
nullptr /* audio_mixer */, nullptr /* audio_processing */);
|
|
}
|
|
|
|
WrapperPtr CreatePeerConnection() {
|
|
return CreatePeerConnection(RTCConfiguration());
|
|
}
|
|
|
|
WrapperPtr CreatePeerConnection(const RTCConfiguration& config) {
|
|
auto* fake_network = NewFakeNetwork();
|
|
auto port_allocator =
|
|
std::make_unique<cricket::BasicPortAllocator>(fake_network);
|
|
port_allocator->set_flags(cricket::PORTALLOCATOR_DISABLE_TCP |
|
|
cricket::PORTALLOCATOR_DISABLE_RELAY);
|
|
port_allocator->set_step_delay(cricket::kMinimumStepDelay);
|
|
auto observer = std::make_unique<MockPeerConnectionObserver>();
|
|
RTCConfiguration modified_config = config;
|
|
modified_config.sdp_semantics = sdp_semantics_;
|
|
auto pc = pc_factory_->CreatePeerConnection(
|
|
modified_config, std::move(port_allocator), nullptr, observer.get());
|
|
if (!pc) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto wrapper = std::make_unique<PeerConnectionWrapperForBundleTest>(
|
|
pc_factory_, pc, std::move(observer));
|
|
wrapper->set_network(fake_network);
|
|
return wrapper;
|
|
}
|
|
|
|
// Accepts the same arguments as CreatePeerConnection and adds default audio
|
|
// and video tracks.
|
|
template <typename... Args>
|
|
WrapperPtr CreatePeerConnectionWithAudioVideo(Args&&... args) {
|
|
auto wrapper = CreatePeerConnection(std::forward<Args>(args)...);
|
|
if (!wrapper) {
|
|
return nullptr;
|
|
}
|
|
wrapper->AddAudioTrack("a");
|
|
wrapper->AddVideoTrack("v");
|
|
return wrapper;
|
|
}
|
|
|
|
cricket::Candidate CreateLocalUdpCandidate(
|
|
const rtc::SocketAddress& address) {
|
|
cricket::Candidate candidate;
|
|
candidate.set_component(cricket::ICE_CANDIDATE_COMPONENT_DEFAULT);
|
|
candidate.set_protocol(cricket::UDP_PROTOCOL_NAME);
|
|
candidate.set_address(address);
|
|
candidate.set_type(cricket::LOCAL_PORT_TYPE);
|
|
return candidate;
|
|
}
|
|
|
|
rtc::FakeNetworkManager* NewFakeNetwork() {
|
|
// The PeerConnection's port allocator is tied to the PeerConnection's
|
|
// lifetime and expects the underlying NetworkManager to outlive it. If
|
|
// PeerConnectionWrapper owned the NetworkManager, it would be destroyed
|
|
// before the PeerConnection (since subclass members are destroyed before
|
|
// base class members). Therefore, the test fixture will own all the fake
|
|
// networks even though tests should access the fake network through the
|
|
// PeerConnectionWrapper.
|
|
auto* fake_network = new FakeNetworkManagerWithNoAnyNetwork();
|
|
fake_networks_.emplace_back(fake_network);
|
|
return fake_network;
|
|
}
|
|
|
|
std::unique_ptr<rtc::VirtualSocketServer> vss_;
|
|
rtc::AutoSocketServerThread main_;
|
|
rtc::scoped_refptr<PeerConnectionFactoryInterface> pc_factory_;
|
|
std::vector<std::unique_ptr<rtc::FakeNetworkManager>> fake_networks_;
|
|
const SdpSemantics sdp_semantics_;
|
|
};
|
|
|
|
class PeerConnectionBundleTest
|
|
: public PeerConnectionBundleBaseTest,
|
|
public ::testing::WithParamInterface<SdpSemantics> {
|
|
protected:
|
|
PeerConnectionBundleTest() : PeerConnectionBundleBaseTest(GetParam()) {}
|
|
};
|
|
|
|
class PeerConnectionBundleTestUnifiedPlan
|
|
: public PeerConnectionBundleBaseTest {
|
|
protected:
|
|
PeerConnectionBundleTestUnifiedPlan()
|
|
: PeerConnectionBundleBaseTest(SdpSemantics::kUnifiedPlan) {}
|
|
};
|
|
|
|
SdpContentMutator RemoveRtcpMux() {
|
|
return [](cricket::ContentInfo* content, cricket::TransportInfo* transport) {
|
|
content->media_description()->set_rtcp_mux(false);
|
|
};
|
|
}
|
|
|
|
std::vector<int> GetCandidateComponents(
|
|
const std::vector<IceCandidateInterface*> candidates) {
|
|
std::vector<int> components;
|
|
components.reserve(candidates.size());
|
|
for (auto* candidate : candidates) {
|
|
components.push_back(candidate->candidate().component());
|
|
}
|
|
return components;
|
|
}
|
|
|
|
// Test that there are 2 local UDP candidates (1 RTP and 1 RTCP candidate) for
|
|
// each media section when disabling bundling and disabling RTCP multiplexing.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
TwoCandidatesForEachTransportWhenNoBundleNoRtcpMux) {
|
|
const SocketAddress kCallerAddress("1.1.1.1", 0);
|
|
const SocketAddress kCalleeAddress("2.2.2.2", 0);
|
|
|
|
RTCConfiguration config;
|
|
config.rtcp_mux_policy = PeerConnectionInterface::kRtcpMuxPolicyNegotiate;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
caller->network()->AddInterface(kCallerAddress);
|
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
|
callee->network()->AddInterface(kCalleeAddress);
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
RTCOfferAnswerOptions options_no_bundle;
|
|
options_no_bundle.use_rtp_mux = false;
|
|
auto answer = callee->CreateAnswer(options_no_bundle);
|
|
SdpContentsForEach(RemoveRtcpMux(), answer->description());
|
|
ASSERT_TRUE(
|
|
callee->SetLocalDescription(CloneSessionDescription(answer.get())));
|
|
ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
|
|
|
|
// Check that caller has separate RTP and RTCP candidates for each media.
|
|
EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout);
|
|
EXPECT_THAT(
|
|
GetCandidateComponents(caller->observer()->GetCandidatesByMline(0)),
|
|
UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
|
|
cricket::ICE_CANDIDATE_COMPONENT_RTCP));
|
|
EXPECT_THAT(
|
|
GetCandidateComponents(caller->observer()->GetCandidatesByMline(1)),
|
|
UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
|
|
cricket::ICE_CANDIDATE_COMPONENT_RTCP));
|
|
|
|
// Check that callee has separate RTP and RTCP candidates for each media.
|
|
EXPECT_TRUE_WAIT(callee->IsIceGatheringDone(), kDefaultTimeout);
|
|
EXPECT_THAT(
|
|
GetCandidateComponents(callee->observer()->GetCandidatesByMline(0)),
|
|
UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
|
|
cricket::ICE_CANDIDATE_COMPONENT_RTCP));
|
|
EXPECT_THAT(
|
|
GetCandidateComponents(callee->observer()->GetCandidatesByMline(1)),
|
|
UnorderedElementsAre(cricket::ICE_CANDIDATE_COMPONENT_RTP,
|
|
cricket::ICE_CANDIDATE_COMPONENT_RTCP));
|
|
}
|
|
|
|
// Test that there is 1 local UDP candidate for both RTP and RTCP for each media
|
|
// section when disabling bundle but enabling RTCP multiplexing.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
OneCandidateForEachTransportWhenNoBundleButRtcpMux) {
|
|
const SocketAddress kCallerAddress("1.1.1.1", 0);
|
|
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
caller->network()->AddInterface(kCallerAddress);
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
RTCOfferAnswerOptions options_no_bundle;
|
|
options_no_bundle.use_rtp_mux = false;
|
|
ASSERT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswer(options_no_bundle)));
|
|
|
|
EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout);
|
|
|
|
EXPECT_EQ(1u, caller->observer()->GetCandidatesByMline(0).size());
|
|
EXPECT_EQ(1u, caller->observer()->GetCandidatesByMline(1).size());
|
|
}
|
|
|
|
// Test that there is 1 local UDP candidate in only the first media section when
|
|
// bundling and enabling RTCP multiplexing.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
OneCandidateOnlyOnFirstTransportWhenBundleAndRtcpMux) {
|
|
const SocketAddress kCallerAddress("1.1.1.1", 0);
|
|
|
|
RTCConfiguration config;
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
caller->network()->AddInterface(kCallerAddress);
|
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
ASSERT_TRUE(caller->SetRemoteDescription(callee->CreateAnswer()));
|
|
|
|
EXPECT_TRUE_WAIT(caller->IsIceGatheringDone(), kDefaultTimeout);
|
|
|
|
EXPECT_EQ(1u, caller->observer()->GetCandidatesByMline(0).size());
|
|
EXPECT_EQ(0u, caller->observer()->GetCandidatesByMline(1).size());
|
|
}
|
|
|
|
// It will fail if the offerer uses the mux-BUNDLE policy but the answerer
|
|
// doesn't support BUNDLE.
|
|
TEST_P(PeerConnectionBundleTest, MaxBundleNotSupportedInAnswer) {
|
|
RTCConfiguration config;
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
bool equal_before =
|
|
(caller->voice_rtp_transport() == caller->video_rtp_transport());
|
|
EXPECT_EQ(true, equal_before);
|
|
RTCOfferAnswerOptions options;
|
|
options.use_rtp_mux = false;
|
|
EXPECT_FALSE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal(options)));
|
|
}
|
|
|
|
// The following parameterized test verifies that an offer/answer with varying
|
|
// bundle policies and either bundle in the answer or not will produce the
|
|
// expected RTP transports for audio and video. In particular, for bundling we
|
|
// care about whether they are separate transports or the same.
|
|
|
|
enum class BundleIncluded { kBundleInAnswer, kBundleNotInAnswer };
|
|
std::ostream& operator<<(std::ostream& out, BundleIncluded value) {
|
|
switch (value) {
|
|
case BundleIncluded::kBundleInAnswer:
|
|
return out << "bundle in answer";
|
|
case BundleIncluded::kBundleNotInAnswer:
|
|
return out << "bundle not in answer";
|
|
}
|
|
return out << "unknown";
|
|
}
|
|
|
|
class PeerConnectionBundleMatrixTest
|
|
: public PeerConnectionBundleBaseTest,
|
|
public ::testing::WithParamInterface<
|
|
std::tuple<SdpSemantics,
|
|
std::tuple<BundlePolicy, BundleIncluded, bool, bool>>> {
|
|
protected:
|
|
PeerConnectionBundleMatrixTest()
|
|
: PeerConnectionBundleBaseTest(std::get<0>(GetParam())) {
|
|
auto param = std::get<1>(GetParam());
|
|
bundle_policy_ = std::get<0>(param);
|
|
bundle_included_ = std::get<1>(param);
|
|
expected_same_before_ = std::get<2>(param);
|
|
expected_same_after_ = std::get<3>(param);
|
|
}
|
|
|
|
PeerConnectionInterface::BundlePolicy bundle_policy_;
|
|
BundleIncluded bundle_included_;
|
|
bool expected_same_before_;
|
|
bool expected_same_after_;
|
|
};
|
|
|
|
TEST_P(PeerConnectionBundleMatrixTest,
|
|
VerifyTransportsBeforeAndAfterSettingRemoteAnswer) {
|
|
RTCConfiguration config;
|
|
config.bundle_policy = bundle_policy_;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
bool equal_before =
|
|
(caller->voice_rtp_transport() == caller->video_rtp_transport());
|
|
EXPECT_EQ(expected_same_before_, equal_before);
|
|
|
|
RTCOfferAnswerOptions options;
|
|
options.use_rtp_mux = (bundle_included_ == BundleIncluded::kBundleInAnswer);
|
|
ASSERT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal(options)));
|
|
bool equal_after =
|
|
(caller->voice_rtp_transport() == caller->video_rtp_transport());
|
|
EXPECT_EQ(expected_same_after_, equal_after);
|
|
}
|
|
|
|
// The max-bundle policy means we should anticipate bundling being negotiated,
|
|
// and multiplex audio/video from the start.
|
|
// For all other policies, bundling should only be enabled if negotiated by the
|
|
// answer.
|
|
INSTANTIATE_TEST_SUITE_P(
|
|
PeerConnectionBundleTest,
|
|
PeerConnectionBundleMatrixTest,
|
|
Combine(Values(SdpSemantics::kPlanB, SdpSemantics::kUnifiedPlan),
|
|
Values(std::make_tuple(BundlePolicy::kBundlePolicyBalanced,
|
|
BundleIncluded::kBundleInAnswer,
|
|
false,
|
|
true),
|
|
std::make_tuple(BundlePolicy::kBundlePolicyBalanced,
|
|
BundleIncluded::kBundleNotInAnswer,
|
|
false,
|
|
false),
|
|
std::make_tuple(BundlePolicy::kBundlePolicyMaxBundle,
|
|
BundleIncluded::kBundleInAnswer,
|
|
true,
|
|
true),
|
|
std::make_tuple(BundlePolicy::kBundlePolicyMaxCompat,
|
|
BundleIncluded::kBundleInAnswer,
|
|
false,
|
|
true),
|
|
std::make_tuple(BundlePolicy::kBundlePolicyMaxCompat,
|
|
BundleIncluded::kBundleNotInAnswer,
|
|
false,
|
|
false))));
|
|
|
|
// Test that the audio/video transports on the callee side are the same before
|
|
// and after setting a local answer when max BUNDLE is enabled and an offer with
|
|
// BUNDLE is received.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
TransportsSameForMaxBundleWithBundleInRemoteOffer) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
RTCConfiguration config;
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
|
|
|
RTCOfferAnswerOptions options_with_bundle;
|
|
options_with_bundle.use_rtp_mux = true;
|
|
ASSERT_TRUE(callee->SetRemoteDescription(
|
|
caller->CreateOfferAndSetAsLocal(options_with_bundle)));
|
|
|
|
EXPECT_EQ(callee->voice_rtp_transport(), callee->video_rtp_transport());
|
|
|
|
ASSERT_TRUE(callee->SetLocalDescription(callee->CreateAnswer()));
|
|
|
|
EXPECT_EQ(callee->voice_rtp_transport(), callee->video_rtp_transport());
|
|
}
|
|
|
|
TEST_P(PeerConnectionBundleTest,
|
|
FailToSetRemoteOfferWithNoBundleWhenBundlePolicyMaxBundle) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
RTCConfiguration config;
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
|
|
|
RTCOfferAnswerOptions options_no_bundle;
|
|
options_no_bundle.use_rtp_mux = false;
|
|
EXPECT_FALSE(callee->SetRemoteDescription(
|
|
caller->CreateOfferAndSetAsLocal(options_no_bundle)));
|
|
}
|
|
|
|
// Test that if the media section which has the bundled transport is rejected,
|
|
// then the peers still connect and the bundled transport switches to the other
|
|
// media section.
|
|
// Note: This is currently failing because of the following bug:
|
|
// https://bugs.chromium.org/p/webrtc/issues/detail?id=6280
|
|
TEST_P(PeerConnectionBundleTest,
|
|
DISABLED_SuccessfullyNegotiateMaxBundleIfBundleTransportMediaRejected) {
|
|
RTCConfiguration config;
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
auto callee = CreatePeerConnection();
|
|
callee->AddVideoTrack("v");
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
|
|
RTCOfferAnswerOptions options;
|
|
options.offer_to_receive_audio = 0;
|
|
ASSERT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal(options)));
|
|
|
|
EXPECT_FALSE(caller->voice_rtp_transport());
|
|
EXPECT_TRUE(caller->video_rtp_transport());
|
|
}
|
|
|
|
// When requiring RTCP multiplexing, the PeerConnection never makes RTCP
|
|
// transport channels.
|
|
TEST_P(PeerConnectionBundleTest, NeverCreateRtcpTransportWithRtcpMuxRequired) {
|
|
RTCConfiguration config;
|
|
config.rtcp_mux_policy = RtcpMuxPolicy::kRtcpMuxPolicyRequire;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
|
|
EXPECT_FALSE(caller->voice_rtp_transport()->rtcp_mux_enabled());
|
|
EXPECT_FALSE(caller->video_rtp_transport()->rtcp_mux_enabled());
|
|
|
|
ASSERT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
|
|
|
|
EXPECT_TRUE(caller->voice_rtp_transport()->rtcp_mux_enabled());
|
|
EXPECT_TRUE(caller->video_rtp_transport()->rtcp_mux_enabled());
|
|
}
|
|
|
|
// When negotiating RTCP multiplexing, the PeerConnection makes RTCP transports
|
|
// when the offer is sent, but will destroy them once the remote answer is set.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
CreateRtcpTransportOnlyBeforeAnswerWithRtcpMuxNegotiate) {
|
|
RTCConfiguration config;
|
|
config.rtcp_mux_policy = RtcpMuxPolicy::kRtcpMuxPolicyNegotiate;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
|
|
EXPECT_FALSE(caller->voice_rtp_transport()->rtcp_mux_enabled());
|
|
EXPECT_FALSE(caller->video_rtp_transport()->rtcp_mux_enabled());
|
|
|
|
ASSERT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
|
|
|
|
EXPECT_TRUE(caller->voice_rtp_transport()->rtcp_mux_enabled());
|
|
EXPECT_TRUE(caller->video_rtp_transport()->rtcp_mux_enabled());
|
|
}
|
|
|
|
TEST_P(PeerConnectionBundleTest, FailToSetDescriptionWithBundleAndNoRtcpMux) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
RTCOfferAnswerOptions options;
|
|
options.use_rtp_mux = true;
|
|
|
|
auto offer = caller->CreateOffer(options);
|
|
SdpContentsForEach(RemoveRtcpMux(), offer->description());
|
|
|
|
std::string error;
|
|
EXPECT_FALSE(caller->SetLocalDescription(CloneSessionDescription(offer.get()),
|
|
&error));
|
|
EXPECT_EQ(
|
|
"Failed to set local offer sdp: rtcp-mux must be enabled when BUNDLE is "
|
|
"enabled.",
|
|
error);
|
|
|
|
EXPECT_FALSE(callee->SetRemoteDescription(std::move(offer), &error));
|
|
EXPECT_EQ(
|
|
"Failed to set remote offer sdp: rtcp-mux must be enabled when BUNDLE is "
|
|
"enabled.",
|
|
error);
|
|
}
|
|
|
|
// Test that candidates sent to the "video" transport do not get pushed down to
|
|
// the "audio" transport channel when bundling.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
IgnoreCandidatesForUnusedTransportWhenBundling) {
|
|
const SocketAddress kAudioAddress1("1.1.1.1", 1111);
|
|
const SocketAddress kAudioAddress2("2.2.2.2", 2222);
|
|
const SocketAddress kVideoAddress("3.3.3.3", 3333);
|
|
const SocketAddress kCallerAddress("4.4.4.4", 0);
|
|
const SocketAddress kCalleeAddress("5.5.5.5", 0);
|
|
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
caller->network()->AddInterface(kCallerAddress);
|
|
callee->network()->AddInterface(kCalleeAddress);
|
|
|
|
RTCOfferAnswerOptions options;
|
|
options.use_rtp_mux = true;
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
ASSERT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal(options)));
|
|
|
|
// The way the *_WAIT checks work is they only wait if the condition fails,
|
|
// which does not help in the case where state is not changing. This is
|
|
// problematic in this test since we want to verify that adding a video
|
|
// candidate does _not_ change state. So we interleave candidates and assume
|
|
// that messages are executed in the order they were posted.
|
|
|
|
cricket::Candidate audio_candidate1 = CreateLocalUdpCandidate(kAudioAddress1);
|
|
ASSERT_TRUE(caller->AddIceCandidateToMedia(&audio_candidate1,
|
|
cricket::MEDIA_TYPE_AUDIO));
|
|
|
|
cricket::Candidate video_candidate = CreateLocalUdpCandidate(kVideoAddress);
|
|
ASSERT_TRUE(caller->AddIceCandidateToMedia(&video_candidate,
|
|
cricket::MEDIA_TYPE_VIDEO));
|
|
|
|
cricket::Candidate audio_candidate2 = CreateLocalUdpCandidate(kAudioAddress2);
|
|
ASSERT_TRUE(caller->AddIceCandidateToMedia(&audio_candidate2,
|
|
cricket::MEDIA_TYPE_AUDIO));
|
|
|
|
EXPECT_TRUE_WAIT(caller->HasConnectionWithRemoteAddress(kAudioAddress1),
|
|
kDefaultTimeout);
|
|
EXPECT_TRUE_WAIT(caller->HasConnectionWithRemoteAddress(kAudioAddress2),
|
|
kDefaultTimeout);
|
|
EXPECT_FALSE(caller->HasConnectionWithRemoteAddress(kVideoAddress));
|
|
}
|
|
|
|
// Test that the transport used by both audio and video is the transport
|
|
// associated with the first MID in the answer BUNDLE group, even if it's in a
|
|
// different order from the offer.
|
|
TEST_P(PeerConnectionBundleTest, BundleOnFirstMidInAnswer) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
ASSERT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
|
|
auto* old_video_transport = caller->video_rtp_transport();
|
|
|
|
auto answer = callee->CreateAnswer();
|
|
auto* old_bundle_group =
|
|
answer->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
std::string first_mid = old_bundle_group->content_names()[0];
|
|
std::string second_mid = old_bundle_group->content_names()[1];
|
|
answer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
|
|
cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
|
|
new_bundle_group.AddContentName(second_mid);
|
|
new_bundle_group.AddContentName(first_mid);
|
|
answer->description()->AddGroup(new_bundle_group);
|
|
|
|
ASSERT_TRUE(caller->SetRemoteDescription(std::move(answer)));
|
|
|
|
EXPECT_EQ(old_video_transport, caller->video_rtp_transport());
|
|
EXPECT_EQ(caller->voice_rtp_transport(), caller->video_rtp_transport());
|
|
}
|
|
|
|
// This tests that applying description with conflicted RTP demuxing criteria
|
|
// will fail.
|
|
TEST_P(PeerConnectionBundleTest,
|
|
ApplyDescriptionWithConflictedDemuxCriteriaFail) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
RTCOfferAnswerOptions options;
|
|
options.use_rtp_mux = false;
|
|
auto offer = caller->CreateOffer(options);
|
|
// Modified the SDP to make two m= sections have the same SSRC.
|
|
ASSERT_GE(offer->description()->contents().size(), 2U);
|
|
offer->description()
|
|
->contents()[0]
|
|
.media_description()
|
|
->mutable_streams()[0]
|
|
.ssrcs[0] = 1111222;
|
|
offer->description()
|
|
->contents()[1]
|
|
.media_description()
|
|
->mutable_streams()[0]
|
|
.ssrcs[0] = 1111222;
|
|
EXPECT_TRUE(
|
|
caller->SetLocalDescription(CloneSessionDescription(offer.get())));
|
|
EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
|
|
EXPECT_TRUE(callee->CreateAnswerAndSetAsLocal(options));
|
|
|
|
// Enable BUNDLE in subsequent offer/answer exchange and two m= sections are
|
|
// expectd to use one RtpTransport underneath.
|
|
options.use_rtp_mux = true;
|
|
EXPECT_TRUE(
|
|
callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal(options)));
|
|
auto answer = callee->CreateAnswer(options);
|
|
// When BUNDLE is enabled, applying the description is expected to fail
|
|
// because the demuxing criteria is conflicted.
|
|
EXPECT_FALSE(callee->SetLocalDescription(std::move(answer)));
|
|
}
|
|
|
|
// This tests that changing the pre-negotiated BUNDLE tag is not supported.
|
|
TEST_P(PeerConnectionBundleTest, RejectDescriptionChangingBundleTag) {
|
|
RTCConfiguration config;
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
auto callee = CreatePeerConnectionWithAudioVideo(config);
|
|
|
|
RTCOfferAnswerOptions options;
|
|
options.use_rtp_mux = true;
|
|
auto offer = caller->CreateOfferAndSetAsLocal(options);
|
|
|
|
// Create a new bundle-group with different bundled_mid.
|
|
auto* old_bundle_group =
|
|
offer->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
std::string first_mid = old_bundle_group->content_names()[0];
|
|
std::string second_mid = old_bundle_group->content_names()[1];
|
|
cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
|
|
new_bundle_group.AddContentName(second_mid);
|
|
|
|
auto re_offer = CloneSessionDescription(offer.get());
|
|
callee->SetRemoteDescription(std::move(offer));
|
|
auto answer = callee->CreateAnswer(options);
|
|
// Reject the first MID.
|
|
answer->description()->contents()[0].rejected = true;
|
|
// Remove the first MID from the bundle group.
|
|
answer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
answer->description()->AddGroup(new_bundle_group);
|
|
// The answer is expected to be rejected.
|
|
EXPECT_FALSE(caller->SetRemoteDescription(std::move(answer)));
|
|
|
|
// Do the same thing for re-offer.
|
|
re_offer->description()->contents()[0].rejected = true;
|
|
re_offer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
re_offer->description()->AddGroup(new_bundle_group);
|
|
// The re-offer is expected to be rejected.
|
|
EXPECT_FALSE(caller->SetLocalDescription(std::move(re_offer)));
|
|
}
|
|
|
|
// This tests that removing contents from BUNDLE group and reject the whole
|
|
// BUNDLE group could work. This is a regression test for
|
|
// (https://bugs.chromium.org/p/chromium/issues/detail?id=827917)
|
|
TEST_P(PeerConnectionBundleTest, RemovingContentAndRejectBundleGroup) {
|
|
RTCConfiguration config;
|
|
#ifndef HAVE_SCTP
|
|
config.enable_rtp_data_channel = true;
|
|
#endif
|
|
config.bundle_policy = BundlePolicy::kBundlePolicyMaxBundle;
|
|
auto caller = CreatePeerConnectionWithAudioVideo(config);
|
|
caller->CreateDataChannel("dc");
|
|
|
|
auto offer = caller->CreateOfferAndSetAsLocal();
|
|
auto re_offer = CloneSessionDescription(offer.get());
|
|
|
|
// Removing the second MID from the BUNDLE group.
|
|
auto* old_bundle_group =
|
|
offer->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
std::string first_mid = old_bundle_group->content_names()[0];
|
|
std::string third_mid = old_bundle_group->content_names()[2];
|
|
cricket::ContentGroup new_bundle_group(cricket::GROUP_TYPE_BUNDLE);
|
|
new_bundle_group.AddContentName(first_mid);
|
|
new_bundle_group.AddContentName(third_mid);
|
|
|
|
// Reject the entire new bundle group.
|
|
re_offer->description()->contents()[0].rejected = true;
|
|
re_offer->description()->contents()[2].rejected = true;
|
|
re_offer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
re_offer->description()->AddGroup(new_bundle_group);
|
|
|
|
EXPECT_TRUE(caller->SetLocalDescription(std::move(re_offer)));
|
|
}
|
|
|
|
// This tests that the BUNDLE group in answer should be a subset of the offered
|
|
// group.
|
|
TEST_P(PeerConnectionBundleTest, AddContentToBundleGroupInAnswerNotSupported) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
auto offer = caller->CreateOffer();
|
|
std::string first_mid = offer->description()->contents()[0].name;
|
|
std::string second_mid = offer->description()->contents()[1].name;
|
|
|
|
cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
|
|
bundle_group.AddContentName(first_mid);
|
|
offer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
offer->description()->AddGroup(bundle_group);
|
|
EXPECT_TRUE(
|
|
caller->SetLocalDescription(CloneSessionDescription(offer.get())));
|
|
EXPECT_TRUE(callee->SetRemoteDescription(std::move(offer)));
|
|
|
|
auto answer = callee->CreateAnswer();
|
|
bundle_group.AddContentName(second_mid);
|
|
answer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
answer->description()->AddGroup(bundle_group);
|
|
|
|
// The answer is expected to be rejected because second mid is not in the
|
|
// offered BUNDLE group.
|
|
EXPECT_FALSE(callee->SetLocalDescription(std::move(answer)));
|
|
}
|
|
|
|
// This tests that the BUNDLE group with non-existing MID should be rejectd.
|
|
TEST_P(PeerConnectionBundleTest, RejectBundleGroupWithNonExistingMid) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
auto offer = caller->CreateOffer();
|
|
auto invalid_bundle_group =
|
|
*offer->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
invalid_bundle_group.AddContentName("non-existing-MID");
|
|
offer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
offer->description()->AddGroup(invalid_bundle_group);
|
|
|
|
EXPECT_FALSE(
|
|
caller->SetLocalDescription(CloneSessionDescription(offer.get())));
|
|
EXPECT_FALSE(callee->SetRemoteDescription(std::move(offer)));
|
|
}
|
|
|
|
// This tests that an answer shouldn't be able to remove an m= section from an
|
|
// established group without rejecting it.
|
|
TEST_P(PeerConnectionBundleTest, RemoveContentFromBundleGroup) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnectionWithAudioVideo();
|
|
|
|
EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
EXPECT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
|
|
|
|
EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
auto answer = callee->CreateAnswer();
|
|
std::string second_mid = answer->description()->contents()[1].name;
|
|
|
|
auto invalid_bundle_group =
|
|
*answer->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
invalid_bundle_group.RemoveContentName(second_mid);
|
|
answer->description()->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
answer->description()->AddGroup(invalid_bundle_group);
|
|
|
|
EXPECT_FALSE(
|
|
callee->SetLocalDescription(CloneSessionDescription(answer.get())));
|
|
}
|
|
|
|
INSTANTIATE_TEST_SUITE_P(PeerConnectionBundleTest,
|
|
PeerConnectionBundleTest,
|
|
Values(SdpSemantics::kPlanB,
|
|
SdpSemantics::kUnifiedPlan));
|
|
|
|
// According to RFC5888, if an endpoint understands the semantics of an
|
|
// "a=group", it MUST return an answer with that group. So, an empty BUNDLE
|
|
// group is valid when the answerer rejects all m= sections (by stopping all
|
|
// transceivers), meaning there's nothing to bundle.
|
|
//
|
|
// Only writing this test for Unified Plan mode, since there's no way to reject
|
|
// m= sections in answers for Plan B without SDP munging.
|
|
TEST_F(PeerConnectionBundleTestUnifiedPlan,
|
|
EmptyBundleGroupCreatedInAnswerWhenAppropriate) {
|
|
auto caller = CreatePeerConnectionWithAudioVideo();
|
|
auto callee = CreatePeerConnection();
|
|
|
|
EXPECT_TRUE(callee->SetRemoteDescription(caller->CreateOfferAndSetAsLocal()));
|
|
|
|
// Stop all transceivers, causing all m= sections to be rejected.
|
|
for (const auto& transceiver : callee->pc()->GetTransceivers()) {
|
|
transceiver->Stop();
|
|
}
|
|
EXPECT_TRUE(
|
|
caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal()));
|
|
|
|
// Verify that the answer actually contained an empty bundle group.
|
|
const SessionDescriptionInterface* desc = callee->pc()->local_description();
|
|
ASSERT_NE(nullptr, desc);
|
|
const cricket::ContentGroup* bundle_group =
|
|
desc->description()->GetGroupByName(cricket::GROUP_TYPE_BUNDLE);
|
|
ASSERT_NE(nullptr, bundle_group);
|
|
EXPECT_TRUE(bundle_group->content_names().empty());
|
|
}
|
|
|
|
} // namespace webrtc
|