Implement QualityLimitationReasonTracker and expose "reason".

This CL implements the logic behind qualityLimitationReason[1] and
qualityLimitationDurations[2]

This CL also exposes qualityLimitationReason in the standard getStats()
API, but does not expose qualityLimitationDurations because that is
blocked on supporting the "record<>" type in RTCStatsMember[3].

[1] https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason
[2] https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations
[3] https://crbug.com/webrtc/10685

TBR=stefan@webrtc.org

Bug: webrtc:10451, webrtc:10686
Change-Id: Ifff0be4ddd64eaec23d59c02af99fdbb1feb3841
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/138825
Commit-Queue: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Åsa Persson <asapersson@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#28090}
diff --git a/api/stats/rtcstats_objects.h b/api/stats/rtcstats_objects.h
index 64bfa19..ff99df5 100644
--- a/api/stats/rtcstats_objects.h
+++ b/api/stats/rtcstats_objects.h
@@ -74,6 +74,14 @@
   static const char* const kUnknown;
 };
 
+// https://w3c.github.io/webrtc-stats/#dom-rtcqualitylimitationreason
+struct RTCQualityLimitationReason {
+  static const char* const kNone;
+  static const char* const kCpu;
+  static const char* const kBandwidth;
+  static const char* const kOther;
+};
+
 // https://webrtc.org/experiments/rtp-hdrext/video-content-type/
 struct RTCContentType {
   static const char* const kUnspecified;
@@ -464,6 +472,11 @@
   // TODO(https://crbug.com/webrtc/10635): This is only implemented for video;
   // implement it for audio as well.
   RTCStatsMember<double> total_packet_send_delay;
+  // Enum type RTCQualityLimitationReason
+  // TODO(https://crbug.com/webrtc/10686): Also expose
+  // qualityLimitationDurations. Requires RTCStatsMember support for
+  // "record<DOMString, double>", see https://crbug.com/webrtc/10685.
+  RTCStatsMember<std::string> quality_limitation_reason;
   // https://henbos.github.io/webrtc-provisional-stats/#dom-rtcoutboundrtpstreamstats-contenttype
   RTCStatsMember<std::string> content_type;
 };
diff --git a/call/video_send_stream.h b/call/video_send_stream.h
index 929aa88..850996e 100644
--- a/call/video_send_stream.h
+++ b/call/video_send_stream.h
@@ -28,6 +28,7 @@
 #include "api/video/video_stream_encoder_settings.h"
 #include "api/video_codecs/video_encoder_config.h"
 #include "call/rtp_config.h"
+#include "common_video/include/quality_limitation_reason.h"
 #include "modules/rtp_rtcp/include/report_block_data.h"
 #include "modules/rtp_rtcp/include/rtcp_statistics.h"
 #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h"
@@ -92,6 +93,11 @@
     bool cpu_limited_resolution = false;
     bool bw_limited_framerate = false;
     bool cpu_limited_framerate = false;
+    // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason
+    QualityLimitationReason quality_limitation_reason =
+        QualityLimitationReason::kNone;
+    // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations
+    std::map<QualityLimitationReason, int64_t> quality_limitation_durations_ms;
     // Total number of times resolution as been requested to be changed due to
     // CPU/quality adaptation.
     int number_of_cpu_adapt_changes = 0;
diff --git a/common_video/BUILD.gn b/common_video/BUILD.gn
index 53bdea0..10a646f 100644
--- a/common_video/BUILD.gn
+++ b/common_video/BUILD.gn
@@ -28,6 +28,7 @@
     "include/bitrate_adjuster.h",
     "include/i420_buffer_pool.h",
     "include/incoming_video_stream.h",
+    "include/quality_limitation_reason.h",
     "include/video_frame.h",
     "include/video_frame_buffer.h",
     "incoming_video_stream.cc",
