Reland "Structured ICE logging via RtcEventLog."

This is a reland of eed5aa8904d09179971d3f4e7e10c109d7c62bfc
Original change's description:
> Structured ICE logging via RtcEventLog.
>
> This change list contains the structured logging module for ICE using
> the RtcEventLog infrastructure, and also extension to the log parser
> and analyzer.
>
> Bug: None
> Change-Id: I6539cf282155c2cde4d3161c53500c0746671a02
> Reviewed-on: https://webrtc-review.googlesource.com/34622
> Commit-Queue: Qingsi Wang <qingsi@google.com>
> Reviewed-by: Björn Terelius <terelius@webrtc.org>
> Reviewed-by: Peter Thatcher <pthatcher@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#21816}

TBR=pthatcher@webrtc.org,terelius@webrtc.org,deadbeef@webrtc.org

Bug: None
Change-Id: I3df585bf636315ceb0273967146111346a83be86
Reviewed-on: https://webrtc-review.googlesource.com/47545
Commit-Queue: Qingsi Wang <qingsi@google.com>
Reviewed-by: Qingsi Wang <qingsi@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#21881}
diff --git a/logging/BUILD.gn b/logging/BUILD.gn
index f1f2d91..b65c0b8 100644
--- a/logging/BUILD.gn
+++ b/logging/BUILD.gn
@@ -164,6 +164,7 @@
   defines = []
 
   deps = [
+    ":ice_log",
     ":rtc_event_audio",
     ":rtc_event_bwe",
     ":rtc_event_log_api",
@@ -221,6 +222,7 @@
   defines = []
 
   deps = [
+    ":ice_log",
     ":rtc_event_log_api",
     ":rtc_event_log_impl_encoder",
     ":rtc_event_log_impl_output",
@@ -263,6 +265,7 @@
     ]
 
     deps = [
+      ":ice_log",
       ":rtc_event_bwe",
       ":rtc_event_log2_proto",
       ":rtc_event_log_api",
@@ -400,6 +403,28 @@
   }
 }
 
+rtc_source_set("ice_log") {
+  sources = [
+    "rtc_event_log/events/rtc_event_ice_candidate_pair.cc",
+    "rtc_event_log/events/rtc_event_ice_candidate_pair.h",
+    "rtc_event_log/events/rtc_event_ice_candidate_pair_config.cc",
+    "rtc_event_log/events/rtc_event_ice_candidate_pair_config.h",
+    "rtc_event_log/icelogger.cc",
+    "rtc_event_log/icelogger.h",
+  ]
+
+  deps = [
+    ":rtc_event_log_api",
+    "../api:libjingle_logging_api",
+    "../rtc_base:rtc_base_approved",
+  ]
+
+  if (!build_with_chromium && is_clang) {
+    # Suppress warnings from the Chromium Clang plugin (bugs.webrtc.org/163).
+    suppressed_configs += [ "//build/config/clang:find_bad_constructs" ]
+  }
+}
+
 if (rtc_include_tests) {
   rtc_source_set("mocks") {
     testonly = true
diff --git a/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.cc b/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.cc
index 1f02bcc..9ebdcbc 100644
--- a/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.cc
+++ b/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.cc
@@ -17,6 +17,8 @@
 #include "logging/rtc_event_log/events/rtc_event_audio_send_stream_config.h"
 #include "logging/rtc_event_log/events/rtc_event_bwe_update_delay_based.h"
 #include "logging/rtc_event_log/events/rtc_event_bwe_update_loss_based.h"
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h"
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h"
 #include "logging/rtc_event_log/events/rtc_event_probe_cluster_created.h"
 #include "logging/rtc_event_log/events/rtc_event_probe_result_failure.h"
 #include "logging/rtc_event_log/events/rtc_event_probe_result_success.h"
@@ -103,6 +105,122 @@
   RTC_NOTREACHED();
   return rtclog::VideoReceiveConfig::RTCP_COMPOUND;
 }
+
+rtclog::IceCandidatePairConfig::IceCandidatePairConfigType
+ConvertIceCandidatePairConfigType(IceCandidatePairEventType type) {
+  switch (type) {
+    case IceCandidatePairEventType::kAdded:
+      return rtclog::IceCandidatePairConfig::ADDED;
+    case IceCandidatePairEventType::kUpdated:
+      return rtclog::IceCandidatePairConfig::UPDATED;
+    case IceCandidatePairEventType::kDestroyed:
+      return rtclog::IceCandidatePairConfig::DESTROYED;
+    case IceCandidatePairEventType::kSelected:
+      return rtclog::IceCandidatePairConfig::SELECTED;
+    default:
+      RTC_NOTREACHED();
+  }
+  RTC_NOTREACHED();
+  return rtclog::IceCandidatePairConfig::ADDED;
+}
+
+rtclog::IceCandidatePairConfig::IceCandidateType ConvertIceCandidateType(
+    IceCandidateType type) {
+  switch (type) {
+    case IceCandidateType::kLocal:
+      return rtclog::IceCandidatePairConfig::LOCAL;
+    case IceCandidateType::kStun:
+      return rtclog::IceCandidatePairConfig::STUN;
+    case IceCandidateType::kPrflx:
+      return rtclog::IceCandidatePairConfig::PRFLX;
+    case IceCandidateType::kRelay:
+      return rtclog::IceCandidatePairConfig::RELAY;
+    case IceCandidateType::kUnknown:
+      return rtclog::IceCandidatePairConfig::UNKNOWN_CANDIDATE_TYPE;
+    default:
+      RTC_NOTREACHED();
+  }
+  RTC_NOTREACHED();
+  return rtclog::IceCandidatePairConfig::UNKNOWN_CANDIDATE_TYPE;
+}
+
+rtclog::IceCandidatePairConfig::Protocol ConvertIceCandidatePairProtocol(
+    IceCandidatePairProtocol protocol) {
+  switch (protocol) {
+    case IceCandidatePairProtocol::kUdp:
+      return rtclog::IceCandidatePairConfig::UDP;
+    case IceCandidatePairProtocol::kTcp:
+      return rtclog::IceCandidatePairConfig::TCP;
+    case IceCandidatePairProtocol::kSsltcp:
+      return rtclog::IceCandidatePairConfig::SSLTCP;
+    case IceCandidatePairProtocol::kTls:
+      return rtclog::IceCandidatePairConfig::TLS;
+    case IceCandidatePairProtocol::kUnknown:
+      return rtclog::IceCandidatePairConfig::UNKNOWN_PROTOCOL;
+    default:
+      RTC_NOTREACHED();
+  }
+  RTC_NOTREACHED();
+  return rtclog::IceCandidatePairConfig::UNKNOWN_PROTOCOL;
+}
+
+rtclog::IceCandidatePairConfig::AddressFamily
+ConvertIceCandidatePairAddressFamily(
+    IceCandidatePairAddressFamily address_family) {
+  switch (address_family) {
+    case IceCandidatePairAddressFamily::kIpv4:
+      return rtclog::IceCandidatePairConfig::IPV4;
+    case IceCandidatePairAddressFamily::kIpv6:
+      return rtclog::IceCandidatePairConfig::IPV6;
+    case IceCandidatePairAddressFamily::kUnknown:
+      return rtclog::IceCandidatePairConfig::UNKNOWN_ADDRESS_FAMILY;
+    default:
+      RTC_NOTREACHED();
+  }
+  RTC_NOTREACHED();
+  return rtclog::IceCandidatePairConfig::UNKNOWN_ADDRESS_FAMILY;
+}
+
+rtclog::IceCandidatePairConfig::NetworkType ConvertIceCandidateNetworkType(
+    IceCandidateNetworkType network_type) {
+  switch (network_type) {
+    case IceCandidateNetworkType::kEthernet:
+      return rtclog::IceCandidatePairConfig::ETHERNET;
+    case IceCandidateNetworkType::kLoopback:
+      return rtclog::IceCandidatePairConfig::LOOPBACK;
+    case IceCandidateNetworkType::kWifi:
+      return rtclog::IceCandidatePairConfig::WIFI;
+    case IceCandidateNetworkType::kVpn:
+      return rtclog::IceCandidatePairConfig::VPN;
+    case IceCandidateNetworkType::kCellular:
+      return rtclog::IceCandidatePairConfig::CELLULAR;
+    case IceCandidateNetworkType::kUnknown:
+      return rtclog::IceCandidatePairConfig::UNKNOWN_NETWORK_TYPE;
+    default:
+      RTC_NOTREACHED();
+  }
+  RTC_NOTREACHED();
+  return rtclog::IceCandidatePairConfig::UNKNOWN_NETWORK_TYPE;
+}
+
+rtclog::IceCandidatePairEvent::IceCandidatePairEventType
+ConvertIceCandidatePairEventType(IceCandidatePairEventType type) {
+  switch (type) {
+    case IceCandidatePairEventType::kCheckSent:
+      return rtclog::IceCandidatePairEvent::CHECK_SENT;
+    case IceCandidatePairEventType::kCheckReceived:
+      return rtclog::IceCandidatePairEvent::CHECK_RECEIVED;
+    case IceCandidatePairEventType::kCheckResponseSent:
+      return rtclog::IceCandidatePairEvent::CHECK_RESPONSE_SENT;
+    case IceCandidatePairEventType::kCheckResponseReceived:
+      return rtclog::IceCandidatePairEvent::CHECK_RESPONSE_RECEIVED;
+    default:
+      RTC_NOTREACHED();
+  }
+  RTC_NOTREACHED();
+  return rtclog::IceCandidatePairEvent::CHECK_SENT;
+}
+
 }  // namespace
 
 std::string RtcEventLogEncoderLegacy::EncodeLogStart(int64_t timestamp_us) {
@@ -171,6 +289,17 @@
       return EncodeBweUpdateLossBased(rtc_event);
     }
 
