Reland "Add an active ICE controller that wraps a legacy controller (#7/n)"
This is a reland of commit 6326c9c201c7331d68c9beb0a93f6f6e21063cd2
Original change's description:
> Add an active ICE controller that wraps a legacy controller (#7/n)
>
> The wrapping ICE controller will allow existing ICE controller implementations to migrate to the active interface, and eventually deprecate the legacy interface.
>
> Follow-up CL has unit tests for P2PTransportChannel using the new wrapping controller.
>
> Bug: webrtc:14367, webrtc:14131
> Change-Id: I6c517449ff1e503e8268a7ef91afda793723fdeb
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/275302
> Reviewed-by: Per Kjellander <perkj@webrtc.org>
> Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
> Commit-Queue: Sameer Vijaykar <samvi@google.com>
> Cr-Commit-Position: refs/heads/main@{#38130}
Bug: webrtc:14367, webrtc:14131
Change-Id: I5662595db1df8c06b3acac9b39749f236906fa7e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/276044
Auto-Submit: Sameer Vijaykar <samvi@google.com>
Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
Reviewed-by: Per Kjellander <perkj@webrtc.org>
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#38149}
diff --git a/api/ice_transport_interface.h b/api/ice_transport_interface.h
index cd6ada8..2ec41aa 100644
--- a/api/ice_transport_interface.h
+++ b/api/ice_transport_interface.h
@@ -99,8 +99,10 @@
// constructed and used.
//
// 2. If the field trial is enabled
- // - then an active ICE controller factory must be supplied and is used.
- // - the legacy ICE controller factory is not used in this case.
+ // a. If an active ICE controller factory is supplied, it is used and
+ // the legacy ICE controller factory is not used.
+ // b. If not, a default active ICE controller is used, wrapping over the
+ // supplied or the default legacy ICE controller.
void set_active_ice_controller_factory(
cricket::ActiveIceControllerFactoryInterface*
active_ice_controller_factory) {
diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn
index 1009304..85ac605 100644
--- a/p2p/BUILD.gn
+++ b/p2p/BUILD.gn
@@ -81,6 +81,8 @@
"base/turn_port.cc",
"base/turn_port.h",
"base/udp_port.h",
+ "base/wrapping_active_ice_controller.cc",
+ "base/wrapping_active_ice_controller.h",
"client/basic_port_allocator.cc",
"client/basic_port_allocator.h",
"client/relay_port_factory_interface.h",
@@ -201,6 +203,7 @@
"base/fake_packet_transport.h",
"base/mock_active_ice_controller.h",
"base/mock_async_resolver.h",
+ "base/mock_ice_agent.h",
"base/mock_ice_controller.h",
"base/mock_ice_transport.h",
"base/test_stun_server.cc",
@@ -260,6 +263,7 @@
"base/transport_description_unittest.cc",
"base/turn_port_unittest.cc",
"base/turn_server_unittest.cc",
+ "base/wrapping_active_ice_controller_unittest.cc",
"client/basic_port_allocator_unittest.cc",
]
deps = [
diff --git a/p2p/base/mock_ice_agent.h b/p2p/base/mock_ice_agent.h
new file mode 100644
index 0000000..e4100ec
--- /dev/null
+++ b/p2p/base/mock_ice_agent.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2018 The WebRTC Project Authors. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef P2P_BASE_MOCK_ICE_AGENT_H_
+#define P2P_BASE_MOCK_ICE_AGENT_H_
+
+#include <vector>
+
+#include "p2p/base/connection.h"
+#include "p2p/base/ice_agent_interface.h"
+#include "p2p/base/ice_switch_reason.h"
+#include "p2p/base/transport_description.h"
+#include "test/gmock.h"
+
+namespace cricket {
+
+class MockIceAgent : public IceAgentInterface {
+ public:
+ ~MockIceAgent() override = default;
+
+ MOCK_METHOD(int64_t, GetLastPingSentMs, (), (override, const));
+ MOCK_METHOD(IceRole, GetIceRole, (), (override, const));
+ MOCK_METHOD(void, OnStartedPinging, (), (override));
+ MOCK_METHOD(void, UpdateConnectionStates, (), (override));
+ MOCK_METHOD(void, UpdateState, (), (override));
+ MOCK_METHOD(void,
+ ForgetLearnedStateForConnections,
+ (std::vector<const Connection*>),
+ (override));
+ MOCK_METHOD(void, SendPingRequest, (const Connection*), (override));
+ MOCK_METHOD(void,
+ SwitchSelectedConnection,
+ (const Connection*, IceSwitchReason),
+ (override));
+ MOCK_METHOD(bool,
+ PruneConnections,
+ (std::vector<const Connection*>),
+ (override));
+};
+
+} // namespace cricket
+
+#endif // P2P_BASE_MOCK_ICE_AGENT_H_
diff --git a/p2p/base/p2p_transport_channel.cc b/p2p/base/p2p_transport_channel.cc
index 2b0e906..0a0d8c8 100644
--- a/p2p/base/p2p_transport_channel.cc
+++ b/p2p/base/p2p_transport_channel.cc
@@ -33,6 +33,7 @@
#include "p2p/base/connection.h"
#include "p2p/base/connection_info.h"
#include "p2p/base/port.h"
+#include "p2p/base/wrapping_active_ice_controller.h"
#include "rtc_base/checks.h"
#include "rtc_base/crc32.h"
#include "rtc_base/experiments/struct_parameters_parser.h"
@@ -2472,10 +2473,15 @@
P2PTransportChannel* transport)
: transport_(transport) {
if (UseActiveIceControllerFieldTrialEnabled(field_trials)) {
- RTC_DCHECK(active_ice_controller_factory);
- ActiveIceControllerFactoryArgs active_args{args,
- /* ice_agent= */ transport};
- active_ice_controller_ = active_ice_controller_factory->Create(active_args);
+ if (active_ice_controller_factory) {
+ ActiveIceControllerFactoryArgs active_args{args,
+ /* ice_agent= */ transport};
+ active_ice_controller_ =
+ active_ice_controller_factory->Create(active_args);
+ } else {
+ active_ice_controller_ = std::make_unique<WrappingActiveIceController>(
+ /* ice_agent= */ transport, ice_controller_factory, args);
+ }
} else {
if (ice_controller_factory != nullptr) {
legacy_ice_controller_ = ice_controller_factory->Create(args);
diff --git a/p2p/base/wrapping_active_ice_controller.cc b/p2p/base/wrapping_active_ice_controller.cc
new file mode 100644
index 0000000..c665921
--- /dev/null
+++ b/p2p/base/wrapping_active_ice_controller.cc
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2022 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 "p2p/base/wrapping_active_ice_controller.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "api/sequence_checker.h"
+#include "api/task_queue/pending_task_safety_flag.h"
+#include "api/units/time_delta.h"
+#include "p2p/base/basic_ice_controller.h"
+#include "p2p/base/connection.h"
+#include "p2p/base/ice_agent_interface.h"
+#include "p2p/base/ice_controller_interface.h"
+#include "p2p/base/ice_switch_reason.h"
+#include "p2p/base/ice_transport_internal.h"
+#include "p2p/base/transport_description.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/thread.h"
+#include "rtc_base/time_utils.h"
+
+namespace {
+using ::webrtc::SafeTask;
+using ::webrtc::TimeDelta;
+} // unnamed namespace
+
+namespace cricket {
+
+WrappingActiveIceController::WrappingActiveIceController(
+ IceAgentInterface* ice_agent,
+ std::unique_ptr<IceControllerInterface> wrapped)
+ : network_thread_(rtc::Thread::Current()),
+ wrapped_(std::move(wrapped)),
+ agent_(*ice_agent) {
+ RTC_DCHECK(ice_agent != nullptr);
+}
+
+WrappingActiveIceController::WrappingActiveIceController(
+ IceAgentInterface* ice_agent,
+ IceControllerFactoryInterface* wrapped_factory,
+ const IceControllerFactoryArgs& wrapped_factory_args)
+ : network_thread_(rtc::Thread::Current()), agent_(*ice_agent) {
+ RTC_DCHECK(ice_agent != nullptr);
+ if (wrapped_factory) {
+ wrapped_ = wrapped_factory->Create(wrapped_factory_args);
+ } else {
+ wrapped_ = std::make_unique<BasicIceController>(wrapped_factory_args);
+ }
+}
+
+WrappingActiveIceController::~WrappingActiveIceController() {}
+
+void WrappingActiveIceController::SetIceConfig(const IceConfig& config) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ wrapped_->SetIceConfig(config);
+}
+
+bool WrappingActiveIceController::GetUseCandidateAttribute(
+ const Connection* connection,
+ NominationMode mode,
+ IceMode remote_ice_mode) const {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ return wrapped_->GetUseCandidateAttr(connection, mode, remote_ice_mode);
+}
+
+void WrappingActiveIceController::OnConnectionAdded(
+ const Connection* connection) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ wrapped_->AddConnection(connection);
+}
+
+void WrappingActiveIceController::OnConnectionPinged(
+ const Connection* connection) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ wrapped_->MarkConnectionPinged(connection);
+}
+
+void WrappingActiveIceController::OnConnectionUpdated(
+ const Connection* connection) {
+ RTC_LOG(LS_VERBOSE) << "Connection report for " << connection->ToString();
+ // Do nothing. Native ICE controllers have direct access to Connection, so no
+ // need to update connection state separately.
+}
+
+void WrappingActiveIceController::OnConnectionSwitched(
+ const Connection* connection) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ selected_connection_ = connection;
+ wrapped_->SetSelectedConnection(connection);
+}
+
+void WrappingActiveIceController::OnConnectionDestroyed(
+ const Connection* connection) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ wrapped_->OnConnectionDestroyed(connection);
+}
+
+void WrappingActiveIceController::MaybeStartPinging() {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ if (started_pinging_) {
+ return;
+ }
+
+ if (wrapped_->HasPingableConnection()) {
+ network_thread_->PostTask(
+ SafeTask(task_safety_.flag(), [this]() { SelectAndPingConnection(); }));
+ agent_.OnStartedPinging();
+ started_pinging_ = true;
+ }
+}
+
+void WrappingActiveIceController::SelectAndPingConnection() {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ agent_.UpdateConnectionStates();
+
+ IceControllerInterface::PingResult result =
+ wrapped_->SelectConnectionToPing(agent_.GetLastPingSentMs());
+ HandlePingResult(result);
+}
+
+void WrappingActiveIceController::HandlePingResult(
+ IceControllerInterface::PingResult result) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+
+ if (result.connection.has_value()) {
+ agent_.SendPingRequest(result.connection.value());
+ }
+
+ network_thread_->PostDelayedTask(
+ SafeTask(task_safety_.flag(), [this]() { SelectAndPingConnection(); }),
+ TimeDelta::Millis(result.recheck_delay_ms));
+}
+
+void WrappingActiveIceController::OnSortAndSwitchRequest(
+ IceSwitchReason reason) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ if (!sort_pending_) {
+ network_thread_->PostTask(SafeTask(task_safety_.flag(), [this, reason]() {
+ SortAndSwitchToBestConnection(reason);
+ }));
+ sort_pending_ = true;
+ }
+}
+
+void WrappingActiveIceController::OnImmediateSortAndSwitchRequest(
+ IceSwitchReason reason) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ SortAndSwitchToBestConnection(reason);
+}
+
+void WrappingActiveIceController::SortAndSwitchToBestConnection(
+ IceSwitchReason reason) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+
+ // Make sure the connection states are up-to-date since this affects how they
+ // will be sorted.
+ agent_.UpdateConnectionStates();
+
+ // Any changes after this point will require a re-sort.
+ sort_pending_ = false;
+
+ IceControllerInterface::SwitchResult result =
+ wrapped_->SortAndSwitchConnection(reason);
+ HandleSwitchResult(reason, result);
+ UpdateStateOnConnectionsResorted();
+}
+
+bool WrappingActiveIceController::OnImmediateSwitchRequest(
+ IceSwitchReason reason,
+ const Connection* selected) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ IceControllerInterface::SwitchResult result =
+ wrapped_->ShouldSwitchConnection(reason, selected);
+ HandleSwitchResult(reason, result);
+ return result.connection.has_value();
+}
+
+void WrappingActiveIceController::HandleSwitchResult(
+ IceSwitchReason reason_for_switch,
+ IceControllerInterface::SwitchResult result) {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ if (result.connection.has_value()) {
+ RTC_LOG(LS_INFO) << "Switching selected connection due to: "
+ << IceSwitchReasonToString(reason_for_switch);
+ agent_.SwitchSelectedConnection(result.connection.value(),
+ reason_for_switch);
+ }
+
+ if (result.recheck_event.has_value()) {
+ // If we do not switch to the connection because it missed the receiving
+ // threshold, the new connection is in a better receiving state than the
+ // currently selected connection. So we need to re-check whether it needs
+ // to be switched at a later time.
+ network_thread_->PostDelayedTask(
+ SafeTask(task_safety_.flag(),
+ [this, recheck_reason = result.recheck_event->reason]() {
+ SortAndSwitchToBestConnection(recheck_reason);
+ }),
+ TimeDelta::Millis(result.recheck_event->recheck_delay_ms));
+ }
+
+ agent_.ForgetLearnedStateForConnections(
+ result.connections_to_forget_state_on);
+}
+
+void WrappingActiveIceController::UpdateStateOnConnectionsResorted() {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ PruneConnections();
+
+ // Update the internal state of the ICE agentl.
+ agent_.UpdateState();
+
+ // Also possibly start pinging.
+ // We could start pinging if:
+ // * The first connection was created.
+ // * ICE credentials were provided.
+ // * A TCP connection became connected.
+ MaybeStartPinging();
+}
+
+void WrappingActiveIceController::PruneConnections() {
+ RTC_DCHECK_RUN_ON(network_thread_);
+
+ // The controlled side can prune only if the selected connection has been
+ // nominated because otherwise it may prune the connection that will be
+ // selected by the controlling side.
+ // TODO(honghaiz): This is not enough to prevent a connection from being
+ // pruned too early because with aggressive nomination, the controlling side
+ // will nominate every connection until it becomes writable.
+ if (agent_.GetIceRole() == ICEROLE_CONTROLLING ||
+ (selected_connection_ && selected_connection_->nominated())) {
+ std::vector<const Connection*> connections_to_prune =
+ wrapped_->PruneConnections();
+ agent_.PruneConnections(connections_to_prune);
+ }
+}
+
+// Only for unit tests
+const Connection* WrappingActiveIceController::FindNextPingableConnection() {
+ RTC_DCHECK_RUN_ON(network_thread_);
+ return wrapped_->FindNextPingableConnection();
+}
+
+} // namespace cricket
diff --git a/p2p/base/wrapping_active_ice_controller.h b/p2p/base/wrapping_active_ice_controller.h
new file mode 100644
index 0000000..449c0f0
--- /dev/null
+++ b/p2p/base/wrapping_active_ice_controller.h
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The WebRTC Project Authors. All rights reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_
+#define P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_
+
+#include <memory>
+
+#include "absl/types/optional.h"
+#include "api/task_queue/pending_task_safety_flag.h"
+#include "p2p/base/active_ice_controller_interface.h"
+#include "p2p/base/connection.h"
+#include "p2p/base/ice_agent_interface.h"
+#include "p2p/base/ice_controller_factory_interface.h"
+#include "p2p/base/ice_controller_interface.h"
+#include "p2p/base/ice_switch_reason.h"
+#include "p2p/base/ice_transport_internal.h"
+#include "p2p/base/transport_description.h"
+#include "rtc_base/thread.h"
+#include "rtc_base/thread_annotations.h"
+
+namespace cricket {
+
+// WrappingActiveIceController provides the functionality of a legacy passive
+// ICE controller but packaged as an active ICE Controller.
+class WrappingActiveIceController : public ActiveIceControllerInterface {
+ public:
+ // Constructs an active ICE controller wrapping an already constructed legacy
+ // ICE controller. Does not take ownership of the ICE agent, which must
+ // already exist and outlive the ICE controller.
+ WrappingActiveIceController(IceAgentInterface* ice_agent,
+ std::unique_ptr<IceControllerInterface> wrapped);
+ // Constructs an active ICE controller that wraps over a legacy ICE
+ // controller. The legacy ICE controller is constructed through a factory, if
+ // one is supplied. If not, a default BasicIceController is wrapped instead.
+ // Does not take ownership of the ICE agent, which must already exist and
+ // outlive the ICE controller.
+ WrappingActiveIceController(
+ IceAgentInterface* ice_agent,
+ IceControllerFactoryInterface* wrapped_factory,
+ const IceControllerFactoryArgs& wrapped_factory_args);
+ virtual ~WrappingActiveIceController();
+
+ void SetIceConfig(const IceConfig& config) override;
+ bool GetUseCandidateAttribute(const Connection* connection,
+ NominationMode mode,
+ IceMode remote_ice_mode) const override;
+
+ void OnConnectionAdded(const Connection* connection) override;
+ void OnConnectionPinged(const Connection* connection) override;
+ void OnConnectionUpdated(const Connection* connection) override;
+ void OnConnectionSwitched(const Connection* connection) override;
+ void OnConnectionDestroyed(const Connection* connection) override;
+
+ void OnSortAndSwitchRequest(IceSwitchReason reason) override;
+ void OnImmediateSortAndSwitchRequest(IceSwitchReason reason) override;
+ bool OnImmediateSwitchRequest(IceSwitchReason reason,
+ const Connection* selected) override;
+
+ // Only for unit tests
+ const Connection* FindNextPingableConnection() override;
+
+ private:
+ void MaybeStartPinging();
+ void SelectAndPingConnection();
+ void HandlePingResult(IceControllerInterface::PingResult result);
+
+ void SortAndSwitchToBestConnection(IceSwitchReason reason);
+ void HandleSwitchResult(IceSwitchReason reason_for_switch,
+ IceControllerInterface::SwitchResult result);
+ void UpdateStateOnConnectionsResorted();
+
+ void PruneConnections();
+
+ rtc::Thread* const network_thread_;
+ webrtc::ScopedTaskSafety task_safety_;
+
+ bool started_pinging_ RTC_GUARDED_BY(network_thread_) = false;
+ bool sort_pending_ RTC_GUARDED_BY(network_thread_) = false;
+ const Connection* selected_connection_ RTC_GUARDED_BY(network_thread_) =
+ nullptr;
+
+ std::unique_ptr<IceControllerInterface> wrapped_
+ RTC_GUARDED_BY(network_thread_);
+ IceAgentInterface& agent_ RTC_GUARDED_BY(network_thread_);
+};
+
+} // namespace cricket
+
+#endif // P2P_BASE_WRAPPING_ACTIVE_ICE_CONTROLLER_H_
diff --git a/p2p/base/wrapping_active_ice_controller_unittest.cc b/p2p/base/wrapping_active_ice_controller_unittest.cc
new file mode 100644
index 0000000..7dfdfef
--- /dev/null
+++ b/p2p/base/wrapping_active_ice_controller_unittest.cc
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2009 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 "p2p/base/wrapping_active_ice_controller.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "p2p/base/connection.h"
+#include "p2p/base/mock_ice_agent.h"
+#include "p2p/base/mock_ice_controller.h"
+#include "rtc_base/fake_clock.h"
+#include "rtc_base/gunit.h"
+#include "rtc_base/thread.h"
+
+namespace {
+
+using ::cricket::Connection;
+using ::cricket::IceConfig;
+using ::cricket::IceControllerFactoryArgs;
+using ::cricket::IceControllerInterface;
+using ::cricket::IceMode;
+using ::cricket::IceRecheckEvent;
+using ::cricket::IceSwitchReason;
+using ::cricket::MockIceAgent;
+using ::cricket::MockIceController;
+using ::cricket::MockIceControllerFactory;
+using ::cricket::NominationMode;
+using ::cricket::WrappingActiveIceController;
+
+using ::testing::_;
+using ::testing::NiceMock;
+using ::testing::Ref;
+using ::testing::Return;
+using ::testing::Sequence;
+
+using ::rtc::AutoThread;
+using ::rtc::Event;
+using ::rtc::ScopedFakeClock;
+using ::webrtc::TimeDelta;
+
+using NiceMockIceController = NiceMock<MockIceController>;
+
+static const Connection* kConnection =
+ reinterpret_cast<const Connection*>(0xabcd);
+static const Connection* kConnectionTwo =
+ reinterpret_cast<const Connection*>(0xbcde);
+static const Connection* kConnectionThree =
+ reinterpret_cast<const Connection*>(0xcdef);
+
+static const std::vector<const Connection*> kEmptyConnsList =
+ std::vector<const Connection*>();
+
+static const TimeDelta kTick = TimeDelta::Millis(1);
+
+TEST(WrappingActiveIceControllerTest, CreateLegacyIceControllerFromFactory) {
+ AutoThread main;
+ MockIceAgent agent;
+ IceControllerFactoryArgs args;
+ MockIceControllerFactory legacy_controller_factory;
+ EXPECT_CALL(legacy_controller_factory, RecordIceControllerCreated()).Times(1);
+ WrappingActiveIceController controller(&agent, &legacy_controller_factory,
+ args);
+}
+
+TEST(WrappingActiveIceControllerTest, PassthroughIceControllerInterface) {
+ AutoThread main;
+ MockIceAgent agent;
+ std::unique_ptr<MockIceController> will_move =
+ std::make_unique<MockIceController>(IceControllerFactoryArgs{});
+ MockIceController* wrapped = will_move.get();
+ WrappingActiveIceController controller(&agent, std::move(will_move));
+
+ IceConfig config{};
+ EXPECT_CALL(*wrapped, SetIceConfig(Ref(config)));
+ controller.SetIceConfig(config);
+
+ EXPECT_CALL(*wrapped,
+ GetUseCandidateAttr(kConnection, NominationMode::AGGRESSIVE,
+ IceMode::ICEMODE_LITE))
+ .WillOnce(Return(true));
+ EXPECT_TRUE(controller.GetUseCandidateAttribute(
+ kConnection, NominationMode::AGGRESSIVE, IceMode::ICEMODE_LITE));
+
+ EXPECT_CALL(*wrapped, AddConnection(kConnection));
+ controller.OnConnectionAdded(kConnection);
+
+ EXPECT_CALL(*wrapped, OnConnectionDestroyed(kConnection));
+ controller.OnConnectionDestroyed(kConnection);
+
+ EXPECT_CALL(*wrapped, SetSelectedConnection(kConnection));
+ controller.OnConnectionSwitched(kConnection);
+
+ EXPECT_CALL(*wrapped, MarkConnectionPinged(kConnection));
+ controller.OnConnectionPinged(kConnection);
+
+ EXPECT_CALL(*wrapped, FindNextPingableConnection())
+ .WillOnce(Return(kConnection));
+ EXPECT_EQ(controller.FindNextPingableConnection(), kConnection);
+}
+
+TEST(WrappingActiveIceControllerTest, HandlesImmediateSwitchRequest) {
+ AutoThread main;
+ ScopedFakeClock clock;
+ NiceMock<MockIceAgent> agent;
+ std::unique_ptr<NiceMockIceController> will_move =
+ std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+ NiceMockIceController* wrapped = will_move.get();
+ WrappingActiveIceController controller(&agent, std::move(will_move));
+
+ IceSwitchReason reason = IceSwitchReason::NOMINATION_ON_CONTROLLED_SIDE;
+ std::vector<const Connection*> conns_to_forget{kConnectionTwo};
+ int recheck_delay_ms = 10;
+ IceControllerInterface::SwitchResult switch_result{
+ kConnection,
+ IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK,
+ recheck_delay_ms),
+ conns_to_forget};
+
+ // ICE controller should switch to given connection immediately.
+ Sequence check_then_switch;
+ EXPECT_CALL(*wrapped, ShouldSwitchConnection(reason, kConnection))
+ .InSequence(check_then_switch)
+ .WillOnce(Return(switch_result));
+ EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason))
+ .InSequence(check_then_switch);
+ EXPECT_CALL(agent, ForgetLearnedStateForConnections(conns_to_forget));
+
+ EXPECT_TRUE(controller.OnImmediateSwitchRequest(reason, kConnection));
+
+ // No rechecks before recheck delay.
+ clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms - 1));
+
+ // ICE controller should recheck for best connection after the recheck delay.
+ Sequence recheck_sort;
+ EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(recheck_sort);
+ EXPECT_CALL(*wrapped,
+ SortAndSwitchConnection(IceSwitchReason::ICE_CONTROLLER_RECHECK))
+ .InSequence(recheck_sort)
+ .WillOnce(Return(IceControllerInterface::SwitchResult{}));
+ EXPECT_CALL(agent, ForgetLearnedStateForConnections(kEmptyConnsList));
+
+ clock.AdvanceTime(kTick);
+}
+
+TEST(WrappingActiveIceControllerTest, HandlesImmediateSortAndSwitchRequest) {
+ AutoThread main;
+ ScopedFakeClock clock;
+ NiceMock<MockIceAgent> agent;
+ std::unique_ptr<NiceMockIceController> will_move =
+ std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+ NiceMockIceController* wrapped = will_move.get();
+ WrappingActiveIceController controller(&agent, std::move(will_move));
+
+ IceSwitchReason reason = IceSwitchReason::NEW_CONNECTION_FROM_LOCAL_CANDIDATE;
+ std::vector<const Connection*> conns_to_forget{kConnectionTwo};
+ std::vector<const Connection*> conns_to_prune{kConnectionThree};
+ int recheck_delay_ms = 10;
+ IceControllerInterface::SwitchResult switch_result{
+ kConnection,
+ IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK,
+ recheck_delay_ms),
+ conns_to_forget};
+
+ Sequence sort_and_switch;
+ EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(sort_and_switch);
+ EXPECT_CALL(*wrapped, SortAndSwitchConnection(reason))
+ .InSequence(sort_and_switch)
+ .WillOnce(Return(switch_result));
+ EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason))
+ .InSequence(sort_and_switch);
+ EXPECT_CALL(*wrapped, PruneConnections())
+ .InSequence(sort_and_switch)
+ .WillOnce(Return(conns_to_prune));
+ EXPECT_CALL(agent, PruneConnections(conns_to_prune))
+ .InSequence(sort_and_switch);
+
+ controller.OnImmediateSortAndSwitchRequest(reason);
+
+ // No rechecks before recheck delay.
+ clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms - 1));
+
+ // ICE controller should recheck for best connection after the recheck delay.
+ Sequence recheck_sort;
+ EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(recheck_sort);
+ EXPECT_CALL(*wrapped,
+ SortAndSwitchConnection(IceSwitchReason::ICE_CONTROLLER_RECHECK))
+ .InSequence(recheck_sort)
+ .WillOnce(Return(IceControllerInterface::SwitchResult{}));
+ EXPECT_CALL(*wrapped, PruneConnections())
+ .InSequence(recheck_sort)
+ .WillOnce(Return(kEmptyConnsList));
+ EXPECT_CALL(agent, PruneConnections(kEmptyConnsList))
+ .InSequence(recheck_sort);
+
+ clock.AdvanceTime(kTick);
+}
+
+TEST(WrappingActiveIceControllerTest, HandlesSortAndSwitchRequest) {
+ AutoThread main;
+ ScopedFakeClock clock;
+
+ // Block the main task queue until ready.
+ Event init;
+ TimeDelta init_delay = TimeDelta::Millis(10);
+ main.PostTask([&init, &init_delay] { init.Wait(init_delay); });
+
+ NiceMock<MockIceAgent> agent;
+ std::unique_ptr<NiceMockIceController> will_move =
+ std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+ NiceMockIceController* wrapped = will_move.get();
+ WrappingActiveIceController controller(&agent, std::move(will_move));
+
+ IceSwitchReason reason = IceSwitchReason::NETWORK_PREFERENCE_CHANGE;
+
+ // No action should occur immediately
+ EXPECT_CALL(agent, UpdateConnectionStates()).Times(0);
+ EXPECT_CALL(*wrapped, SortAndSwitchConnection(_)).Times(0);
+ EXPECT_CALL(agent, SwitchSelectedConnection(_, _)).Times(0);
+
+ controller.OnSortAndSwitchRequest(reason);
+
+ std::vector<const Connection*> conns_to_forget{kConnectionTwo};
+ int recheck_delay_ms = 10;
+ IceControllerInterface::SwitchResult switch_result{
+ kConnection,
+ IceRecheckEvent(IceSwitchReason::ICE_CONTROLLER_RECHECK,
+ recheck_delay_ms),
+ conns_to_forget};
+
+ // Sort and switch should take place as the subsequent task.
+ Sequence sort_and_switch;
+ EXPECT_CALL(agent, UpdateConnectionStates()).InSequence(sort_and_switch);
+ EXPECT_CALL(*wrapped, SortAndSwitchConnection(reason))
+ .InSequence(sort_and_switch)
+ .WillOnce(Return(switch_result));
+ EXPECT_CALL(agent, SwitchSelectedConnection(kConnection, reason))
+ .InSequence(sort_and_switch);
+
+ // Unblock the init task.
+ clock.AdvanceTime(init_delay);
+}
+
+TEST(WrappingActiveIceControllerTest, StartPingingAfterSortAndSwitch) {
+ AutoThread main;
+ ScopedFakeClock clock;
+
+ // Block the main task queue until ready.
+ Event init;
+ TimeDelta init_delay = TimeDelta::Millis(10);
+ main.PostTask([&init, &init_delay] { init.Wait(init_delay); });
+
+ NiceMock<MockIceAgent> agent;
+ std::unique_ptr<NiceMockIceController> will_move =
+ std::make_unique<NiceMockIceController>(IceControllerFactoryArgs{});
+ NiceMockIceController* wrapped = will_move.get();
+ WrappingActiveIceController controller(&agent, std::move(will_move));
+
+ // Pinging does not start automatically, unless triggered through a sort.
+ EXPECT_CALL(*wrapped, HasPingableConnection()).Times(0);
+ EXPECT_CALL(*wrapped, SelectConnectionToPing(_)).Times(0);
+ EXPECT_CALL(agent, OnStartedPinging()).Times(0);
+
+ controller.OnSortAndSwitchRequest(IceSwitchReason::DATA_RECEIVED);
+
+ // Pinging does not start if no pingable connection.
+ EXPECT_CALL(*wrapped, HasPingableConnection()).WillOnce(Return(false));
+ EXPECT_CALL(*wrapped, SelectConnectionToPing(_)).Times(0);
+ EXPECT_CALL(agent, OnStartedPinging()).Times(0);
+
+ // Unblock the init task.
+ clock.AdvanceTime(init_delay);
+
+ int recheck_delay_ms = 10;
+ IceControllerInterface::PingResult ping_result(kConnection, recheck_delay_ms);
+
+ // Pinging starts when there is a pingable connection.
+ Sequence start_pinging;
+ EXPECT_CALL(*wrapped, HasPingableConnection())
+ .InSequence(start_pinging)
+ .WillOnce(Return(true));
+ EXPECT_CALL(agent, OnStartedPinging()).InSequence(start_pinging);
+ EXPECT_CALL(agent, GetLastPingSentMs())
+ .InSequence(start_pinging)
+ .WillOnce(Return(123));
+ EXPECT_CALL(*wrapped, SelectConnectionToPing(123))
+ .InSequence(start_pinging)
+ .WillOnce(Return(ping_result));
+ EXPECT_CALL(agent, SendPingRequest(kConnection)).InSequence(start_pinging);
+
+ controller.OnSortAndSwitchRequest(IceSwitchReason::DATA_RECEIVED);
+ clock.AdvanceTime(kTick);
+
+ // ICE controller should recheck and ping after the recheck delay.
+ // No ping should be sent if no connection selected to ping.
+ EXPECT_CALL(agent, GetLastPingSentMs()).WillOnce(Return(456));
+ EXPECT_CALL(*wrapped, SelectConnectionToPing(456))
+ .WillOnce(Return(IceControllerInterface::PingResult(
+ /* connection= */ nullptr, recheck_delay_ms)));
+ EXPECT_CALL(agent, SendPingRequest(kConnection)).Times(0);
+
+ clock.AdvanceTime(TimeDelta::Millis(recheck_delay_ms));
+}
+
+} // namespace