diff --git a/common_video/include/quality_limitation_reason.h b/common_video/include/quality_limitation_reason.h
new file mode 100644
index 0000000..068136a
--- /dev/null
+++ b/common_video/include/quality_limitation_reason.h
@@ -0,0 +1,26 @@
+/*
+ *  Copyright 2019 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 COMMON_VIDEO_INCLUDE_QUALITY_LIMITATION_REASON_H_
+#define COMMON_VIDEO_INCLUDE_QUALITY_LIMITATION_REASON_H_
+
+namespace webrtc {
+
+// https://w3c.github.io/webrtc-stats/#rtcqualitylimitationreason-enum
+enum class QualityLimitationReason {
+  kNone,
+  kCpu,
+  kBandwidth,
+  kOther,
+};
+
+}  // namespace webrtc
+
+#endif  // COMMON_VIDEO_INCLUDE_QUALITY_LIMITATION_REASON_H_
diff --git a/media/base/media_channel.h b/media/base/media_channel.h
index 3b9a54c..c991de3 100644
--- a/media/base/media_channel.h
+++ b/media/base/media_channel.h
@@ -31,6 +31,7 @@
 #include "api/video/video_source_interface.h"
 #include "api/video/video_timing.h"
 #include "api/video_codecs/video_encoder_config.h"
+#include "common_video/include/quality_limitation_reason.h"
 #include "media/base/codec.h"
 #include "media/base/delayable.h"
 #include "media/base/media_config.h"
@@ -552,6 +553,12 @@
   int nominal_bitrate = 0;
   int adapt_reason = 0;
   int adapt_changes = 0;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason
+  webrtc::QualityLimitationReason quality_limitation_reason =
+      webrtc::QualityLimitationReason::kNone;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations
+  std::map<webrtc::QualityLimitationReason, int64_t>
+      quality_limitation_durations_ms;
   int avg_encode_ms = 0;
   int encode_usage_percent = 0;
   uint32_t frames_encoded = 0;
diff --git a/media/engine/webrtc_video_engine.cc b/media/engine/webrtc_video_engine.cc
index 5950204..92ed38d 100644
--- a/media/engine/webrtc_video_engine.cc
+++ b/media/engine/webrtc_video_engine.cc
@@ -2267,6 +2267,8 @@
   if (stats.bw_limited_resolution)
     info.adapt_reason |= ADAPTREASON_BANDWIDTH;
 
+  info.quality_limitation_reason = stats.quality_limitation_reason;
+  info.quality_limitation_durations_ms = stats.quality_limitation_durations_ms;
   info.encoder_implementation_name = stats.encoder_implementation_name;
   info.ssrc_groups = ssrc_groups_;
   info.framerate_input = stats.input_frame_rate;
diff --git a/pc/rtc_stats_collector.cc b/pc/rtc_stats_collector.cc
index db7ca61..4386579 100644
--- a/pc/rtc_stats_collector.cc
+++ b/pc/rtc_stats_collector.cc
@@ -199,6 +199,20 @@
   return nullptr;
 }
 
+const char* QualityLimitationReasonToRTCQualityLimitationReason(
+    QualityLimitationReason reason) {
+  switch (reason) {
+    case QualityLimitationReason::kNone:
+      return RTCQualityLimitationReason::kNone;
+    case QualityLimitationReason::kCpu:
+      return RTCQualityLimitationReason::kCpu;
+    case QualityLimitationReason::kBandwidth:
+      return RTCQualityLimitationReason::kBandwidth;
+    case QualityLimitationReason::kOther:
+      return RTCQualityLimitationReason::kOther;
+  }
+}
+
 double DoubleAudioLevelFromIntAudioLevel(int audio_level) {
   RTC_DCHECK_GE(audio_level, 0);
   RTC_DCHECK_LE(audio_level, 32767);
@@ -375,6 +389,9 @@
   outbound_video->total_packet_send_delay =
       static_cast<double>(video_sender_info.total_packet_send_delay_ms) /
       rtc::kNumMillisecsPerSec;
+  outbound_video->quality_limitation_reason =
+      QualityLimitationReasonToRTCQualityLimitationReason(
+          video_sender_info.quality_limitation_reason);
   // TODO(https://crbug.com/webrtc/10529): When info's |content_info| is
   // optional, support the "unspecified" value.
   if (video_sender_info.content_type == VideoContentType::SCREENSHARE)
diff --git a/pc/rtc_stats_collector_unittest.cc b/pc/rtc_stats_collector_unittest.cc
index c8876bf..51e6eb9 100644
--- a/pc/rtc_stats_collector_unittest.cc
+++ b/pc/rtc_stats_collector_unittest.cc
@@ -1941,6 +1941,8 @@
   video_media_info.senders[0].total_encode_time_ms = 9000;
   video_media_info.senders[0].total_encoded_bytes_target = 1234;
   video_media_info.senders[0].total_packet_send_delay_ms = 10000;
+  video_media_info.senders[0].quality_limitation_reason =
+      QualityLimitationReason::kBandwidth;
   video_media_info.senders[0].qp_sum = absl::nullopt;
   video_media_info.senders[0].content_type = VideoContentType::UNSPECIFIED;
 
@@ -1986,6 +1988,7 @@
   expected_video.total_encode_time = 9.0;
   expected_video.total_encoded_bytes_target = 1234;
   expected_video.total_packet_send_delay = 10.0;
+  expected_video.quality_limitation_reason = "bandwidth";
   // |expected_video.content_type| should be undefined.
   // |expected_video.qp_sum| should be undefined.
   ASSERT_TRUE(report->Get(expected_video.id()));
diff --git a/pc/rtc_stats_integrationtest.cc b/pc/rtc_stats_integrationtest.cc
index df1d58d..36518c9 100644
--- a/pc/rtc_stats_integrationtest.cc
+++ b/pc/rtc_stats_integrationtest.cc
@@ -834,6 +834,7 @@
           outbound_stream.total_encoded_bytes_target);
       verifier.TestMemberIsNonNegative<double>(
           outbound_stream.total_packet_send_delay);
+      verifier.TestMemberIsDefined(outbound_stream.quality_limitation_reason);
       // The integration test is not set up to test screen share; don't require
       // this to be present.
       verifier.MarkMemberTested(outbound_stream.content_type, true);
@@ -844,6 +845,7 @@
           outbound_stream.total_encoded_bytes_target);
       // TODO(https://crbug.com/webrtc/10635): Implement for audio as well.
       verifier.TestMemberIsUndefined(outbound_stream.total_packet_send_delay);
+      verifier.TestMemberIsUndefined(outbound_stream.quality_limitation_reason);
       verifier.TestMemberIsUndefined(outbound_stream.content_type);
     }
     return verifier.ExpectAllMembersSuccessfullyTested();
diff --git a/stats/rtcstats_objects.cc b/stats/rtcstats_objects.cc
index ec2f6e8..8098707 100644
--- a/stats/rtcstats_objects.cc
+++ b/stats/rtcstats_objects.cc
@@ -53,6 +53,12 @@
 const char* const RTCNetworkType::kVpn = "vpn";
 const char* const RTCNetworkType::kUnknown = "unknown";
 
+// https://w3c.github.io/webrtc-stats/#dom-rtcqualitylimitationreason
+const char* const RTCQualityLimitationReason::kNone = "none";
+const char* const RTCQualityLimitationReason::kCpu = "cpu";
+const char* const RTCQualityLimitationReason::kBandwidth = "bandwidth";
+const char* const RTCQualityLimitationReason::kOther = "other";
+
 // https://webrtc.org/experiments/rtp-hdrext/video-content-type/
 const char* const RTCContentType::kUnspecified = "unspecified";
 const char* const RTCContentType::kScreenshare = "screenshare";
@@ -681,6 +687,7 @@
     &total_encode_time,
     &total_encoded_bytes_target,
     &total_packet_send_delay,
+    &quality_limitation_reason,
     &content_type)
 // clang-format on
 
@@ -701,6 +708,7 @@
       total_encode_time("totalEncodeTime"),
       total_encoded_bytes_target("totalEncodedBytesTarget"),
       total_packet_send_delay("totalPacketSendDelay"),
+      quality_limitation_reason("qualityLimitationReason"),
       content_type("contentType") {}
 
 RTCOutboundRTPStreamStats::RTCOutboundRTPStreamStats(
@@ -716,6 +724,7 @@
       total_encode_time(other.total_encode_time),
       total_encoded_bytes_target(other.total_encoded_bytes_target),
       total_packet_send_delay(other.total_packet_send_delay),
+      quality_limitation_reason(other.quality_limitation_reason),
       content_type(other.content_type) {}
 
 RTCOutboundRTPStreamStats::~RTCOutboundRTPStreamStats() {}
diff --git a/video/BUILD.gn b/video/BUILD.gn
index e15b15e..29e1e45 100644
--- a/video/BUILD.gn
+++ b/video/BUILD.gn
@@ -16,6 +16,8 @@
     "call_stats.h",
     "encoder_rtcp_feedback.cc",
     "encoder_rtcp_feedback.h",
+    "quality_limitation_reason_tracker.cc",
+    "quality_limitation_reason_tracker.h",
     "quality_threshold.cc",
     "quality_threshold.h",
     "receive_statistics_proxy.cc",
@@ -520,6 +522,7 @@
       "frame_encode_metadata_writer_unittest.cc",
       "overuse_frame_detector_unittest.cc",
       "picture_id_tests.cc",
+      "quality_limitation_reason_tracker_unittest.cc",
       "quality_scaling_tests.cc",
       "quality_threshold_unittest.cc",
       "receive_statistics_proxy_unittest.cc",
diff --git a/video/quality_limitation_reason_tracker.cc b/video/quality_limitation_reason_tracker.cc
new file mode 100644
index 0000000..c2b2cc4
--- /dev/null
+++ b/video/quality_limitation_reason_tracker.cc
@@ -0,0 +1,52 @@
+/*
+ *  Copyright 2019 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 "video/quality_limitation_reason_tracker.h"
+
+#include <utility>
+
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+
+QualityLimitationReasonTracker::QualityLimitationReasonTracker(Clock* clock)
+    : clock_(clock),
+      current_reason_(QualityLimitationReason::kNone),
+      current_reason_updated_timestamp_ms_(clock_->TimeInMilliseconds()),
+      durations_ms_({std::make_pair(QualityLimitationReason::kNone, 0),
+                     std::make_pair(QualityLimitationReason::kCpu, 0),
+                     std::make_pair(QualityLimitationReason::kBandwidth, 0),
+                     std::make_pair(QualityLimitationReason::kOther, 0)}) {}
+
+QualityLimitationReason QualityLimitationReasonTracker::current_reason() const {
+  return current_reason_;
+}
+
+void QualityLimitationReasonTracker::SetReason(QualityLimitationReason reason) {
+  if (reason == current_reason_)
+    return;
+  int64_t now_ms = clock_->TimeInMilliseconds();
+  durations_ms_[current_reason_] +=
+      now_ms - current_reason_updated_timestamp_ms_;
+  current_reason_ = reason;
+  current_reason_updated_timestamp_ms_ = now_ms;
+}
+
+std::map<QualityLimitationReason, int64_t>
+QualityLimitationReasonTracker::DurationsMs() const {
+  std::map<QualityLimitationReason, int64_t> total_durations_ms = durations_ms_;
+  auto it = total_durations_ms.find(current_reason_);
+  RTC_DCHECK(it != total_durations_ms.end());
+  it->second +=
+      clock_->TimeInMilliseconds() - current_reason_updated_timestamp_ms_;
+  return total_durations_ms;
+}
+
+}  // namespace webrtc
diff --git a/video/quality_limitation_reason_tracker.h b/video/quality_limitation_reason_tracker.h
new file mode 100644
index 0000000..bd01899
--- /dev/null
+++ b/video/quality_limitation_reason_tracker.h
@@ -0,0 +1,53 @@
+/*
+ *  Copyright 2019 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 VIDEO_QUALITY_LIMITATION_REASON_TRACKER_H_
+#define VIDEO_QUALITY_LIMITATION_REASON_TRACKER_H_
+
+#include <map>
+
+#include "common_video/include/quality_limitation_reason.h"
+#include "system_wrappers/include/clock.h"
+
+namespace webrtc {
+
+// A tracker of quality limitation reasons. The quality limitation reason is the
+// primary reason for limiting resolution and/or framerate (such as CPU or
+// bandwidth limitations). The tracker keeps track of the current reason and the
+// duration of time spent in each reason. See qualityLimitationReason[1] and
+// qualityLimitationDurations[2] in the webrtc-stats spec.
+// [1]
+// https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationreason
+// [2]
+// https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-qualitylimitationdurations
+class QualityLimitationReasonTracker {
+ public:
+  // The caller is responsible for making sure |clock| outlives the tracker.
+  explicit QualityLimitationReasonTracker(Clock* clock);
+
+  // The current reason defaults to QualityLimitationReason::kNone.
+  QualityLimitationReason current_reason() const;
+  void SetReason(QualityLimitationReason reason);
+  std::map<QualityLimitationReason, int64_t> DurationsMs() const;
+
+ private:
+  Clock* const clock_;
+  QualityLimitationReason current_reason_;
+  int64_t current_reason_updated_timestamp_ms_;
+  // The total amount of time spent in each reason at time
+  // |current_reason_updated_timestamp_ms_|. To get the total amount duration
+  // so-far, including the time spent in |current_reason_| elapsed since the
+  // last time |current_reason_| was updated, see DurationsMs().
+  std::map<QualityLimitationReason, int64_t> durations_ms_;
+};
+
+}  // namespace webrtc
+
+#endif  // VIDEO_QUALITY_LIMITATION_REASON_TRACKER_H_
diff --git a/video/quality_limitation_reason_tracker_unittest.cc b/video/quality_limitation_reason_tracker_unittest.cc
new file mode 100644
index 0000000..9756b36
--- /dev/null
+++ b/video/quality_limitation_reason_tracker_unittest.cc
@@ -0,0 +1,115 @@
+/*
+ *  Copyright 2019 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 "video/quality_limitation_reason_tracker.h"
+
+#include "common_video/include/quality_limitation_reason.h"
+#include "system_wrappers/include/clock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+
+class QualityLimitationReasonTrackerTest : public ::testing::Test {
+ public:
+  QualityLimitationReasonTrackerTest()
+      : fake_clock_(1234), tracker_(&fake_clock_) {}
+
+ protected:
+  SimulatedClock fake_clock_;
+  QualityLimitationReasonTracker tracker_;
+};
+
+TEST_F(QualityLimitationReasonTrackerTest, DefaultValues) {
+  EXPECT_EQ(QualityLimitationReason::kNone, tracker_.current_reason());
+  auto durations_ms = tracker_.DurationsMs();
+  EXPECT_EQ(4u, durations_ms.size());
+  EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kNone)->second);
+  EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kCpu)->second);
+  EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kBandwidth)->second);
+  EXPECT_EQ(0, durations_ms.find(QualityLimitationReason::kOther)->second);
+}
+
+TEST_F(QualityLimitationReasonTrackerTest, NoneDurationIncreasesByDefault) {
+  int64_t initial_duration_ms =
+      tracker_.DurationsMs()[QualityLimitationReason::kNone];
+  fake_clock_.AdvanceTimeMilliseconds(9999);
+  EXPECT_EQ(initial_duration_ms + 9999,
+            tracker_.DurationsMs()[QualityLimitationReason::kNone]);
+}
+
+TEST_F(QualityLimitationReasonTrackerTest,
+       RememberDurationAfterSwitchingReason) {
+  tracker_.SetReason(QualityLimitationReason::kCpu);
+  int64_t initial_duration_ms =
+      tracker_.DurationsMs()[QualityLimitationReason::kCpu];
+  fake_clock_.AdvanceTimeMilliseconds(50);
+  tracker_.SetReason(QualityLimitationReason::kOther);
+  fake_clock_.AdvanceTimeMilliseconds(50);
+  EXPECT_EQ(initial_duration_ms + 50,
+            tracker_.DurationsMs()[QualityLimitationReason::kCpu]);
+}
+
+class QualityLimitationReasonTrackerTestWithParamReason
+    : public QualityLimitationReasonTrackerTest,
+      public ::testing::WithParamInterface<QualityLimitationReason> {
+ public:
+  QualityLimitationReasonTrackerTestWithParamReason()
+      : reason_(GetParam()),
+        different_reason_(reason_ != QualityLimitationReason::kCpu
+                              ? QualityLimitationReason::kCpu
+                              : QualityLimitationReason::kOther) {}
+
+ protected:
+  QualityLimitationReason reason_;
+  QualityLimitationReason different_reason_;
+};
+
+TEST_P(QualityLimitationReasonTrackerTestWithParamReason,
+       DurationIncreasesOverTime) {
+  int64_t initial_duration_ms = tracker_.DurationsMs()[reason_];
+  tracker_.SetReason(reason_);
+  EXPECT_EQ(initial_duration_ms, tracker_.DurationsMs()[reason_]);
+  fake_clock_.AdvanceTimeMilliseconds(4321);
+  EXPECT_EQ(initial_duration_ms + 4321, tracker_.DurationsMs()[reason_]);
+}
+
+TEST_P(QualityLimitationReasonTrackerTestWithParamReason,
+       SwitchBetweenReasonsBackAndForth) {
+  int64_t initial_duration_ms = tracker_.DurationsMs()[reason_];
+  // Spend 100 ms in |different_reason_|.
+  tracker_.SetReason(different_reason_);
+  fake_clock_.AdvanceTimeMilliseconds(100);
+  EXPECT_EQ(initial_duration_ms, tracker_.DurationsMs()[reason_]);
+  // Spend 50 ms in |reason_|.
+  tracker_.SetReason(reason_);
+  fake_clock_.AdvanceTimeMilliseconds(50);
+  EXPECT_EQ(initial_duration_ms + 50, tracker_.DurationsMs()[reason_]);
+  // Spend another 1000 ms in |different_reason_|.
+  tracker_.SetReason(different_reason_);
+  fake_clock_.AdvanceTimeMilliseconds(1000);
+  EXPECT_EQ(initial_duration_ms + 50, tracker_.DurationsMs()[reason_]);
+  // Spend another 100 ms in |reason_|.
+  tracker_.SetReason(reason_);
+  fake_clock_.AdvanceTimeMilliseconds(100);
+  EXPECT_EQ(initial_duration_ms + 150, tracker_.DurationsMs()[reason_]);
+  // Change reason one last time without advancing time.
+  tracker_.SetReason(different_reason_);
+  EXPECT_EQ(initial_duration_ms + 150, tracker_.DurationsMs()[reason_]);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ,
+    QualityLimitationReasonTrackerTestWithParamReason,
+    ::testing::Values(QualityLimitationReason::kNone,       // "/0"
+                      QualityLimitationReason::kCpu,        // "/1"
+                      QualityLimitationReason::kBandwidth,  // "/2"
+                      QualityLimitationReason::kOther));    // "/3"
+
+}  // namespace webrtc
diff --git a/video/send_statistics_proxy.cc b/video/send_statistics_proxy.cc
index 0313316..cf417f5 100644
--- a/video/send_statistics_proxy.cc
+++ b/video/send_statistics_proxy.cc
@@ -137,6 +137,7 @@
       encode_time_(kEncodeTimeWeigthFactor),
       quality_downscales_(-1),
       cpu_downscales_(-1),
+      quality_limitation_reason_tracker_(clock_),
       media_byte_rate_tracker_(kBucketSizeMs, kBucketCount),
       encoded_frame_rate_tracker_(kBucketSizeMs, kBucketCount),
       uma_container_(
@@ -729,6 +730,8 @@
           : VideoContentType::SCREENSHARE;
   stats_.encode_frame_rate = round(encoded_frame_rate_tracker_.ComputeRate());
   stats_.media_bitrate_bps = media_byte_rate_tracker_.ComputeRate() * 8;
+  stats_.quality_limitation_durations_ms =
+      quality_limitation_reason_tracker_.DurationsMs();
   return stats_;
 }
 
@@ -1062,6 +1065,26 @@
       ++stats_.number_of_quality_adapt_changes;
       break;
   }
+
+  bool is_cpu_limited = cpu_counts.num_resolution_reductions > 0 ||
+                        cpu_counts.num_framerate_reductions > 0;
+  bool is_bandwidth_limited = quality_counts.num_resolution_reductions > 0 ||
+                              quality_counts.num_framerate_reductions > 0;
+  if (is_bandwidth_limited) {
+    // We may be both CPU limited and bandwidth limited at the same time but
+    // there is no way to express this in standardized stats. Heuristically,
+    // bandwidth is more likely to be a limiting factor than CPU, and more
+    // likely to vary over time, so only when we aren't bandwidth limited do we
+    // want to know about our CPU being the bottleneck.
+    quality_limitation_reason_tracker_.SetReason(
+        QualityLimitationReason::kBandwidth);
+  } else if (is_cpu_limited) {
+    quality_limitation_reason_tracker_.SetReason(QualityLimitationReason::kCpu);
+  } else {
+    quality_limitation_reason_tracker_.SetReason(
+        QualityLimitationReason::kNone);
+  }
+
   UpdateAdaptationStats(cpu_counts, quality_counts);
 }
 
@@ -1075,6 +1098,10 @@
   stats_.cpu_limited_framerate = cpu_counts.num_framerate_reductions > 0;
   stats_.bw_limited_resolution = quality_counts.num_resolution_reductions > 0;
   stats_.bw_limited_framerate = quality_counts.num_framerate_reductions > 0;
+  stats_.quality_limitation_reason =
+      quality_limitation_reason_tracker_.current_reason();
+  // |stats_.quality_limitation_durations_ms| depends on the current time
+  // when it is polled; it is updated in SendStatisticsProxy::GetStats().
 }
 
 // TODO(asapersson): Include fps changes.
diff --git a/video/send_statistics_proxy.h b/video/send_statistics_proxy.h
index 51d5b2f..30e8f8b 100644
--- a/video/send_statistics_proxy.h
+++ b/video/send_statistics_proxy.h
@@ -26,6 +26,7 @@
 #include "rtc_base/rate_tracker.h"
 #include "rtc_base/thread_annotations.h"
 #include "system_wrappers/include/clock.h"
+#include "video/quality_limitation_reason_tracker.h"
 #include "video/report_block_stats.h"
 #include "video/stats_counter.h"
 
@@ -244,6 +245,8 @@
   rtc::ExpFilter encode_time_ RTC_GUARDED_BY(crit_);
   int quality_downscales_ RTC_GUARDED_BY(crit_);
   int cpu_downscales_ RTC_GUARDED_BY(crit_);
+  QualityLimitationReasonTracker quality_limitation_reason_tracker_
+      RTC_GUARDED_BY(crit_);
   rtc::RateTracker media_byte_rate_tracker_ RTC_GUARDED_BY(crit_);
   rtc::RateTracker encoded_frame_rate_tracker_ RTC_GUARDED_BY(crit_);
 
diff --git a/video/send_statistics_proxy_unittest.cc b/video/send_statistics_proxy_unittest.cc
index 58514e5..928bc8b 100644
--- a/video/send_statistics_proxy_unittest.cc
+++ b/video/send_statistics_proxy_unittest.cc
@@ -1067,6 +1067,145 @@
                    "WebRTC.Video.Screenshare.AdaptChangesPerMinute.Quality"));
 }
 
+TEST_F(SendStatisticsProxyTest,
+       QualityLimitationReasonIsCpuWhenCpuIsResolutionLimited) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  cpu_counts.num_resolution_reductions = 1;
+
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts,
+      quality_counts);
+
+  EXPECT_EQ(QualityLimitationReason::kCpu,
+            statistics_proxy_->GetStats().quality_limitation_reason);
+}
+
+TEST_F(SendStatisticsProxyTest,
+       QualityLimitationReasonIsCpuWhenCpuIsFramerateLimited) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  cpu_counts.num_framerate_reductions = 1;
+
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts,
+      quality_counts);
+
+  EXPECT_EQ(QualityLimitationReason::kCpu,
+            statistics_proxy_->GetStats().quality_limitation_reason);
+}
+
+TEST_F(SendStatisticsProxyTest,
+       QualityLimitationReasonIsBandwidthWhenQualityIsResolutionLimited) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  quality_counts.num_resolution_reductions = 1;
+
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kQuality, cpu_counts,
+      quality_counts);
+
+  EXPECT_EQ(QualityLimitationReason::kBandwidth,
+            statistics_proxy_->GetStats().quality_limitation_reason);
+}
+
+TEST_F(SendStatisticsProxyTest,
+       QualityLimitationReasonIsBandwidthWhenQualityIsFramerateLimited) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  quality_counts.num_framerate_reductions = 1;
+
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kQuality, cpu_counts,
+      quality_counts);
+
+  EXPECT_EQ(QualityLimitationReason::kBandwidth,
+            statistics_proxy_->GetStats().quality_limitation_reason);
+}
+
+TEST_F(SendStatisticsProxyTest,
+       QualityLimitationReasonIsBandwidthWhenBothCpuAndQualityIsLimited) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  cpu_counts.num_resolution_reductions = 1;
+  quality_counts.num_resolution_reductions = 1;
+
+  // Even if the last adaptation reason is kCpu, if the counters indicate being
+  // both CPU and quality (=bandwidth) limited, kBandwidth takes precedence.
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts,
+      quality_counts);
+
+  EXPECT_EQ(QualityLimitationReason::kBandwidth,
+            statistics_proxy_->GetStats().quality_limitation_reason);
+}
+
+TEST_F(SendStatisticsProxyTest, QualityLimitationReasonIsNoneWhenNotLimited) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  // Observe a limitation due to CPU. This makes sure the test doesn't pass
+  // due to "none" being the default value.
+  cpu_counts.num_resolution_reductions = 1;
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts,
+      quality_counts);
+  // Go back to not being limited.
+  cpu_counts.num_resolution_reductions = 0;
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kNone, cpu_counts,
+      quality_counts);
+
+  EXPECT_EQ(QualityLimitationReason::kNone,
+            statistics_proxy_->GetStats().quality_limitation_reason);
+}
+
+TEST_F(SendStatisticsProxyTest, QualityLimitationDurationIncreasesWithTime) {
+  SendStatisticsProxy::AdaptationSteps cpu_counts;
+  SendStatisticsProxy::AdaptationSteps quality_counts;
+
+  // Not limited for 3000 ms
+  fake_clock_.AdvanceTimeMilliseconds(3000);
+  // CPU limited for 2000 ms
+  cpu_counts.num_resolution_reductions = 1;
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts,
+      quality_counts);
+  fake_clock_.AdvanceTimeMilliseconds(2000);
+  // Bandwidth limited for 1000 ms
+  cpu_counts.num_resolution_reductions = 0;
+  quality_counts.num_resolution_reductions = 1;
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kQuality, cpu_counts,
+      quality_counts);
+  fake_clock_.AdvanceTimeMilliseconds(1000);
+  // CPU limited for another 2000 ms
+  cpu_counts.num_resolution_reductions = 1;
+  quality_counts.num_resolution_reductions = 0;
+  statistics_proxy_->OnAdaptationChanged(
+      VideoStreamEncoderObserver::AdaptationReason::kCpu, cpu_counts,
+      quality_counts);
+  fake_clock_.AdvanceTimeMilliseconds(2000);
+
+  auto quality_limitation_durations_ms =
+      statistics_proxy_->GetStats().quality_limitation_durations_ms;
+
+  EXPECT_EQ(3000,
+            quality_limitation_durations_ms[QualityLimitationReason::kNone]);
+  EXPECT_EQ(4000,
+            quality_limitation_durations_ms[QualityLimitationReason::kCpu]);
+  EXPECT_EQ(
+      1000,
+      quality_limitation_durations_ms[QualityLimitationReason::kBandwidth]);
+  EXPECT_EQ(0,
+            quality_limitation_durations_ms[QualityLimitationReason::kOther]);
+}
+
 TEST_F(SendStatisticsProxyTest, SwitchContentTypeUpdatesHistograms) {
   for (int i = 0; i < SendStatisticsProxy::kMinRequiredMetricsSamples; ++i)
     statistics_proxy_->OnIncomingFrame(kWidth, kHeight);