| /* | 
 |  *  Copyright 2017 The WebRTC project authors. All Rights Reserved. | 
 |  * | 
 |  *  Use of this source code is governed by a BSD-style license | 
 |  *  that can be found in the LICENSE file in the root of the source | 
 |  *  tree. An additional intellectual property rights grant can be found | 
 |  *  in the file PATENTS.  All contributing project authors may | 
 |  *  be found in the AUTHORS file in the root of the source tree. | 
 |  */ | 
 |  | 
 | #include "api/audio_codecs/builtin_audio_decoder_factory.h" | 
 | #include "api/audio_codecs/builtin_audio_encoder_factory.h" | 
 | #include "api/peerconnectionproxy.h" | 
 | #include "api/video_codecs/builtin_video_decoder_factory.h" | 
 | #include "api/video_codecs/builtin_video_encoder_factory.h" | 
 | #include "p2p/base/fakeportallocator.h" | 
 | #include "p2p/base/teststunserver.h" | 
 | #include "p2p/client/basicportallocator.h" | 
 | #include "pc/mediasession.h" | 
 | #include "pc/peerconnection.h" | 
 | #include "pc/peerconnectionwrapper.h" | 
 | #include "pc/sdputils.h" | 
 | #ifdef WEBRTC_ANDROID | 
 | #include "pc/test/androidtestinitializer.h" | 
 | #endif | 
 | #include "absl/memory/memory.h" | 
 | #include "pc/test/fakeaudiocapturemodule.h" | 
 | #include "rtc_base/fakenetwork.h" | 
 | #include "rtc_base/gunit.h" | 
 | #include "rtc_base/virtualsocketserver.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 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; | 
 |   } | 
 |  | 
 |   rtc::PacketTransportInternal* voice_rtp_transport() { | 
 |     return (voice_channel() ? voice_channel()->rtp_packet_transport() | 
 |                             : nullptr); | 
 |   } | 
 |  | 
 |   rtc::PacketTransportInternal* voice_rtcp_transport() { | 
 |     return (voice_channel() ? voice_channel()->rtcp_packet_transport() | 
 |                             : nullptr); | 
 |   } | 
 |  | 
 |   cricket::VoiceChannel* voice_channel() { | 
 |     auto transceivers = GetInternalPeerConnection()->GetTransceiversInternal(); | 
 |     for (auto transceiver : transceivers) { | 
 |       if (transceiver->media_type() == cricket::MEDIA_TYPE_AUDIO) { | 
 |         return static_cast<cricket::VoiceChannel*>( | 
 |             transceiver->internal()->channel()); | 
 |       } | 
 |     } | 
 |     return nullptr; | 
 |   } | 
 |  | 
 |   rtc::PacketTransportInternal* video_rtp_transport() { | 
 |     return (video_channel() ? video_channel()->rtp_packet_transport() | 
 |                             : nullptr); | 
 |   } | 
 |  | 
 |   rtc::PacketTransportInternal* video_rtcp_transport() { | 
 |     return (video_channel() ? video_channel()->rtcp_packet_transport() | 
 |                             : nullptr); | 
 |   } | 
 |  | 
 |   cricket::VideoChannel* video_channel() { | 
 |     auto transceivers = GetInternalPeerConnection()->GetTransceiversInternal(); | 
 |     for (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 = | 
 |         absl::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 = absl::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 = absl::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 rtc::FakeNetworkManager(); | 
 |     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; | 
 |   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_CASE_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_rtcp_transport()); | 
 |   EXPECT_FALSE(caller->video_rtcp_transport()); | 
 |  | 
 |   ASSERT_TRUE( | 
 |       caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); | 
 |  | 
 |   EXPECT_FALSE(caller->voice_rtcp_transport()); | 
 |   EXPECT_FALSE(caller->video_rtcp_transport()); | 
 | } | 
 |  | 
 | // 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_TRUE(caller->voice_rtcp_transport()); | 
 |   EXPECT_TRUE(caller->video_rtcp_transport()); | 
 |  | 
 |   ASSERT_TRUE( | 
 |       caller->SetRemoteDescription(callee->CreateAnswerAndSetAsLocal())); | 
 |  | 
 |   EXPECT_FALSE(caller->voice_rtcp_transport()); | 
 |   EXPECT_FALSE(caller->video_rtcp_transport()); | 
 | } | 
 |  | 
 | 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] | 
 |       .description->mutable_streams()[0] | 
 |       .ssrcs[0] = 1111222; | 
 |   offer->description() | 
 |       ->contents()[1] | 
 |       .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_CASE_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 |