+    case RtcEvent::Type::IceCandidatePairConfig: {
+      auto& rtc_event =
+          static_cast<const RtcEventIceCandidatePairConfig&>(event);
+      return EncodeIceCandidatePairConfig(rtc_event);
+    }
+
+    case RtcEvent::Type::IceCandidatePairEvent: {
+      auto& rtc_event = static_cast<const RtcEventIceCandidatePair&>(event);
+      return EncodeIceCandidatePairEvent(rtc_event);
+    }
+
     case RtcEvent::Type::ProbeClusterCreated: {
       auto& rtc_event = static_cast<const RtcEventProbeClusterCreated&>(event);
       return EncodeProbeClusterCreated(rtc_event);
@@ -343,6 +472,48 @@
   return Serialize(&rtclog_event);
 }
 
+std::string RtcEventLogEncoderLegacy::EncodeIceCandidatePairConfig(
+    const RtcEventIceCandidatePairConfig& event) {
+  rtclog::Event encoded_rtc_event;
+  encoded_rtc_event.set_timestamp_us(event.timestamp_us_);
+  encoded_rtc_event.set_type(rtclog::Event::ICE_CANDIDATE_PAIR_CONFIG);
+
+  auto encoded_ice_event =
+      encoded_rtc_event.mutable_ice_candidate_pair_config();
+  encoded_ice_event->set_config_type(
+      ConvertIceCandidatePairConfigType(event.type_));
+  encoded_ice_event->set_candidate_pair_id(event.candidate_pair_id_);
+  const auto& desc = event.candidate_pair_desc_;
+  encoded_ice_event->set_local_candidate_type(
+      ConvertIceCandidateType(desc.local_candidate_type));
+  encoded_ice_event->set_local_relay_protocol(
+      ConvertIceCandidatePairProtocol(desc.local_relay_protocol));
+  encoded_ice_event->set_local_network_type(
+      ConvertIceCandidateNetworkType(desc.local_network_type));
+  encoded_ice_event->set_local_address_family(
+      ConvertIceCandidatePairAddressFamily(desc.local_address_family));
+  encoded_ice_event->set_remote_candidate_type(
+      ConvertIceCandidateType(desc.remote_candidate_type));
+  encoded_ice_event->set_remote_address_family(
+      ConvertIceCandidatePairAddressFamily(desc.remote_address_family));
+  encoded_ice_event->set_candidate_pair_protocol(
+      ConvertIceCandidatePairProtocol(desc.candidate_pair_protocol));
+  return Serialize(&encoded_rtc_event);
+}
+
+std::string RtcEventLogEncoderLegacy::EncodeIceCandidatePairEvent(
+    const RtcEventIceCandidatePair& event) {
+  rtclog::Event encoded_rtc_event;
+  encoded_rtc_event.set_timestamp_us(event.timestamp_us_);
+  encoded_rtc_event.set_type(rtclog::Event::ICE_CANDIDATE_PAIR_EVENT);
+
+  auto encoded_ice_event = encoded_rtc_event.mutable_ice_candidate_pair_event();
+  encoded_ice_event->set_event_type(
+      ConvertIceCandidatePairEventType(event.type_));
+  encoded_ice_event->set_candidate_pair_id(event.candidate_pair_id_);
+  return Serialize(&encoded_rtc_event);
+}
+
 std::string RtcEventLogEncoderLegacy::EncodeProbeClusterCreated(
     const RtcEventProbeClusterCreated& event) {
   rtclog::Event rtclog_event;
diff --git a/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.h b/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.h
index 63102c5..87db039 100644
--- a/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.h
+++ b/logging/rtc_event_log/encoder/rtc_event_log_encoder_legacy.h
@@ -32,6 +32,8 @@
 class RtcEventAudioSendStreamConfig;
 class RtcEventBweUpdateDelayBased;
 class RtcEventBweUpdateLossBased;
+class RtcEventIceCandidatePairConfig;
+class RtcEventIceCandidatePair;
 class RtcEventLoggingStarted;
 class RtcEventLoggingStopped;
 class RtcEventProbeClusterCreated;
@@ -71,6 +73,10 @@
   std::string EncodeBweUpdateDelayBased(
       const RtcEventBweUpdateDelayBased& event);
   std::string EncodeBweUpdateLossBased(const RtcEventBweUpdateLossBased& event);
+  std::string EncodeIceCandidatePairConfig(
+      const RtcEventIceCandidatePairConfig& event);
+  std::string EncodeIceCandidatePairEvent(
+      const RtcEventIceCandidatePair& event);
   std::string EncodeProbeClusterCreated(
       const RtcEventProbeClusterCreated& event);
   std::string EncodeProbeResultFailure(const RtcEventProbeResultFailure& event);
diff --git a/logging/rtc_event_log/events/rtc_event.h b/logging/rtc_event_log/events/rtc_event.h
index 6410698..25820ef 100644
--- a/logging/rtc_event_log/events/rtc_event.h
+++ b/logging/rtc_event_log/events/rtc_event.h
@@ -37,6 +37,8 @@
     AudioSendStreamConfig,
     BweUpdateDelayBased,
     BweUpdateLossBased,
+    IceCandidatePairConfig,
+    IceCandidatePairEvent,
     ProbeClusterCreated,
     ProbeResultFailure,
     ProbeResultSuccess,
diff --git a/logging/rtc_event_log/events/rtc_event_ice_candidate_pair.cc b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair.cc
new file mode 100644
index 0000000..b3d084e
--- /dev/null
+++ b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair.cc
@@ -0,0 +1,30 @@
+/*
+ *  Copyright (c) 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 "logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h"
+
+namespace webrtc {
+
+RtcEventIceCandidatePair::RtcEventIceCandidatePair(
+    IceCandidatePairEventType type,
+    uint32_t candidate_pair_id)
+    : type_(type), candidate_pair_id_(candidate_pair_id) {}
+
+RtcEventIceCandidatePair::~RtcEventIceCandidatePair() = default;
+
+RtcEvent::Type RtcEventIceCandidatePair::GetType() const {
+  return RtcEvent::Type::IceCandidatePairEvent;
+}
+
+bool RtcEventIceCandidatePair::IsConfigEvent() const {
+  return false;
+}
+
+}  // namespace webrtc
diff --git a/logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h
new file mode 100644
index 0000000..b6cd61f
--- /dev/null
+++ b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h
@@ -0,0 +1,51 @@
+/*
+ *  Copyright (c) 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.
+ */
+
+#ifndef LOGGING_RTC_EVENT_LOG_EVENTS_RTC_EVENT_ICE_CANDIDATE_PAIR_H_
+#define LOGGING_RTC_EVENT_LOG_EVENTS_RTC_EVENT_ICE_CANDIDATE_PAIR_H_
+
+#include "logging/rtc_event_log/events/rtc_event.h"
+
+#include <string>
+
+namespace webrtc {
+
+enum class IceCandidatePairEventType {
+  // Config event types for events related to the candiate pair creation and
+  // life-cycle management.
+  kAdded,
+  kUpdated,
+  kDestroyed,
+  kSelected,
+  // Non-config event types.
+  kCheckSent,
+  kCheckReceived,
+  kCheckResponseSent,
+  kCheckResponseReceived,
+};
+
+class RtcEventIceCandidatePair final : public RtcEvent {
+ public:
+  RtcEventIceCandidatePair(IceCandidatePairEventType type,
+                           uint32_t candidate_pair_id);
+
+  ~RtcEventIceCandidatePair() override;
+
+  Type GetType() const override;
+
+  bool IsConfigEvent() const override;
+
+  const IceCandidatePairEventType type_;
+  const uint32_t candidate_pair_id_;
+};
+
+}  // namespace webrtc
+
+#endif  // LOGGING_RTC_EVENT_LOG_EVENTS_RTC_EVENT_ICE_CANDIDATE_PAIR_H_
diff --git a/logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.cc b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.cc
new file mode 100644
index 0000000..33fd8c7
--- /dev/null
+++ b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.cc
@@ -0,0 +1,58 @@
+/*
+ *  Copyright (c) 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 "logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h"
+
+namespace webrtc {
+
+IceCandidatePairDescription::IceCandidatePairDescription() {
+  local_candidate_type = IceCandidateType::kUnknown;
+  local_relay_protocol = IceCandidatePairProtocol::kUnknown;
+  local_network_type = IceCandidateNetworkType::kUnknown;
+  local_address_family = IceCandidatePairAddressFamily::kUnknown;
+  remote_candidate_type = IceCandidateType::kUnknown;
+  remote_address_family = IceCandidatePairAddressFamily::kUnknown;
+  candidate_pair_protocol = IceCandidatePairProtocol::kUnknown;
+}
+
+IceCandidatePairDescription::IceCandidatePairDescription(
+    const IceCandidatePairDescription& other) {
+  local_candidate_type = other.local_candidate_type;
+  local_relay_protocol = other.local_relay_protocol;
+  local_network_type = other.local_network_type;
+  local_address_family = other.local_address_family;
+  remote_candidate_type = other.remote_candidate_type;
+  remote_address_family = other.remote_address_family;
+  candidate_pair_protocol = other.candidate_pair_protocol;
+}
+
+IceCandidatePairDescription::~IceCandidatePairDescription() {}
+
+RtcEventIceCandidatePairConfig::RtcEventIceCandidatePairConfig(
+    IceCandidatePairEventType type,
+    uint32_t candidate_pair_id,
+    const IceCandidatePairDescription& candidate_pair_desc)
+    : type_(type),
+      candidate_pair_id_(candidate_pair_id),
+      candidate_pair_desc_(candidate_pair_desc) {}
+
+RtcEventIceCandidatePairConfig::~RtcEventIceCandidatePairConfig() = default;
+
+RtcEvent::Type RtcEventIceCandidatePairConfig::GetType() const {
+  return RtcEvent::Type::IceCandidatePairConfig;
+}
+
+// The ICE candidate pair config event is not equivalent to a RtcEventLog config
+// event.
+bool RtcEventIceCandidatePairConfig::IsConfigEvent() const {
+  return false;
+}
+
+}  // namespace webrtc
diff --git a/logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h
new file mode 100644
index 0000000..70c81bf
--- /dev/null
+++ b/logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h
@@ -0,0 +1,92 @@
+/*
+ *  Copyright (c) 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.
+ */
+
+#ifndef LOGGING_RTC_EVENT_LOG_EVENTS_RTC_EVENT_ICE_CANDIDATE_PAIR_CONFIG_H_
+#define LOGGING_RTC_EVENT_LOG_EVENTS_RTC_EVENT_ICE_CANDIDATE_PAIR_CONFIG_H_
+
+#include "logging/rtc_event_log/events/rtc_event.h"
+
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h"
+
+#include <string>
+
+namespace webrtc {
+
+// TODO(qingsi): Change the names of candidate types to "host", "srflx", "prflx"
+// and "relay" after the naming is spec-compliant in the signaling part
+enum class IceCandidateType {
+  kLocal,
+  kStun,
+  kPrflx,
+  kRelay,
+  kUnknown,
+};
+
+enum class IceCandidatePairProtocol {
+  kUdp,
+  kTcp,
+  kSsltcp,
+  kTls,
+  kUnknown,
+};
+
+enum class IceCandidatePairAddressFamily {
+  kIpv4,
+  kIpv6,
+  kUnknown,
+};
+
+enum class IceCandidateNetworkType {
+  kEthernet,
+  kLoopback,
+  kWifi,
+  kVpn,
+  kCellular,
+  kUnknown,
+};
+
+class IceCandidatePairDescription {
+ public:
+  IceCandidatePairDescription();
+  explicit IceCandidatePairDescription(
+      const IceCandidatePairDescription& other);
+
+  ~IceCandidatePairDescription();
+
+  IceCandidateType local_candidate_type;
+  IceCandidatePairProtocol local_relay_protocol;
+  IceCandidateNetworkType local_network_type;
+  IceCandidatePairAddressFamily local_address_family;
+  IceCandidateType remote_candidate_type;
+  IceCandidatePairAddressFamily remote_address_family;
+  IceCandidatePairProtocol candidate_pair_protocol;
+};
+
+class RtcEventIceCandidatePairConfig final : public RtcEvent {
+ public:
+  RtcEventIceCandidatePairConfig(
+      IceCandidatePairEventType type,
+      uint32_t candidate_pair_id,
+      const IceCandidatePairDescription& candidate_pair_desc);
+
+  ~RtcEventIceCandidatePairConfig() override;
+
+  Type GetType() const override;
+
+  bool IsConfigEvent() const override;
+
+  const IceCandidatePairEventType type_;
+  const uint32_t candidate_pair_id_;
+  const IceCandidatePairDescription candidate_pair_desc_;
+};
+
+}  // namespace webrtc
+
+#endif  // LOGGING_RTC_EVENT_LOG_EVENTS_RTC_EVENT_ICE_CANDIDATE_PAIR_CONFIG_H_
diff --git a/logging/rtc_event_log/icelogger.cc b/logging/rtc_event_log/icelogger.cc
new file mode 100644
index 0000000..108f966
--- /dev/null
+++ b/logging/rtc_event_log/icelogger.cc
@@ -0,0 +1,54 @@
+/*
+ *  Copyright (c) 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 "logging/rtc_event_log/icelogger.h"
+
+#include "logging/rtc_event_log/rtc_event_log.h"
+#include "rtc_base/ptr_util.h"
+
+namespace webrtc {
+
+IceEventLog::IceEventLog() {}
+IceEventLog::~IceEventLog() {}
+
+bool IceEventLog::IsIceCandidatePairConfigEvent(
+    IceCandidatePairEventType type) {
+  return (type == IceCandidatePairEventType::kAdded) ||
+         (type == IceCandidatePairEventType::kUpdated) ||
+         (type == IceCandidatePairEventType::kDestroyed) ||
+         (type == IceCandidatePairEventType::kSelected);
+}
+
+void IceEventLog::LogCandidatePairEvent(
+    IceCandidatePairEventType type,
+    uint32_t candidate_pair_id,
+    const IceCandidatePairDescription& candidate_pair_desc) {
+  if (event_log_ == nullptr) {
+    return;
+  }
+  if (IsIceCandidatePairConfigEvent(type)) {
+    candidate_pair_desc_by_id_[candidate_pair_id] = candidate_pair_desc;
+    event_log_->Log(rtc::MakeUnique<RtcEventIceCandidatePairConfig>(
+        type, candidate_pair_id, candidate_pair_desc));
+    return;
+  }
+  event_log_->Log(
+      rtc::MakeUnique<RtcEventIceCandidatePair>(type, candidate_pair_id));
+}
+
+void IceEventLog::DumpCandidatePairDescriptionToMemoryAsConfigEvents() const {
+  for (const auto& desc_id_pair : candidate_pair_desc_by_id_) {
+    event_log_->Log(rtc::MakeUnique<RtcEventIceCandidatePairConfig>(
+        IceCandidatePairEventType::kUpdated, desc_id_pair.first,
+        desc_id_pair.second));
+  }
+}
+
+}  // namespace webrtc
diff --git a/logging/rtc_event_log/icelogger.h b/logging/rtc_event_log/icelogger.h
new file mode 100644
index 0000000..b3689c7
--- /dev/null
+++ b/logging/rtc_event_log/icelogger.h
@@ -0,0 +1,51 @@
+/*
+ *  Copyright (c) 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.
+ */
+
+#ifndef LOGGING_RTC_EVENT_LOG_ICELOGGER_H_
+#define LOGGING_RTC_EVENT_LOG_ICELOGGER_H_
+
+#include <unordered_map>
+
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h"
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h"
+
+namespace webrtc {
+
+class RtcEventLog;
+
+// IceEventLog wraps RtcEventLog and provides structural logging of ICE-specific
+// events. The logged events are serialized with other RtcEvent's if protobuf is
+// enabled in the build.
+class IceEventLog {
+ public:
+  IceEventLog();
+  ~IceEventLog();
+  void set_event_log(RtcEventLog* event_log) { event_log_ = event_log; }
+  void LogCandidatePairEvent(
+      IceCandidatePairEventType type,
+      uint32_t candidate_pair_id,
+      const IceCandidatePairDescription& candidate_pair_desc);
+  // This method constructs a config event for each candidate pair with their
+  // description and logs these config events. It is intended to be called when
+  // logging starts to ensure that we have at least one config for each
+  // candidate pair id.
+  void DumpCandidatePairDescriptionToMemoryAsConfigEvents() const;
+
+ private:
+  bool IsIceCandidatePairConfigEvent(IceCandidatePairEventType type);
+
+  RtcEventLog* event_log_ = nullptr;
+  std::unordered_map<uint32_t, IceCandidatePairDescription>
+      candidate_pair_desc_by_id_;
+};
+
+}  // namespace webrtc
+
+#endif  // LOGGING_RTC_EVENT_LOG_ICELOGGER_H_
diff --git a/logging/rtc_event_log/rtc_event_log.proto b/logging/rtc_event_log/rtc_event_log.proto
index 8d2e22a..bfac4f6 100644
--- a/logging/rtc_event_log/rtc_event_log.proto
+++ b/logging/rtc_event_log/rtc_event_log.proto
@@ -41,6 +41,8 @@
     BWE_PROBE_CLUSTER_CREATED_EVENT = 17;
     BWE_PROBE_RESULT_EVENT = 18;
     ALR_STATE_EVENT = 19;
+    ICE_CANDIDATE_PAIR_CONFIG = 20;
+    ICE_CANDIDATE_PAIR_EVENT = 21;
   }
 
   // required - Indicates the type of this event
@@ -85,6 +87,12 @@
 
     // required if type == ALR_STATE_EVENT
     AlrState alr_state = 19;
+
+    // required if type == ICE_CANDIDATE_PAIR_CONFIG
+    IceCandidatePairConfig ice_candidate_pair_config = 20;
+
+    // required if type == ICE_CANDIDATE_PAIR_EVENT
+    IceCandidatePairEvent ice_candidate_pair_event = 21;
   }
 }
 
@@ -323,3 +331,85 @@
   // required - If we are in ALR or not.
   optional bool in_alr = 1;
 }
+
+message IceCandidatePairConfig {
+  enum IceCandidatePairConfigType {
+    ADDED = 0;
+    UPDATED = 1;
+    DESTROYED = 2;
+    SELECTED = 3;
+  }
+
+  enum IceCandidateType {
+    LOCAL = 0;
+    STUN = 1;
+    PRFLX = 2;
+    RELAY = 3;
+    UNKNOWN_CANDIDATE_TYPE = 4;
+  }
+
+  enum Protocol {
+    UDP = 0;
+    TCP = 1;
+    SSLTCP = 2;
+    TLS = 3;
+    UNKNOWN_PROTOCOL = 4;
+  }
+
+  enum AddressFamily {
+    IPV4 = 0;
+    IPV6 = 1;
+    UNKNOWN_ADDRESS_FAMILY = 2;
+  }
+
+  enum NetworkType {
+    ETHERNET = 0;
+    LOOPBACK = 1;
+    WIFI = 2;
+    VPN = 3;
+    CELLULAR = 4;
+    UNKNOWN_NETWORK_TYPE = 5;
+  }
+
+  // required
+  optional IceCandidatePairConfigType config_type = 1;
+
+  // required
+  optional uint32 candidate_pair_id = 2;
+
+  // required
+  optional IceCandidateType local_candidate_type = 3;
+
+  // required
+  optional Protocol local_relay_protocol = 4;
+
+  // required
+  optional NetworkType local_network_type = 5;
+
+  // required
+  optional AddressFamily local_address_family = 6;
+
+  // required
+  optional IceCandidateType remote_candidate_type = 7;
+
+  // required
+  optional AddressFamily remote_address_family = 8;
+
+  // required
+  optional Protocol candidate_pair_protocol = 9;
+}
+
+message IceCandidatePairEvent {
+  enum IceCandidatePairEventType {
+    CHECK_SENT = 0;
+    CHECK_RECEIVED = 1;
+    CHECK_RESPONSE_SENT = 2;
+    CHECK_RESPONSE_RECEIVED = 3;
+  }
+
+  // required
+  optional IceCandidatePairEventType event_type = 1;
+
+  // required
+  optional uint32 candidate_pair_id = 2;
+}
diff --git a/logging/rtc_event_log/rtc_event_log2stats.cc b/logging/rtc_event_log/rtc_event_log2stats.cc
index f38322d..6b0c4c0 100644
--- a/logging/rtc_event_log/rtc_event_log2stats.cc
+++ b/logging/rtc_event_log/rtc_event_log2stats.cc
@@ -166,6 +166,10 @@
       return "BWE_PROBE_RESULT";
     case webrtc::rtclog::Event::ALR_STATE_EVENT:
       return "ALR_STATE_EVENT";
+    case webrtc::rtclog::Event::ICE_CANDIDATE_PAIR_CONFIG:
+      return "ICE_CANDIDATE_PAIR_CONFIG";
+    case webrtc::rtclog::Event::ICE_CANDIDATE_PAIR_EVENT:
+      return "ICE_CANDIDATE_PAIR_EVENT";
   }
   RTC_NOTREACHED();
   return "UNKNOWN_EVENT";
diff --git a/logging/rtc_event_log/rtc_event_log2text.cc b/logging/rtc_event_log/rtc_event_log2text.cc
index 42ff732..c71a2b8 100644
--- a/logging/rtc_event_log/rtc_event_log2text.cc
+++ b/logging/rtc_event_log/rtc_event_log2text.cc
@@ -60,6 +60,7 @@
 DEFINE_bool(playout, true, "Use --noplayout to exclude audio playout events.");
 DEFINE_bool(ana, true, "Use --noana to exclude ANA events.");
 DEFINE_bool(probe, true, "Use --noprobe to exclude probe events.");
+DEFINE_bool(ice, true, "Use --noice to exclude ICE events.");
 
 DEFINE_bool(print_full_packets,
             false,
@@ -796,6 +797,34 @@
         event_recognized = true;
         break;
       }
+
+      case webrtc::ParsedRtcEventLog::ICE_CANDIDATE_PAIR_CONFIG: {
+        if (FLAG_ice) {
+          webrtc::ParsedRtcEventLog::IceCandidatePairConfig ice_cp_config =
+              parsed_stream.GetIceCandidatePairConfig(i);
+          // TODO(qingsi): convert the numeric representation of states to text
+          std::cout << parsed_stream.GetTimestamp(i)
+                    << "\tICE_CANDIDATE_PAIR_CONFIG"
+                    << "\ttype=" << static_cast<int>(ice_cp_config.type)
+                    << std::endl;
+        }
+        event_recognized = true;
+        break;
+      }
+
+      case webrtc::ParsedRtcEventLog::ICE_CANDIDATE_PAIR_EVENT: {
+        if (FLAG_ice) {
+          webrtc::ParsedRtcEventLog::IceCandidatePairEvent ice_cp_event =
+              parsed_stream.GetIceCandidatePairEvent(i);
+          // TODO(qingsi): convert the numeric representation of states to text
+          std::cout << parsed_stream.GetTimestamp(i)
+                    << "\tICE_CANDIDATE_PAIR_EVENT"
+                    << "\ttype=" << static_cast<int>(ice_cp_event.type)
+                    << std::endl;
+        }
+        event_recognized = true;
+        break;
+      }
     }
 
     if (!event_recognized) {
diff --git a/logging/rtc_event_log/rtc_event_log_parser.cc b/logging/rtc_event_log/rtc_event_log_parser.cc
index 194da57..6978928 100644
--- a/logging/rtc_event_log/rtc_event_log_parser.cc
+++ b/logging/rtc_event_log/rtc_event_log_parser.cc
@@ -76,6 +76,10 @@
       return ParsedRtcEventLog::EventType::BWE_PROBE_RESULT_EVENT;
     case rtclog::Event::ALR_STATE_EVENT:
       return ParsedRtcEventLog::EventType::ALR_STATE_EVENT;
+    case rtclog::Event::ICE_CANDIDATE_PAIR_CONFIG:
+      return ParsedRtcEventLog::EventType::ICE_CANDIDATE_PAIR_CONFIG;
+    case rtclog::Event::ICE_CANDIDATE_PAIR_EVENT:
+      return ParsedRtcEventLog::EventType::ICE_CANDIDATE_PAIR_EVENT;
   }
   return ParsedRtcEventLog::EventType::UNKNOWN_EVENT;
 }
@@ -94,6 +98,108 @@
   return BandwidthUsage::kBwNormal;
 }
 
+IceCandidatePairEventType GetRuntimeIceCandidatePairConfigType(
+    rtclog::IceCandidatePairConfig::IceCandidatePairConfigType type) {
+  switch (type) {
+    case rtclog::IceCandidatePairConfig::ADDED:
+      return IceCandidatePairEventType::kAdded;
+    case rtclog::IceCandidatePairConfig::UPDATED:
+      return IceCandidatePairEventType::kUpdated;
+    case rtclog::IceCandidatePairConfig::DESTROYED:
+      return IceCandidatePairEventType::kDestroyed;
+    case rtclog::IceCandidatePairConfig::SELECTED:
+      return IceCandidatePairEventType::kSelected;
+  }
+  RTC_NOTREACHED();
+  return IceCandidatePairEventType::kAdded;
+}
+
+IceCandidateType GetRuntimeIceCandidateType(
+    rtclog::IceCandidatePairConfig::IceCandidateType type) {
+  switch (type) {
+    case rtclog::IceCandidatePairConfig::LOCAL:
+      return IceCandidateType::kLocal;
+    case rtclog::IceCandidatePairConfig::STUN:
+      return IceCandidateType::kStun;
+    case rtclog::IceCandidatePairConfig::PRFLX:
+      return IceCandidateType::kPrflx;
+    case rtclog::IceCandidatePairConfig::RELAY:
+      return IceCandidateType::kRelay;
+    case rtclog::IceCandidatePairConfig::UNKNOWN_CANDIDATE_TYPE:
+      return IceCandidateType::kUnknown;
+  }
+  RTC_NOTREACHED();
+  return IceCandidateType::kUnknown;
+}
+
+IceCandidatePairProtocol GetRuntimeIceCandidatePairProtocol(
+    rtclog::IceCandidatePairConfig::Protocol protocol) {
+  switch (protocol) {
+    case rtclog::IceCandidatePairConfig::UDP:
+      return IceCandidatePairProtocol::kUdp;
+    case rtclog::IceCandidatePairConfig::TCP:
+      return IceCandidatePairProtocol::kTcp;
+    case rtclog::IceCandidatePairConfig::SSLTCP:
+      return IceCandidatePairProtocol::kSsltcp;
+    case rtclog::IceCandidatePairConfig::TLS:
+      return IceCandidatePairProtocol::kTls;
+    case rtclog::IceCandidatePairConfig::UNKNOWN_PROTOCOL:
+      return IceCandidatePairProtocol::kUnknown;
+  }
+  RTC_NOTREACHED();
+  return IceCandidatePairProtocol::kUnknown;
+}
+
+IceCandidatePairAddressFamily GetRuntimeIceCandidatePairAddressFamily(
+    rtclog::IceCandidatePairConfig::AddressFamily address_family) {
+  switch (address_family) {
+    case rtclog::IceCandidatePairConfig::IPV4:
+      return IceCandidatePairAddressFamily::kIpv4;
+    case rtclog::IceCandidatePairConfig::IPV6:
+      return IceCandidatePairAddressFamily::kIpv6;
+    case rtclog::IceCandidatePairConfig::UNKNOWN_ADDRESS_FAMILY:
+      return IceCandidatePairAddressFamily::kUnknown;
+  }
+  RTC_NOTREACHED();
+  return IceCandidatePairAddressFamily::kUnknown;
+}
+
+IceCandidateNetworkType GetRuntimeIceCandidateNetworkType(
+    rtclog::IceCandidatePairConfig::NetworkType network_type) {
+  switch (network_type) {
+    case rtclog::IceCandidatePairConfig::ETHERNET:
+      return IceCandidateNetworkType::kEthernet;
+    case rtclog::IceCandidatePairConfig::LOOPBACK:
+      return IceCandidateNetworkType::kLoopback;
+    case rtclog::IceCandidatePairConfig::WIFI:
+      return IceCandidateNetworkType::kWifi;
+    case rtclog::IceCandidatePairConfig::VPN:
+      return IceCandidateNetworkType::kVpn;
+    case rtclog::IceCandidatePairConfig::CELLULAR:
+      return IceCandidateNetworkType::kCellular;
+    case rtclog::IceCandidatePairConfig::UNKNOWN_NETWORK_TYPE:
+      return IceCandidateNetworkType::kUnknown;
+  }
+  RTC_NOTREACHED();
+  return IceCandidateNetworkType::kUnknown;
+}
+
+IceCandidatePairEventType GetRuntimeIceCandidatePairEventType(
+    rtclog::IceCandidatePairEvent::IceCandidatePairEventType type) {
+  switch (type) {
+    case rtclog::IceCandidatePairEvent::CHECK_SENT:
+      return IceCandidatePairEventType::kCheckSent;
+    case rtclog::IceCandidatePairEvent::CHECK_RECEIVED:
+      return IceCandidatePairEventType::kCheckReceived;
+    case rtclog::IceCandidatePairEvent::CHECK_RESPONSE_SENT:
+      return IceCandidatePairEventType::kCheckResponseSent;
+    case rtclog::IceCandidatePairEvent::CHECK_RESPONSE_RECEIVED:
+      return IceCandidatePairEventType::kCheckResponseReceived;
+  }
+  RTC_NOTREACHED();
+  return IceCandidatePairEventType::kCheckSent;
+}
+
 std::pair<uint64_t, bool> ParseVarInt(std::istream& stream) {
   uint64_t varint = 0;
   for (size_t bytes_read = 0; bytes_read < 10; ++bytes_read) {
@@ -670,6 +776,61 @@
   return res;
 }
 
+ParsedRtcEventLog::IceCandidatePairConfig
+ParsedRtcEventLog::GetIceCandidatePairConfig(size_t index) const {
+  RTC_CHECK_LT(index, GetNumberOfEvents());
+  const rtclog::Event& rtc_event = events_[index];
+  RTC_CHECK(rtc_event.has_type());
+  RTC_CHECK_EQ(rtc_event.type(), rtclog::Event::ICE_CANDIDATE_PAIR_CONFIG);
+  IceCandidatePairConfig res;
+  const rtclog::IceCandidatePairConfig& config =
+      rtc_event.ice_candidate_pair_config();
+  res.timestamp = GetTimestamp(index);
+  RTC_CHECK(config.has_config_type());
+  res.type = GetRuntimeIceCandidatePairConfigType(config.config_type());
+  RTC_CHECK(config.has_candidate_pair_id());
+  res.candidate_pair_id = config.candidate_pair_id();
+  RTC_CHECK(config.has_local_candidate_type());
+  res.local_candidate_type =
+      GetRuntimeIceCandidateType(config.local_candidate_type());
+  RTC_CHECK(config.has_local_relay_protocol());
+  res.local_relay_protocol =
+      GetRuntimeIceCandidatePairProtocol(config.local_relay_protocol());
+  RTC_CHECK(config.has_local_network_type());
+  res.local_network_type =
+      GetRuntimeIceCandidateNetworkType(config.local_network_type());
+  RTC_CHECK(config.has_local_address_family());
+  res.local_address_family =
+      GetRuntimeIceCandidatePairAddressFamily(config.local_address_family());
+  RTC_CHECK(config.has_remote_candidate_type());
+  res.remote_candidate_type =
+      GetRuntimeIceCandidateType(config.remote_candidate_type());
+  RTC_CHECK(config.has_remote_address_family());
+  res.remote_address_family =
+      GetRuntimeIceCandidatePairAddressFamily(config.remote_address_family());
+  RTC_CHECK(config.has_candidate_pair_protocol());
+  res.candidate_pair_protocol =
+      GetRuntimeIceCandidatePairProtocol(config.candidate_pair_protocol());
+  return res;
+}
+
+ParsedRtcEventLog::IceCandidatePairEvent
+ParsedRtcEventLog::GetIceCandidatePairEvent(size_t index) const {
+  RTC_CHECK_LT(index, GetNumberOfEvents());
+  const rtclog::Event& rtc_event = events_[index];
+  RTC_CHECK(rtc_event.has_type());
+  RTC_CHECK_EQ(rtc_event.type(), rtclog::Event::ICE_CANDIDATE_PAIR_EVENT);
+  IceCandidatePairEvent res;
+  const rtclog::IceCandidatePairEvent& event =
+      rtc_event.ice_candidate_pair_event();
+  res.timestamp = GetTimestamp(index);
+  RTC_CHECK(event.has_event_type());
+  res.type = GetRuntimeIceCandidatePairEventType(event.event_type());
+  RTC_CHECK(event.has_candidate_pair_id());
+  res.candidate_pair_id = event.candidate_pair_id();
+  return res;
+}
+
 // Returns the MediaType for registered SSRCs. Search from the end to use last
 // registered types first.
 ParsedRtcEventLog::MediaType ParsedRtcEventLog::GetMediaType(
diff --git a/logging/rtc_event_log/rtc_event_log_parser.h b/logging/rtc_event_log/rtc_event_log_parser.h
index 6438918..d4488bc 100644
--- a/logging/rtc_event_log/rtc_event_log_parser.h
+++ b/logging/rtc_event_log/rtc_event_log_parser.h
@@ -17,6 +17,8 @@
 
 #include "call/video_receive_stream.h"
 #include "call/video_send_stream.h"
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair.h"
+#include "logging/rtc_event_log/events/rtc_event_ice_candidate_pair_config.h"
 #include "logging/rtc_event_log/events/rtc_event_probe_result_failure.h"
 #include "logging/rtc_event_log/rtc_event_log.h"
 #include "logging/rtc_event_log/rtc_stream_config.h"
@@ -70,6 +72,25 @@
     bool in_alr;
   };
 
+  struct IceCandidatePairConfig {
+    uint64_t timestamp;
+    IceCandidatePairEventType type;
+    uint32_t candidate_pair_id;
+    IceCandidateType local_candidate_type;
+    IceCandidatePairProtocol local_relay_protocol;
+    IceCandidateNetworkType local_network_type;
+    IceCandidatePairAddressFamily local_address_family;
+    IceCandidateType remote_candidate_type;
+    IceCandidatePairAddressFamily remote_address_family;
+    IceCandidatePairProtocol candidate_pair_protocol;
+  };
+
+  struct IceCandidatePairEvent {
+    uint64_t timestamp;
+    IceCandidatePairEventType type;
+    uint32_t candidate_pair_id;
+  };
+
   enum EventType {
     UNKNOWN_EVENT = 0,
     LOG_START = 1,
@@ -86,7 +107,9 @@
     AUDIO_NETWORK_ADAPTATION_EVENT = 16,
     BWE_PROBE_CLUSTER_CREATED_EVENT = 17,
     BWE_PROBE_RESULT_EVENT = 18,
-    ALR_STATE_EVENT = 19
+    ALR_STATE_EVENT = 19,
+    ICE_CANDIDATE_PAIR_CONFIG = 20,
+    ICE_CANDIDATE_PAIR_EVENT = 21,
   };
 
   enum class MediaType { ANY, AUDIO, VIDEO, DATA };
@@ -188,6 +211,9 @@
 
   AlrStateEvent GetAlrState(size_t index) const;
 
+  IceCandidatePairConfig GetIceCandidatePairConfig(size_t index) const;
+  IceCandidatePairEvent GetIceCandidatePairEvent(size_t index) const;
+
  private:
   rtclog::StreamConfig GetVideoReceiveConfig(const rtclog::Event& event) const;
   std::vector<rtclog::StreamConfig> GetVideoSendConfig(
diff --git a/rtc_tools/event_log_visualizer/analyzer.cc b/rtc_tools/event_log_visualizer/analyzer.cc
index 3c6a074..2bbb09c 100644
--- a/rtc_tools/event_log_visualizer/analyzer.cc
+++ b/rtc_tools/event_log_visualizer/analyzer.cc
@@ -62,6 +62,8 @@
 
 namespace {
 
+const int kNumMicrosecsPerSec = 1000000;
+
 void SortPacketFeedbackVector(std::vector<PacketFeedback>* vec) {
   auto pred = [](const PacketFeedback& packet_feedback) {
     return packet_feedback.arrival_time_ms == PacketFeedback::kNotReceived;
@@ -88,9 +90,10 @@
 double AbsSendTimeToMicroseconds(int64_t abs_send_time) {
   // The timestamp is a fixed point representation with 6 bits for seconds
   // and 18 bits for fractions of a second. Thus, we divide by 2^18 to get the
-  // time in seconds and then multiply by 1000000 to convert to microseconds.
+  // time in seconds and then multiply by kNumMicrosecsPerSec to convert to
+  // microseconds.
   static constexpr double kTimestampToMicroSec =
-      1000000.0 / static_cast<double>(1ul << 18);
+      static_cast<double>(kNumMicrosecsPerSec) / static_cast<double>(1ul << 18);
   return abs_send_time * kTimestampToMicroSec;
 }
 
@@ -193,7 +196,9 @@
     RTC_LOG(LS_WARNING) << "New capture time " << new_packet.header.timestamp
                         << ", received time " << new_packet.timestamp;
     RTC_LOG(LS_WARNING) << "Receive time difference " << recv_time_diff << " = "
-                        << static_cast<double>(recv_time_diff) / 1000000 << "s";
+                        << static_cast<double>(recv_time_diff) /
+                               kNumMicrosecsPerSec
+                        << "s";
     RTC_LOG(LS_WARNING) << "Send time difference " << send_time_diff << " = "
                         << static_cast<double>(send_time_diff) /
                                kVideoSampleRate
@@ -211,7 +216,8 @@
     uint64_t begin_time,
     TimeSeries* result) {
   for (size_t i = 0; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<float> y = get_y(data[i]);
     if (y)
       result->points.emplace_back(x, *y);
@@ -229,7 +235,8 @@
     uint64_t begin_time,
     TimeSeries* result) {
   for (size_t i = 1; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<ResultType> y = get_y(data[i - 1], data[i]);
     if (y)
       result->points.emplace_back(x, static_cast<float>(*y));
@@ -246,7 +253,8 @@
     TimeSeries* result) {
   ResultType sum = 0;
   for (size_t i = 0; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<ResultType> y = extract(data[i]);
     if (y) {
       sum += *y;
@@ -267,7 +275,8 @@
     TimeSeries* result) {
   ResultType sum = 0;
   for (size_t i = 1; i < data.size(); i++) {
-    float x = static_cast<float>(data[i].timestamp - begin_time) / 1000000;
+    float x = static_cast<float>(data[i].timestamp - begin_time) /
+              kNumMicrosecsPerSec;
     rtc::Optional<ResultType> y = extract(data[i - 1], data[i]);
     if (y)
       sum += *y;
@@ -307,13 +316,118 @@
         sum_in_window -= *value;
       ++window_index_begin;
     }
-    float window_duration_s = static_cast<float>(window_duration_us) / 1000000;
-    float x = static_cast<float>(t - begin_time) / 1000000;
+    float window_duration_s =
+        static_cast<float>(window_duration_us) / kNumMicrosecsPerSec;
+    float x = static_cast<float>(t - begin_time) / kNumMicrosecsPerSec;
     float y = sum_in_window / window_duration_s;
     result->points.emplace_back(x, y);
   }
 }
 
+const char kUnknownEnumValue[] = "unknown";
+
+const char kIceCandidateTypeLocal[] = "local";
+const char kIceCandidateTypeStun[] = "stun";
+const char kIceCandidateTypePrflx[] = "prflx";
+const char kIceCandidateTypeRelay[] = "relay";
+
+const char kProtocolUdp[] = "udp";
+const char kProtocolTcp[] = "tcp";
+const char kProtocolSsltcp[] = "ssltcp";
+const char kProtocolTls[] = "tls";
+
+const char kAddressFamilyIpv4[] = "ipv4";
+const char kAddressFamilyIpv6[] = "ipv6";
+
+const char kNetworkTypeEthernet[] = "ethernet";
+const char kNetworkTypeLoopback[] = "loopback";
+const char kNetworkTypeWifi[] = "wifi";
+const char kNetworkTypeVpn[] = "vpn";
+const char kNetworkTypeCellular[] = "cellular";
+
+std::string GetIceCandidateTypeAsString(webrtc::IceCandidateType type) {
+  switch (type) {
+    case webrtc::IceCandidateType::kLocal:
+      return kIceCandidateTypeLocal;
+    case webrtc::IceCandidateType::kStun:
+      return kIceCandidateTypeStun;
+    case webrtc::IceCandidateType::kPrflx:
+      return kIceCandidateTypePrflx;
+    case webrtc::IceCandidateType::kRelay:
+      return kIceCandidateTypeRelay;
+    default:
+      return kUnknownEnumValue;
+  }
+}
+
+std::string GetProtocolAsString(webrtc::IceCandidatePairProtocol protocol) {
+  switch (protocol) {
+    case webrtc::IceCandidatePairProtocol::kUdp:
+      return kProtocolUdp;
+    case webrtc::IceCandidatePairProtocol::kTcp:
+      return kProtocolTcp;
+    case webrtc::IceCandidatePairProtocol::kSsltcp:
+      return kProtocolSsltcp;
+    case webrtc::IceCandidatePairProtocol::kTls:
+      return kProtocolTls;
+    default:
+      return kUnknownEnumValue;
+  }
+}
+
+std::string GetAddressFamilyAsString(
+    webrtc::IceCandidatePairAddressFamily family) {
+  switch (family) {
+    case webrtc::IceCandidatePairAddressFamily::kIpv4:
+      return kAddressFamilyIpv4;
+    case webrtc::IceCandidatePairAddressFamily::kIpv6:
+      return kAddressFamilyIpv6;
+    default:
+      return kUnknownEnumValue;
+  }
+}
+
+std::string GetNetworkTypeAsString(webrtc::IceCandidateNetworkType type) {
+  switch (type) {
+    case webrtc::IceCandidateNetworkType::kEthernet:
+      return kNetworkTypeEthernet;
+    case webrtc::IceCandidateNetworkType::kLoopback:
+      return kNetworkTypeLoopback;
+    case webrtc::IceCandidateNetworkType::kWifi:
+      return kNetworkTypeWifi;
+    case webrtc::IceCandidateNetworkType::kVpn:
+      return kNetworkTypeVpn;
+    case webrtc::IceCandidateNetworkType::kCellular:
+      return kNetworkTypeCellular;
+    default:
+      return kUnknownEnumValue;
+  }
+}
+
+std::string GetCandidatePairLogDescriptionAsString(
+    const ParsedRtcEventLog::IceCandidatePairConfig& config) {
+  // Example: stun:wifi->relay(tcp):cellular@udp:ipv4
+  // represents a pair of a local server-reflexive candidate on a WiFi network
+  // and a remote relay candidate using TCP as the relay protocol on a cell
+  // network, when the candidate pair communicates over UDP using IPv4.
+  std::stringstream ss;
+  std::string local_candidate_type =
+      GetIceCandidateTypeAsString(config.local_candidate_type);
+  std::string remote_candidate_type =
+      GetIceCandidateTypeAsString(config.remote_candidate_type);
+  if (config.local_candidate_type == webrtc::IceCandidateType::kRelay) {
+    local_candidate_type +=
+        "(" + GetProtocolAsString(config.local_relay_protocol) + ")";
+  }
+  ss << local_candidate_type << ":"
+     << GetNetworkTypeAsString(config.local_network_type) << ":"
+     << GetAddressFamilyAsString(config.local_address_family) << "->"
+     << remote_candidate_type << ":"
+     << GetAddressFamilyAsString(config.remote_address_family) << "@"
+     << GetProtocolAsString(config.candidate_pair_protocol);
+  return ss.str();
+}
+
 }  // namespace
 
 EventLogAnalyzer::EventLogAnalyzer(const ParsedRtcEventLog& log)
@@ -526,6 +640,16 @@
         alr_state_events_.push_back(parsed_log_.GetAlrState(i));
         break;
       }
+      case ParsedRtcEventLog::ICE_CANDIDATE_PAIR_CONFIG: {
+        ice_candidate_pair_configs_.push_back(
+            parsed_log_.GetIceCandidatePairConfig(i));
+        break;
+      }
+      case ParsedRtcEventLog::ICE_CANDIDATE_PAIR_EVENT: {
+        ice_candidate_pair_events_.push_back(
+            parsed_log_.GetIceCandidatePairEvent(i));
+        break;
+      }
       case ParsedRtcEventLog::UNKNOWN_EVENT: {
         break;
       }
@@ -538,7 +662,7 @@
   }
   begin_time_ = first_timestamp;
   end_time_ = last_timestamp;
-  call_duration_s_ = static_cast<float>(end_time_ - begin_time_) / 1000000;
+  call_duration_s_ = ToCallTime(end_time_);
   if (last_log_start) {
     // The log was missing the last LOG_END event. Fake it.
     log_segments_.push_back(std::make_pair(*last_log_start, end_time_));
@@ -625,7 +749,7 @@
     last_rtp_timestamp = unwrapper.Unwrap(packets[i].header.timestamp);
     last_log_timestamp = packets[i].timestamp;
   }
-  if (last_log_timestamp - first_log_timestamp < 1000000) {
+  if (last_log_timestamp - first_log_timestamp < kNumMicrosecsPerSec) {
     RTC_LOG(LS_WARNING)
         << "Failed to estimate RTP clock frequency: Stream too short. ("
         << packets.size() << " packets, "
@@ -633,7 +757,8 @@
     return rtc::nullopt;
   }
   double duration =
-      static_cast<double>(last_log_timestamp - first_log_timestamp) / 1000000;
+      static_cast<double>(last_log_timestamp - first_log_timestamp) /
+      kNumMicrosecsPerSec;
   double estimated_frequency =
       (last_rtp_timestamp - first_rtp_timestamp) / duration;
   for (uint32_t f : {8000, 16000, 32000, 48000, 90000}) {
@@ -648,7 +773,7 @@
 }
 
 float EventLogAnalyzer::ToCallTime(int64_t timestamp) const {
-  return static_cast<float>(timestamp - begin_time_) / 1000000;
+  return static_cast<float>(timestamp - begin_time_) / kNumMicrosecsPerSec;
 }
 
 void EventLogAnalyzer::CreatePacketGraph(PacketDirection desired_direction,
@@ -699,8 +824,7 @@
     std::string label = label_prefix + " " + GetStreamName(stream_id);
     TimeSeries time_series(label, LineStyle::kStep);
     for (size_t i = 0; i < packet_stream.size(); i++) {
-      float x = static_cast<float>(packet_stream[i].timestamp - begin_time_) /
-                1000000;
+      float x = ToCallTime(packet_stream[i].timestamp);
       time_series.points.emplace_back(x, i + 1);
     }
 
@@ -738,7 +862,7 @@
       parsed_log_.GetAudioPlayout(i, &ssrc);
       uint64_t timestamp = parsed_log_.GetTimestamp(i);
       if (MatchingSsrc(ssrc, desired_ssrc_)) {
-        float x = static_cast<float>(timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(timestamp);
         float y = static_cast<float>(timestamp - last_playout[ssrc]) / 1000;
         if (time_series[ssrc].points.size() == 0) {
           // There were no previusly logged playout for this SSRC.
@@ -776,7 +900,7 @@
     //             streams. Tracking bug: webrtc:6399
     for (auto& packet : packet_stream) {
       if (packet.header.extension.hasAudioLevel) {
-        float x = static_cast<float>(packet.timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(packet.timestamp);
         // The audio level is stored in -dBov (so e.g. -10 dBov is stored as 10)
         // Here we convert it to dBov.
         float y = static_cast<float>(-packet.header.extension.audioLevel);
@@ -867,7 +991,7 @@
             std::max(highest_prior_seq_number, sequence_number);
         ++window_index_begin;
       }
-      float x = static_cast<float>(t - begin_time_) / 1000000;
+      float x = ToCallTime(t);
       int64_t expected_packets = highest_seq_number - highest_prior_seq_number;
       if (expected_packets > 0) {
         int64_t received_packets = window_index_end - window_index_begin;
@@ -956,7 +1080,7 @@
   TimeSeries time_series("Fraction lost", LineStyle::kLine,
                          PointStyle::kHighlight);
   for (auto& bwe_update : bwe_loss_updates_) {
-    float x = static_cast<float>(bwe_update.timestamp - begin_time_) / 1000000;
+    float x = ToCallTime(bwe_update.timestamp);
     float y = static_cast<float>(bwe_update.fraction_loss) / 255 * 100;
     time_series.points.emplace_back(x, y);
   }
@@ -1016,8 +1140,8 @@
       ++window_index_begin;
     }
     float window_duration_in_seconds =
-        static_cast<float>(window_duration_) / 1000000;
-    float x = static_cast<float>(time - begin_time_) / 1000000;
+        static_cast<float>(window_duration_) / kNumMicrosecsPerSec;
+    float x = ToCallTime(time);
     float y = bytes_in_window * 8 / window_duration_in_seconds / 1000;
     bitrate_series.points.emplace_back(x, y);
   }
@@ -1027,8 +1151,7 @@
   if (desired_direction == kOutgoingPacket) {
     TimeSeries loss_series("Loss-based estimate", LineStyle::kStep);
     for (auto& loss_update : bwe_loss_updates_) {
-      float x =
-          static_cast<float>(loss_update.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(loss_update.timestamp);
       float y = static_cast<float>(loss_update.new_bitrate) / 1000;
       loss_series.points.emplace_back(x, y);
     }
@@ -1046,8 +1169,7 @@
     BandwidthUsage last_detector_state = BandwidthUsage::kBwNormal;
 
     for (auto& delay_update : bwe_delay_updates_) {
-      float x =
-          static_cast<float>(delay_update.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(delay_update.timestamp);
       float y = static_cast<float>(delay_update.bitrate_bps) / 1000;
 
       if (last_detector_state != delay_update.detector_state) {
@@ -1079,7 +1201,7 @@
     TimeSeries created_series("Probe cluster created.", LineStyle::kNone,
                               PointStyle::kHighlight);
     for (auto& cluster : bwe_probe_cluster_created_events_) {
-      float x = static_cast<float>(cluster.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(cluster.timestamp);
       float y = static_cast<float>(cluster.bitrate_bps) / 1000;
       created_series.points.emplace_back(x, y);
     }
@@ -1088,7 +1210,7 @@
                              PointStyle::kHighlight);
     for (auto& result : bwe_probe_result_events_) {
       if (result.bitrate_bps) {
-        float x = static_cast<float>(result.timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(result.timestamp);
         float y = static_cast<float>(*result.bitrate_bps) / 1000;
         result_series.points.emplace_back(x, y);
       }
@@ -1150,7 +1272,7 @@
   for (const auto& kv : remb_packets) {
     const LoggedRtcpPacket* const rtcp = kv.second;
     const rtcp::Remb* const remb = static_cast<rtcp::Remb*>(rtcp->packet.get());
-    float x = static_cast<float>(rtcp->timestamp - begin_time_) / 1000000;
+    float x = ToCallTime(rtcp->timestamp);
     float y = static_cast<float>(remb->bitrate_bps()) / 1000;
     remb_series.points.emplace_back(x, y);
   }
@@ -1290,8 +1412,7 @@
             acked_bitrate.Update(packet.payload_size, packet.arrival_time_ms);
           bitrate_bps = acked_bitrate.Rate(feedback.back().arrival_time_ms);
         }
-        float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                  1000000;
+        float x = ToCallTime(clock.TimeInMicroseconds());
         float y = bitrate_bps.value_or(0) / 1000;
         acked_time_series.points.emplace_back(x, y);
 #if !(BWE_TEST_LOGGING_COMPILE_TIME_ENABLE)
@@ -1322,8 +1443,7 @@
     if (observer.GetAndResetBitrateUpdated() ||
         time_us - last_update_us >= 1e6) {
       uint32_t y = observer.last_bitrate_bps() / 1000;
-      float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                1000000;
+      float x = ToCallTime(clock.TimeInMicroseconds());
       time_series.points.emplace_back(x, y);
       last_update_us = time_us;
     }
@@ -1396,15 +1516,13 @@
     rtc::Optional<uint32_t> bitrate_bps = acked_bitrate.Rate(arrival_time_ms);
     if (bitrate_bps) {
       uint32_t y = *bitrate_bps / 1000;
-      float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                1000000;
+      float x = ToCallTime(clock.TimeInMicroseconds());
       acked_time_series.points.emplace_back(x, y);
     }
     if (packet_router.GetAndResetBitrateUpdated() ||
         clock.TimeInMicroseconds() - last_update_us >= 1e6) {
       uint32_t y = packet_router.last_bitrate_bps() / 1000;
-      float x = static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-                1000000;
+      float x = ToCallTime(clock.TimeInMicroseconds());
       time_series.points.emplace_back(x, y);
       last_update_us = clock.TimeInMicroseconds();
     }
@@ -1475,9 +1593,7 @@
             feedback_adapter.GetTransportFeedbackVector();
         SortPacketFeedbackVector(&feedback);
         for (const PacketFeedback& packet : feedback) {
-          float x =
-              static_cast<float>(clock.TimeInMicroseconds() - begin_time_) /
-              1000000;
+          float x = ToCallTime(clock.TimeInMicroseconds());
           if (packet.send_time_ms == PacketFeedback::kNoSendTime) {
             late_feedback_series.points.emplace_back(x, prev_y);
             continue;
@@ -1587,7 +1703,7 @@
                                *estimated_frequency * 1000;
       double send_time_ms =
           static_cast<double>(packet.timestamp - first_send_timestamp) / 1000;
-      float x = static_cast<float>(packet.timestamp - begin_time_) / 1000000;
+      float x = ToCallTime(packet.timestamp);
       float y = send_time_ms - capture_time_ms;
       pacer_delay_series.points.emplace_back(x, y);
     }
@@ -1609,7 +1725,7 @@
       TimeSeries timestamp_data(GetStreamName(stream_id) + " capture-time",
                                 LineStyle::kLine, PointStyle::kHighlight);
       for (LoggedRtpPacket packet : rtp_packets) {
-        float x = static_cast<float>(packet.timestamp - begin_time_) / 1000000;
+        float x = ToCallTime(packet.timestamp);
         float y = packet.header.timestamp;
         timestamp_data.points.emplace_back(x, y);
       }
@@ -1628,7 +1744,7 @@
             continue;
           rtcp::SenderReport* sr;
           sr = static_cast<rtcp::SenderReport*>(rtcp.packet.get());
-          float x = static_cast<float>(rtcp.timestamp - begin_time_) / 1000000;
+          float x = ToCallTime(rtcp.timestamp);
           float y = sr->rtp_timestamp();
           timestamp_data.points.emplace_back(x, y);
         }
@@ -1986,6 +2102,81 @@
   plot->SetTitle("NetEq timing");
 }
 
+void EventLogAnalyzer::CreateIceCandidatePairConfigGraph(Plot* plot) {
+  std::map<uint32_t, TimeSeries> configs_by_cp_id;
+  for (const auto& config : ice_candidate_pair_configs_) {
+    if (configs_by_cp_id.find(config.candidate_pair_id) ==
+        configs_by_cp_id.end()) {
+      const std::string candidate_pair_desc =
+          GetCandidatePairLogDescriptionAsString(config);
+      configs_by_cp_id[config.candidate_pair_id] = TimeSeries(
+          candidate_pair_desc, LineStyle::kNone, PointStyle::kHighlight);
+      candidate_pair_desc_by_id_[config.candidate_pair_id] =
+          candidate_pair_desc;
+    }
+    float x = ToCallTime(config.timestamp);
+    float y = static_cast<float>(config.type);
+    configs_by_cp_id[config.candidate_pair_id].points.emplace_back(x, y);
+  }
+
+  // TODO(qingsi): There can be a large number of candidate pairs generated by
+  // certain calls and the frontend cannot render the chart in this case due to
+  // the failure of generating a palette with the same number of colors.
+  for (auto& kv : configs_by_cp_id) {
+    plot->AppendTimeSeries(std::move(kv.second));
+  }
+
+  plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin);
+  plot->SetSuggestedYAxis(0, 3, "Numeric Config Type", kBottomMargin,
+                          kTopMargin);
+  plot->SetTitle("[IceEventLog] ICE candidate pair configs");
+}
+
+std::string EventLogAnalyzer::GetCandidatePairLogDescriptionFromId(
+    uint32_t candidate_pair_id) {
+  if (candidate_pair_desc_by_id_.find(candidate_pair_id) !=
+      candidate_pair_desc_by_id_.end()) {
+    return candidate_pair_desc_by_id_[candidate_pair_id];
+  }
+  for (const auto& config : ice_candidate_pair_configs_) {
+    // TODO(qingsi): Add the handling of the "Updated" config event after the
+    // visualization of property change for candidate pairs is introduced.
+    if (candidate_pair_desc_by_id_.find(config.candidate_pair_id) ==
+        candidate_pair_desc_by_id_.end()) {
+      const std::string candidate_pair_desc =
+          GetCandidatePairLogDescriptionAsString(config);
+      candidate_pair_desc_by_id_[config.candidate_pair_id] =
+          candidate_pair_desc;
+    }
+  }
+  return candidate_pair_desc_by_id_[candidate_pair_id];
+}
+
+void EventLogAnalyzer::CreateIceConnectivityCheckGraph(Plot* plot) {
+  std::map<uint32_t, TimeSeries> checks_by_cp_id;
+  for (const auto& event : ice_candidate_pair_events_) {
+    if (checks_by_cp_id.find(event.candidate_pair_id) ==
+        checks_by_cp_id.end()) {
+      checks_by_cp_id[event.candidate_pair_id] = TimeSeries(
+          GetCandidatePairLogDescriptionFromId(event.candidate_pair_id),
+          LineStyle::kNone, PointStyle::kHighlight);
+    }
+    float x = ToCallTime(event.timestamp);
+    float y = static_cast<float>(event.type);
+    checks_by_cp_id[event.candidate_pair_id].points.emplace_back(x, y);
+  }
+
+  // TODO(qingsi): The same issue as in CreateIceCandidatePairConfigGraph.
+  for (auto& kv : checks_by_cp_id) {
+    plot->AppendTimeSeries(std::move(kv.second));
+  }
+
+  plot->SetXAxis(0, call_duration_s_, "Time (s)", kLeftMargin, kRightMargin);
+  plot->SetSuggestedYAxis(0, 4, "Numeric Connectivity State", kBottomMargin,
+                          kTopMargin);
+  plot->SetTitle("[IceEventLog] ICE connectivity checks");
+}
+
 void EventLogAnalyzer::Notification(
     std::unique_ptr<TriageNotification> notification) {
   notifications_.push_back(std::move(notification));
diff --git a/rtc_tools/event_log_visualizer/analyzer.h b/rtc_tools/event_log_visualizer/analyzer.h
index 5d0faab..fafce66 100644
--- a/rtc_tools/event_log_visualizer/analyzer.h
+++ b/rtc_tools/event_log_visualizer/analyzer.h
@@ -109,6 +109,9 @@
                                     int file_sample_rate_hz,
                                     Plot* plot);
 
+  void CreateIceCandidatePairConfigGraph(Plot* plot);
+  void CreateIceConnectivityCheckGraph(Plot* plot);
+
   // Returns a vector of capture and arrival timestamps for the video frames
   // of the stream with the most number of frames.
   std::vector<std::pair<int64_t, int64_t>> GetFrameTimestamps() const;
@@ -159,6 +162,8 @@
 
   void Notification(std::unique_ptr<TriageNotification> notification);
 
+  std::string GetCandidatePairLogDescriptionFromId(uint32_t candidate_pair_id);
+
   const ParsedRtcEventLog& parsed_log_;
 
   // A list of SSRCs we are interested in analysing.
@@ -204,6 +209,14 @@
 
   std::vector<ParsedRtcEventLog::AlrStateEvent> alr_state_events_;
 
+  std::vector<ParsedRtcEventLog::IceCandidatePairConfig>
+      ice_candidate_pair_configs_;
+
+  std::vector<ParsedRtcEventLog::IceCandidatePairEvent>
+      ice_candidate_pair_events_;
+
+  std::map<uint32_t, std::string> candidate_pair_desc_by_id_;
+
   // Window and step size used for calculating moving averages, e.g. bitrate.
   // The generated data points will be |step_| microseconds apart.
   // Only events occuring at most |window_duration_| microseconds before the
diff --git a/rtc_tools/event_log_visualizer/main.cc b/rtc_tools/event_log_visualizer/main.cc
index 4a6ec64..2e7a79e 100644
--- a/rtc_tools/event_log_visualizer/main.cc
+++ b/rtc_tools/event_log_visualizer/main.cc
@@ -115,6 +115,12 @@
 DEFINE_bool(plot_audio_jitter_buffer,
             false,
             "Plot the audio jitter buffer delay profile.");
+DEFINE_bool(plot_ice_candidate_pair_config,
+            false,
+            "Plot the ICE candidate pair config events.");
+DEFINE_bool(plot_ice_connectivity_check,
+            false,
+            "Plot the ICE candidate pair connectivity checks.");
 
 DEFINE_string(
     force_fieldtrials,
@@ -318,6 +324,13 @@
                                           collection->AppendNewPlot());
   }
 
+  if (FLAG_plot_ice_candidate_pair_config) {
+    analyzer.CreateIceCandidatePairConfigGraph(collection->AppendNewPlot());
+  }
+  if (FLAG_plot_ice_connectivity_check) {
+    analyzer.CreateIceConnectivityCheckGraph(collection->AppendNewPlot());
+  }
+
   collection->Draw();
 
   if (FLAG_print_triage_notifications) {
@@ -356,4 +369,6 @@
   FLAG_plot_audio_encoder_dtx = setting;
   FLAG_plot_audio_encoder_num_channels = setting;
   FLAG_plot_audio_jitter_buffer = setting;
+  FLAG_plot_ice_candidate_pair_config = setting;
+  FLAG_plot_ice_connectivity_check = setting;
 }