[DVQA] Add support for DVQA to pause/resume receiving of stream by peer

Bug: b/271542055, webrtc:14995
Change-Id: Ic02451347160f512588b6fef5d6ac4ad904b5e18
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/297440
Reviewed-by: Jeremy Leconte <jleconte@google.com>
Commit-Queue: Artem Titov <titovartem@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#39568}
diff --git a/api/test/video_quality_analyzer_interface.h b/api/test/video_quality_analyzer_interface.h
index d35be8c..abd0426 100644
--- a/api/test/video_quality_analyzer_interface.h
+++ b/api/test/video_quality_analyzer_interface.h
@@ -150,6 +150,17 @@
   // call.
   virtual void UnregisterParticipantInCall(absl::string_view peer_name) {}
 
+  // Informs analyzer that peer `peer_name` is expected to receive stream
+  // `stream_label`.
+  virtual void OnPeerStartedReceiveVideoStream(absl::string_view peer_name,
+                                               absl::string_view stream_label) {
+  }
+  // Informs analyzer that peer `peer_name` shouldn't receive stream
+  // `stream_label`.
+  virtual void OnPeerStoppedReceiveVideoStream(absl::string_view peer_name,
+                                               absl::string_view stream_label) {
+  }
+
   // Tells analyzer that analysis complete and it should calculate final
   // statistics.
   virtual void Stop() {}
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 59b5683..a7cd342 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -685,6 +685,7 @@
         "../rtc_base/system:file_wrapper",
         "pc/e2e:e2e_unittests",
         "pc/e2e/analyzer/video:video_analyzer_unittests",
+        "pc/e2e/analyzer/video/dvqa:dvqa_unittests",
         "peer_scenario/tests",
         "scenario:scenario_unittests",
         "time_controller:time_controller",
diff --git a/test/pc/e2e/analyzer/video/BUILD.gn b/test/pc/e2e/analyzer/video/BUILD.gn
index cbb4c07..5c489e8 100644
--- a/test/pc/e2e/analyzer/video/BUILD.gn
+++ b/test/pc/e2e/analyzer/video/BUILD.gn
@@ -233,6 +233,7 @@
     "../../../../../api:scoped_refptr",
     "../../../../../api/numerics",
     "../../../../../api/units:data_size",
+    "../../../../../api/units:time_delta",
     "../../../../../api/units:timestamp",
     "../../../../../api/video:video_frame",
     "../../../../../api/video:video_frame_type",
@@ -246,6 +247,7 @@
     "../../../../../rtc_base/synchronization:mutex",
     "../../../../../rtc_tools:video_quality_analysis",
     "../../../../../system_wrappers",
+    "dvqa:pausable_state",
   ]
   absl_deps = [
     "//third_party/abseil-cpp/absl/strings:strings",
@@ -492,6 +494,7 @@
       ":default_video_quality_analyzer_internal",
       "../../../..:test_support",
       "../../../../../api/units:timestamp",
+      "../../../../../system_wrappers",
     ]
     absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
   }
@@ -505,13 +508,17 @@
       "../../../..:test_support",
       "../../../../../api:create_frame_generator",
       "../../../../../api:rtp_packet_info",
+      "../../../../../api:time_controller",
       "../../../../../api/test/metrics:global_metrics_logger_and_exporter",
+      "../../../../../api/units:time_delta",
+      "../../../../../api/units:timestamp",
       "../../../../../api/video:encoded_image",
       "../../../../../api/video:video_frame",
       "../../../../../common_video",
       "../../../../../rtc_base:stringutils",
       "../../../../../rtc_tools:video_quality_analysis",
       "../../../../../system_wrappers",
+      "../../../../time_controller",
     ]
   }
 
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc
index a5c9dd7..389cb09 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.cc
@@ -27,6 +27,7 @@
 #include "rtc_base/checks.h"
 #include "rtc_base/logging.h"
 #include "rtc_base/strings/string_builder.h"
+#include "system_wrappers/include/clock.h"
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.h"
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator.h"
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer_internal_shared_objects.h"
@@ -203,9 +204,9 @@
 
     auto state_it = stream_states_.find(stream_index);
     if (state_it == stream_states_.end()) {
-      stream_states_.emplace(
-          stream_index,
-          StreamState(peer_index, frame_receivers_indexes, captured_time));
+      stream_states_.emplace(stream_index,
+                             StreamState(peer_index, frame_receivers_indexes,
+                                         captured_time, clock_));
     }
     StreamState* state = &stream_states_.at(stream_index);
     state->PushBack(frame_id);
@@ -222,6 +223,11 @@
 
         uint16_t oldest_frame_id = state->PopFront(i);
         RTC_DCHECK_EQ(frame_id, oldest_frame_id);
+
+        if (state->GetPausableState(i)->IsPaused()) {
+          continue;
+        }
+
         frame_counters_.dropped++;
         InternalStatsKey key(stream_index, peer_index, i);
         stream_frame_counters_.at(key).dropped++;
@@ -528,40 +534,27 @@
   // After we received frame here we need to check if there are any dropped
   // frames between this one and last one, that was rendered for this video
   // stream.
-  int dropped_count = 0;
-  while (!state->IsEmpty(peer_index) &&
-         state->Front(peer_index) != frame.id()) {
-    dropped_count++;
-    uint16_t dropped_frame_id = state->PopFront(peer_index);
-    // Frame with id `dropped_frame_id` was dropped. We need:
-    // 1. Update global and stream frame counters
-    // 2. Extract corresponding frame from `captured_frames_in_flight_`
-    // 3. Send extracted frame to comparison with dropped=true
-    // 4. Cleanup dropped frame
-    frame_counters_.dropped++;
-    stream_frame_counters_.at(stats_key).dropped++;
-
-    auto dropped_frame_it = captured_frames_in_flight_.find(dropped_frame_id);
-    RTC_DCHECK(dropped_frame_it != captured_frames_in_flight_.end());
-    dropped_frame_it->second.MarkDropped(peer_index);
-
-    analyzer_stats_.frames_in_flight_left_count.AddSample(
-        StatsSample(captured_frames_in_flight_.size(), Now()));
-    frames_comparator_.AddComparison(
-        stats_key, /*captured=*/absl::nullopt, /*rendered=*/absl::nullopt,
-        FrameComparisonType::kDroppedFrame,
-        dropped_frame_it->second.GetStatsForPeer(peer_index));
-
-    if (dropped_frame_it->second.HaveAllPeersReceived()) {
-      captured_frames_in_flight_.erase(dropped_frame_it);
-    }
-  }
+  int dropped_count = ProcessNotSeenFramesBeforeRendered(peer_index, frame.id(),
+                                                         stats_key, *state);
   RTC_DCHECK(!state->IsEmpty(peer_index));
   state->PopFront(peer_index);
 
-  if (state->last_rendered_frame_time(peer_index)) {
+  if (state->last_rendered_frame_time(peer_index).has_value()) {
+    TimeDelta time_between_rendered_frames =
+        state->GetPausableState(peer_index)
+            ->GetActiveDurationFrom(
+                *state->last_rendered_frame_time(peer_index));
+    if (state->GetPausableState(peer_index)->IsPaused()) {
+      // If stream is currently paused for this receiver, but we still received
+      // frame, we have to add time from last pause up to Now() to the time
+      // between rendered frames.
+      time_between_rendered_frames +=
+          Now() - state->GetPausableState(peer_index)->GetLastEventTime();
+    }
+    frame_in_flight->SetTimeBetweenRenderedFrames(peer_index,
+                                                  time_between_rendered_frames);
     frame_in_flight->SetPrevFrameRenderedTime(
-        peer_index, state->last_rendered_frame_time(peer_index).value());
+        peer_index, *state->last_rendered_frame_time(peer_index));
   }
   state->SetLastRenderedFrameTime(peer_index,
                                   frame_in_flight->rendered_time(peer_index));
@@ -734,6 +727,34 @@
   }
 }
 
+void DefaultVideoQualityAnalyzer::OnPeerStartedReceiveVideoStream(
+    absl::string_view peer_name,
+    absl::string_view stream_label) {
+  MutexLock lock(&mutex_);
+  RTC_CHECK(peers_->HasName(peer_name));
+  size_t peer_index = peers_->index(peer_name);
+  RTC_CHECK(streams_.HasName(stream_label));
+  size_t stream_index = streams_.index(stream_label);
+
+  auto it = stream_states_.find(stream_index);
+  RTC_CHECK(it != stream_states_.end());
+  it->second.GetPausableState(peer_index)->Resume();
+}
+
+void DefaultVideoQualityAnalyzer::OnPeerStoppedReceiveVideoStream(
+    absl::string_view peer_name,
+    absl::string_view stream_label) {
+  MutexLock lock(&mutex_);
+  RTC_CHECK(peers_->HasName(peer_name));
+  size_t peer_index = peers_->index(peer_name);
+  RTC_CHECK(streams_.HasName(stream_label));
+  size_t stream_index = streams_.index(stream_label);
+
+  auto it = stream_states_.find(stream_index);
+  RTC_CHECK(it != stream_states_.end());
+  it->second.GetPausableState(peer_index)->Pause();
+}
+
 void DefaultVideoQualityAnalyzer::Stop() {
   std::map<InternalStatsKey, Timestamp> last_rendered_frame_times;
   {
@@ -923,7 +944,8 @@
   // Add frames in flight for this stream into frames comparator.
   // Frames in flight were not rendered, so they won't affect stream's
   // last rendered frame time.
-  while (!stream_state.IsEmpty(peer_index)) {
+  while (!stream_state.IsEmpty(peer_index) &&
+         !stream_state.GetPausableState(peer_index)->IsPaused()) {
     uint16_t frame_id = stream_state.PopFront(peer_index);
     auto it = captured_frames_in_flight_.find(frame_id);
     RTC_DCHECK(it != captured_frames_in_flight_.end());
@@ -936,6 +958,103 @@
   }
 }
 
+int DefaultVideoQualityAnalyzer::ProcessNotSeenFramesBeforeRendered(
+    size_t peer_index,
+    uint16_t rendered_frame_id,
+    const InternalStatsKey& stats_key,
+    StreamState& state) {
+  int dropped_count = 0;
+  while (!state.IsEmpty(peer_index) &&
+         state.Front(peer_index) != rendered_frame_id) {
+    uint16_t next_frame_id = state.PopFront(peer_index);
+    auto next_frame_it = captured_frames_in_flight_.find(next_frame_id);
+    RTC_DCHECK(next_frame_it != captured_frames_in_flight_.end());
+    FrameInFlight& next_frame = next_frame_it->second;
+
+    // Depending if the receiver was subscribed to this stream or not at the
+    // time when frame was captured, the frame should be considered as dropped
+    // or superfluous (see below for explanation). Superfluous frames must be
+    // excluded from stats calculations.
+    //
+    // We should consider next cases:
+    // Legend:
+    //    + - frame captured on the stream
+    //    p - stream is paused
+    //    r - stream is resumed
+    //
+    //    last                                                   currently
+    //  rendered                                                  rendered
+    //    frame                                                    frame
+    //      |---------------------- dropped -------------------------|
+    // (1) -[]---+---+---+---+---+---+---+---+---+---+---+---+---+---[]-> time
+    //      |                                                        |
+    //      |                                                        |
+    //      |-- dropped ---┐       ┌- dropped -┐       ┌- dropped ---|
+    // (2) -[]---+---+---+-|-+---+-|-+---+---+-|-+---+-|-+---+---+---[]-> time
+    //      |              p       r           p       r             |
+    //      |                                                        |
+    //      |-- dropped ---┐       ┌------------ dropped ------------|
+    // (3) -[]---+---+---+-|-+---+-|-+---+---+---+---+---+-|-+---+---[]-> time
+    //                     p       r                       p
+    //
+    // Cases explanation:
+    //   (1) Regular media flow, frame is received after freeze.
+    //   (2) Stream was paused and received multiple times. Frame is received
+    //       after freeze from last resume.
+    //   (3) Stream was paused and received multiple times. Frame is received
+    //       after stream was paused because frame was already in the network.
+    //
+    // Based on that if stream wasn't paused when `next_frame_id` was captured,
+    // then `next_frame_id` should be considered as dropped. If stream was NOT
+    // resumed after `next_frame_id` was captured but we still received a
+    // `rendered_frame_id` on this stream, then `next_frame_id` also should
+    // be considered as dropped. In other cases `next_frame_id` should be
+    // considered as superfluous, because receiver wasn't expected to receive
+    // `next_frame_id` at all.
+
+    bool is_dropped = false;
+    bool is_paused = state.GetPausableState(peer_index)
+                         ->WasPausedAt(next_frame.captured_time());
+    if (!is_paused) {
+      is_dropped = true;
+    } else {
+      bool was_resumed_after =
+          state.GetPausableState(peer_index)
+              ->WasResumedAfter(next_frame.captured_time());
+      if (!was_resumed_after) {
+        is_dropped = true;
+      }
+    }
+
+    if (is_dropped) {
+      dropped_count++;
+      // Frame with id `dropped_frame_id` was dropped. We need:
+      // 1. Update global and stream frame counters
+      // 2. Extract corresponding frame from `captured_frames_in_flight_`
+      // 3. Send extracted frame to comparison with dropped=true
+      // 4. Cleanup dropped frame
+      frame_counters_.dropped++;
+      stream_frame_counters_.at(stats_key).dropped++;
+
+      next_frame.MarkDropped(peer_index);
+
+      analyzer_stats_.frames_in_flight_left_count.AddSample(
+          StatsSample(captured_frames_in_flight_.size(), Now()));
+      frames_comparator_.AddComparison(stats_key, /*captured=*/absl::nullopt,
+                                       /*rendered=*/absl::nullopt,
+                                       FrameComparisonType::kDroppedFrame,
+                                       next_frame.GetStatsForPeer(peer_index));
+    } else {
+      next_frame.MarkSuperfluous(peer_index);
+    }
+
+    if (next_frame_it->second.HaveAllPeersReceived()) {
+      captured_frames_in_flight_.erase(next_frame_it);
+    }
+  }
+  return dropped_count;
+}
+
 void DefaultVideoQualityAnalyzer::ReportResults() {
   MutexLock lock(&mutex_);
   for (auto& item : frames_comparator_.stream_stats()) {
@@ -1047,8 +1166,7 @@
       {MetricMetadataKey::kExperimentalTestNameMetadataKey, test_label_}};
 
   double sum_squared_interframe_delays_secs = 0;
-  Timestamp video_start_time = Timestamp::PlusInfinity();
-  Timestamp video_end_time = Timestamp::MinusInfinity();
+  double video_duration_ms = 0;
   for (const SamplesStatsCounter::StatsSample& sample :
        stats.time_between_rendered_frames_ms.GetTimedSamples()) {
     double interframe_delay_ms = sample.value;
@@ -1058,18 +1176,13 @@
     // to smoothness of video playback and includes both freezes and pauses.
     sum_squared_interframe_delays_secs +=
         interframe_delays_secs * interframe_delays_secs;
-    if (sample.time < video_start_time) {
-      video_start_time = sample.time;
-    }
-    if (sample.time > video_end_time) {
-      video_end_time = sample.time;
-    }
+
+    video_duration_ms += sample.value;
   }
   double harmonic_framerate_fps = 0;
-  TimeDelta video_duration = video_end_time - video_start_time;
-  if (sum_squared_interframe_delays_secs > 0.0 && video_duration.IsFinite()) {
+  if (sum_squared_interframe_delays_secs > 0.0) {
     harmonic_framerate_fps =
-        video_duration.seconds<double>() / sum_squared_interframe_delays_secs;
+        video_duration_ms / 1000.0 / sum_squared_interframe_delays_secs;
   }
 
   metrics_logger_->LogMetric(
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h
index b67e5a0..c4bf324 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer.h
@@ -80,6 +80,10 @@
 
   void RegisterParticipantInCall(absl::string_view peer_name) override;
   void UnregisterParticipantInCall(absl::string_view peer_name) override;
+  void OnPeerStartedReceiveVideoStream(absl::string_view peer_name,
+                                       absl::string_view stream_label) override;
+  void OnPeerStoppedReceiveVideoStream(absl::string_view peer_name,
+                                       absl::string_view stream_label) override;
 
   void Stop() override;
   std::string GetStreamLabel(uint16_t frame_id) override;
@@ -118,6 +122,17 @@
                                                       size_t peer_index)
       RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
 
+  // Processes frames for the peer identified by `peer_index` up to
+  // `rendered_frame_id` (excluded). Sends each dropped frame for comparison and
+  // discards superfluous frames (they were not expected to be received by
+  // `peer_index` and not accounted in the stats).
+  // Returns number of dropped frames.
+  int ProcessNotSeenFramesBeforeRendered(size_t peer_index,
+                                         uint16_t rendered_frame_id,
+                                         const InternalStatsKey& stats_key,
+                                         StreamState& state)
+      RTC_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
   // Report results for all metrics for all streams.
   void ReportResults();
   void ReportResults(const InternalStatsKey& key,
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.cc
index fb87fd5..bee5638 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.cc
@@ -10,6 +10,7 @@
 
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.h"
 
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
@@ -24,7 +25,8 @@
 namespace {
 
 template <typename T>
-absl::optional<T> MaybeGetValue(const std::map<size_t, T>& map, size_t key) {
+absl::optional<T> MaybeGetValue(const std::unordered_map<size_t, T>& map,
+                                size_t key) {
   auto it = map.find(key);
   if (it == map.end()) {
     return absl::nullopt;
@@ -63,7 +65,8 @@
   for (size_t peer : expected_receivers_) {
     auto it = receiver_stats_.find(peer);
     if (it == receiver_stats_.end() ||
-        (!it->second.dropped && it->second.rendered_time.IsInfinite())) {
+        (!it->second.dropped && !it->second.superfluous &&
+         it->second.rendered_time.IsInfinite())) {
       out.push_back(peer);
     }
   }
@@ -77,7 +80,8 @@
       return false;
     }
 
-    if (!it->second.dropped && it->second.rendered_time.IsInfinite()) {
+    if (!it->second.dropped && !it->second.superfluous &&
+        it->second.rendered_time.IsInfinite()) {
       return false;
     }
   }
@@ -179,6 +183,8 @@
 FrameStats FrameInFlight::GetStatsForPeer(size_t peer) const {
   RTC_DCHECK_NE(frame_id_, VideoFrame::kNotSetId)
       << "Frame id isn't initialized";
+  RTC_DCHECK(!IsSuperfluous(peer))
+      << "This frame is superfluous for peer " << peer;
   FrameStats stats(frame_id_, captured_time_);
   stats.pre_encode_time = pre_encode_time_;
   stats.encoded_time = encoded_time_;
@@ -196,6 +202,8 @@
     stats.decode_end_time = receiver_stats->decode_end_time;
     stats.rendered_time = receiver_stats->rendered_time;
     stats.prev_frame_rendered_time = receiver_stats->prev_frame_rendered_time;
+    stats.time_between_rendered_frames =
+        receiver_stats->time_between_rendered_frames;
     stats.decoded_frame_width = receiver_stats->decoded_frame_width;
     stats.decoded_frame_height = receiver_stats->decoded_frame_height;
     stats.used_decoder = receiver_stats->used_decoder;
@@ -206,4 +214,12 @@
   return stats;
 }
 
+bool FrameInFlight::IsSuperfluous(size_t peer) const {
+  auto it = receiver_stats_.find(peer);
+  if (it == receiver_stats_.end()) {
+    return false;
+  }
+  return it->second.superfluous;
+}
+
 }  // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.h b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.h
index 3c5bc95..8322eb7 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.h
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frame_in_flight.h
@@ -13,12 +13,14 @@
 
 #include <map>
 #include <set>
+#include <unordered_map>
 #include <utility>
 #include <vector>
 
 #include "absl/types/optional.h"
 #include "api/numerics/samples_stats_counter.h"
 #include "api/units/data_size.h"
+#include "api/units/time_delta.h"
 #include "api/units/timestamp.h"
 #include "api/video/video_frame.h"
 #include "api/video/video_frame_type.h"
@@ -34,6 +36,8 @@
   Timestamp rendered_time = Timestamp::MinusInfinity();
   Timestamp prev_frame_rendered_time = Timestamp::MinusInfinity();
 
+  TimeDelta time_between_rendered_frames = TimeDelta::Zero();
+
   // Type and encoded size of received frame.
   VideoFrameType frame_type = VideoFrameType::kEmptyFrame;
   DataSize encoded_image_size = DataSize::Bytes(0);
@@ -46,6 +50,9 @@
 
   bool dropped = false;
   bool decoder_failed = false;
+
+  // Superfluous frames should be used for stats calculation for that peer.
+  bool superfluous = false;
 };
 
 // Represents a frame which was sent by sender and is currently on the way to
@@ -67,6 +74,9 @@
   // Returns internal copy of source `VideoFrame` or `absl::nullopt` if it was
   // removed before.
   const absl::optional<VideoFrame>& frame() const { return frame_; }
+
+  Timestamp captured_time() const { return captured_time_; }
+
   // Removes internal copy of the source `VideoFrame` to free up extra memory.
   // Returns was frame removed or not.
   bool RemoveFrame();
@@ -82,9 +92,9 @@
   // received it or not.
   bool HaveAllPeersReceived() const;
 
-  void SetPreEncodeTime(webrtc::Timestamp time) { pre_encode_time_ = time; }
+  void SetPreEncodeTime(Timestamp time) { pre_encode_time_ = time; }
 
-  void OnFrameEncoded(webrtc::Timestamp time,
+  void OnFrameEncoded(Timestamp time,
                       VideoFrameType frame_type,
                       DataSize encoded_image_size,
                       uint32_t target_encode_bitrate,
@@ -95,15 +105,15 @@
   bool HasEncodedTime() const { return encoded_time_.IsFinite(); }
 
   void OnFramePreDecode(size_t peer,
-                        webrtc::Timestamp received_time,
-                        webrtc::Timestamp decode_start_time,
+                        Timestamp received_time,
+                        Timestamp decode_start_time,
                         VideoFrameType frame_type,
                         DataSize encoded_image_size);
 
   bool HasReceivedTime(size_t peer) const;
 
   void OnFrameDecoded(size_t peer,
-                      webrtc::Timestamp time,
+                      Timestamp time,
                       int width,
                       int height,
                       const StreamCodecInfo& used_decoder);
@@ -111,12 +121,12 @@
 
   bool HasDecodeEndTime(size_t peer) const;
 
-  void OnFrameRendered(size_t peer, webrtc::Timestamp time);
+  void OnFrameRendered(size_t peer, Timestamp time);
 
   bool HasRenderedTime(size_t peer) const;
 
   // Crash if rendered time is not set for specified `peer`.
-  webrtc::Timestamp rendered_time(size_t peer) const {
+  Timestamp rendered_time(size_t peer) const {
     return receiver_stats_.at(peer).rendered_time;
   }
 
@@ -124,13 +134,23 @@
   void MarkDropped(size_t peer) { receiver_stats_[peer].dropped = true; }
   bool IsDropped(size_t peer) const;
 
+  void MarkSuperfluous(size_t peer) {
+    receiver_stats_[peer].superfluous = true;
+  }
+
   void SetPrevFrameRenderedTime(size_t peer, webrtc::Timestamp time) {
     receiver_stats_[peer].prev_frame_rendered_time = time;
   }
 
+  void SetTimeBetweenRenderedFrames(size_t peer, TimeDelta time) {
+    receiver_stats_[peer].time_between_rendered_frames = time;
+  }
+
   FrameStats GetStatsForPeer(size_t peer) const;
 
  private:
+  bool IsSuperfluous(size_t peer) const;
+
   const size_t stream_;
   // Set of peer's indexes who are expected to receive this frame. This is not
   // the set of peer's indexes that received the frame. For example, if peer A
@@ -156,12 +176,12 @@
   DataSize encoded_image_size_ = DataSize::Bytes(0);
   uint32_t target_encode_bitrate_ = 0;
   // Sender side qp values per spatial or simulcast layer. If neither the
-  // spatial or simulcast index is set in `webrtc::EncodedImage`, 0 is used.
+  // spatial or simulcast index is set in `EncodedImage`, 0 is used.
   std::map<int, SamplesStatsCounter> stream_layers_qp_;
   // Can be not set if frame was dropped by encoder.
   absl::optional<StreamCodecInfo> used_encoder_ = absl::nullopt;
   // Map from the receiver peer's index to frame stats for that peer.
-  std::map<size_t, ReceiverFrameStats> receiver_stats_;
+  std::unordered_map<size_t, ReceiverFrameStats> receiver_stats_;
 };
 
 }  // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator.cc
index cbc0b7e..fc91dd1 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator.cc
@@ -523,20 +523,21 @@
 
     if (frame_stats.prev_frame_rendered_time.IsFinite() &&
         frame_stats.rendered_time.IsFinite()) {
-      TimeDelta time_between_rendered_frames =
-          frame_stats.rendered_time - frame_stats.prev_frame_rendered_time;
-      stats->time_between_rendered_frames_ms.AddSample(StatsSample(
-          time_between_rendered_frames, frame_stats.rendered_time, metadata));
+      stats->time_between_rendered_frames_ms.AddSample(
+          StatsSample(frame_stats.time_between_rendered_frames,
+                      frame_stats.rendered_time, metadata));
       TimeDelta average_time_between_rendered_frames = TimeDelta::Millis(
           stats->time_between_rendered_frames_ms.GetAverage());
-      if (time_between_rendered_frames >
+      if (frame_stats.time_between_rendered_frames >
           std::max(kFreezeThreshold + average_time_between_rendered_frames,
                    3 * average_time_between_rendered_frames)) {
-        stats->freeze_time_ms.AddSample(StatsSample(
-            time_between_rendered_frames, frame_stats.rendered_time, metadata));
+        stats->freeze_time_ms.AddSample(
+            StatsSample(frame_stats.time_between_rendered_frames,
+                        frame_stats.rendered_time, metadata));
         auto freeze_end_it =
             stream_last_freeze_end_time_.find(comparison.stats_key);
         RTC_DCHECK(freeze_end_it != stream_last_freeze_end_time_.end());
+        // TODO(bugs.webrtc.org/14995): rethink this metric for paused stream.
         stats->time_between_freezes_ms.AddSample(StatsSample(
             frame_stats.prev_frame_rendered_time - freeze_end_it->second,
             frame_stats.rendered_time, metadata));
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator_test.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator_test.cc
index 8d3cd47..d6732e1 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator_test.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_frames_comparator_test.cc
@@ -219,6 +219,8 @@
   FrameStats frame_stats2 = FrameStatsWith10msDeltaBetweenPhasesAnd10x10Frame(
       /*frame_id=*/2, stream_start_time + TimeDelta::Millis(15));
   frame_stats2.prev_frame_rendered_time = frame_stats1.rendered_time;
+  frame_stats2.time_between_rendered_frames =
+      frame_stats2.rendered_time - frame_stats1.rendered_time;
 
   comparator.Start(/*max_threads_count=*/1);
   comparator.EnsureStatsForStream(stream, sender, peers_count,
@@ -1610,6 +1612,8 @@
     FrameStats frame_stats = FrameStatsWith10msDeltaBetweenPhasesAnd10x10Frame(
         /*frame_id=*/i + 1, stream_start_time + TimeDelta::Millis(30 * i));
     frame_stats.prev_frame_rendered_time = prev_frame_rendered_time;
+    frame_stats.time_between_rendered_frames =
+        frame_stats.rendered_time - prev_frame_rendered_time;
     prev_frame_rendered_time = frame_stats.rendered_time;
 
     comparator.AddComparison(stats_key,
@@ -1624,6 +1628,8 @@
       FrameStatsWith10msDeltaBetweenPhasesAnd10x10Frame(
           /*frame_id=*/10, stream_start_time + TimeDelta::Millis(120 + 300));
   freeze_frame_stats.prev_frame_rendered_time = prev_frame_rendered_time;
+  freeze_frame_stats.time_between_rendered_frames =
+      freeze_frame_stats.rendered_time - prev_frame_rendered_time;
 
   comparator.AddComparison(stats_key,
                            /*skipped_between_rendered=*/4,
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_internal_shared_objects.h b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_internal_shared_objects.h
index 10f1314..b425480 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_internal_shared_objects.h
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_internal_shared_objects.h
@@ -59,6 +59,10 @@
   Timestamp rendered_time = Timestamp::MinusInfinity();
   Timestamp prev_frame_rendered_time = Timestamp::MinusInfinity();
 
+  // Time between this and previous rendered frame excluding time when related
+  // stream was paused for related receiver.
+  TimeDelta time_between_rendered_frames = TimeDelta::Zero();
+
   VideoFrameType encoded_frame_type = VideoFrameType::kEmptyFrame;
   DataSize encoded_image_size = DataSize::Bytes(0);
   VideoFrameType pre_decoded_frame_type = VideoFrameType::kEmptyFrame;
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.cc
index d59ef12..eee69a7 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.cc
@@ -10,18 +10,21 @@
 
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.h"
 
-#include <map>
 #include <set>
+#include <unordered_map>
 
 #include "absl/types/optional.h"
 #include "api/units/timestamp.h"
 #include "rtc_base/checks.h"
+#include "system_wrappers/include/clock.h"
+#include "test/pc/e2e/analyzer/video/dvqa/pausable_state.h"
 
 namespace webrtc {
 namespace {
 
 template <typename T>
-absl::optional<T> MaybeGetValue(const std::map<size_t, T>& map, size_t key) {
+absl::optional<T> MaybeGetValue(const std::unordered_map<size_t, T>& map,
+                                size_t key) {
   auto it = map.find(key);
   if (it == map.end()) {
     return absl::nullopt;
@@ -33,15 +36,18 @@
 
 StreamState::StreamState(size_t sender,
                          std::set<size_t> receivers,
-                         Timestamp stream_started_time)
+                         Timestamp stream_started_time,
+                         Clock* clock)
     : sender_(sender),
       stream_started_time_(stream_started_time),
+      clock_(clock),
       receivers_(receivers),
       frame_ids_(std::move(receivers)) {
   frame_ids_.AddReader(kAliveFramesQueueIndex);
   RTC_CHECK_NE(sender_, kAliveFramesQueueIndex);
   for (size_t receiver : receivers_) {
     RTC_CHECK_NE(receiver, kAliveFramesQueueIndex);
+    pausable_state_.emplace(receiver, PausableState(clock_));
   }
 }
 
@@ -69,12 +75,14 @@
   RTC_CHECK_NE(peer, kAliveFramesQueueIndex);
   frame_ids_.AddReader(peer, kAliveFramesQueueIndex);
   receivers_.insert(peer);
+  pausable_state_.emplace(peer, PausableState(clock_));
 }
 
 void StreamState::RemovePeer(size_t peer) {
   RTC_CHECK_NE(peer, kAliveFramesQueueIndex);
   frame_ids_.RemoveReader(peer);
   receivers_.erase(peer);
+  pausable_state_.erase(peer);
 
   // If we removed the last receiver for the alive frames, we need to pop them
   // from the queue, because now they received by all receivers.
@@ -86,6 +94,13 @@
   }
 }
 
+PausableState* StreamState::GetPausableState(size_t peer) {
+  auto it = pausable_state_.find(peer);
+  RTC_CHECK(it != pausable_state_.end())
+      << "No pausable state for receiver " << peer;
+  return &it->second;
+}
+
 uint16_t StreamState::MarkNextAliveFrameAsDead() {
   absl::optional<uint16_t> frame_id =
       frame_ids_.PopFront(kAliveFramesQueueIndex);
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.h b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.h
index 829a79c..f0dc4cd 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.h
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state.h
@@ -12,11 +12,13 @@
 #define TEST_PC_E2E_ANALYZER_VIDEO_DEFAULT_VIDEO_QUALITY_ANALYZER_STREAM_STATE_H_
 
 #include <limits>
-#include <map>
 #include <set>
+#include <unordered_map>
 
 #include "absl/types/optional.h"
 #include "api/units/timestamp.h"
+#include "system_wrappers/include/clock.h"
+#include "test/pc/e2e/analyzer/video/dvqa/pausable_state.h"
 #include "test/pc/e2e/analyzer/video/multi_reader_queue.h"
 
 namespace webrtc {
@@ -37,7 +39,8 @@
  public:
   StreamState(size_t sender,
               std::set<size_t> receivers,
-              Timestamp stream_started_time);
+              Timestamp stream_started_time,
+              Clock* clock);
 
   size_t sender() const { return sender_; }
   Timestamp stream_started_time() const { return stream_started_time_; }
@@ -59,9 +62,14 @@
   // DefaultVideoQualityAnalyzer still may request it for stats processing.
   void RemovePeer(size_t peer);
 
+  // Returns a pointer to the PausableState of this stream for specified peer.
+  // The pointer is owned by StreamState and guranteed to be not null.
+  PausableState* GetPausableState(size_t peer);
+
   size_t GetAliveFramesCount() const {
     return frame_ids_.size(kAliveFramesQueueIndex);
   }
+  // Returns frame id of the frame which was marked as dead.
   uint16_t MarkNextAliveFrameAsDead();
 
   void SetLastRenderedFrameTime(size_t peer, Timestamp time);
@@ -78,6 +86,7 @@
   // Index of the owner. Owner's queue in `frame_ids_` will keep alive frames.
   const size_t sender_;
   const Timestamp stream_started_time_;
+  Clock* const clock_;
   std::set<size_t> receivers_;
   // To correctly determine dropped frames we have to know sequence of frames
   // in each stream so we will keep a list of frame ids inside the stream.
@@ -92,7 +101,9 @@
   // frame_id2 and consider those frames as dropped and then compare received
   // frame with the one from `FrameInFlight` with id frame_id3.
   MultiReaderQueue<uint16_t> frame_ids_;
-  std::map<size_t, Timestamp> last_rendered_frame_time_;
+  std::unordered_map<size_t, Timestamp> last_rendered_frame_time_;
+  // Mapping from peer's index to pausable state for this receiver.
+  std::unordered_map<size_t, PausableState> pausable_state_;
 };
 
 }  // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state_test.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state_test.cc
index 01a6aab..9c4d584 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state_test.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_stream_state_test.cc
@@ -13,6 +13,7 @@
 #include <set>
 
 #include "api/units/timestamp.h"
+#include "system_wrappers/include/clock.h"
 #include "test/gtest.h"
 
 namespace webrtc {
@@ -20,8 +21,8 @@
 
 TEST(StreamStateTest, PopFrontAndFrontIndependentForEachPeer) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
   state.PushBack(/*frame_id=*/2);
 
@@ -37,8 +38,8 @@
 
 TEST(StreamStateTest, IsEmpty) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
 
   EXPECT_FALSE(state.IsEmpty(/*peer=*/1));
@@ -50,8 +51,8 @@
 
 TEST(StreamStateTest, PopFrontForOnlyOnePeerDontChangeAliveFramesCount) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
   state.PushBack(/*frame_id=*/2);
 
@@ -65,8 +66,8 @@
 
 TEST(StreamStateTest, PopFrontForAllPeersReducesAliveFramesCount) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
   state.PushBack(/*frame_id=*/2);
 
@@ -80,8 +81,8 @@
 
 TEST(StreamStateTest, RemovePeerForLastExpectedReceiverUpdatesAliveFrames) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
   state.PushBack(/*frame_id=*/2);
 
@@ -96,8 +97,8 @@
 
 TEST(StreamStateTest, MarkNextAliveFrameAsDeadDecreseAliveFramesCount) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
   state.PushBack(/*frame_id=*/2);
 
@@ -110,8 +111,8 @@
 
 TEST(StreamStateTest, MarkNextAliveFrameAsDeadDoesntAffectFrontFrameForPeer) {
   StreamState state(/*sender=*/0,
-                    /*receivers=*/std::set<size_t>{1, 2},
-                    Timestamp::Seconds(1));
+                    /*receivers=*/std::set<size_t>{1, 2}, Timestamp::Seconds(1),
+                    Clock::GetRealTimeClock());
   state.PushBack(/*frame_id=*/1);
   state.PushBack(/*frame_id=*/2);
 
diff --git a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc
index fc970e1..6a459c5 100644
--- a/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc
+++ b/test/pc/e2e/analyzer/video/default_video_quality_analyzer_test.cc
@@ -19,6 +19,9 @@
 #include "api/rtp_packet_infos.h"
 #include "api/test/create_frame_generator.h"
 #include "api/test/metrics/global_metrics_logger_and_exporter.h"
+#include "api/test/time_controller.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
 #include "api/video/encoded_image.h"
 #include "api/video/i420_buffer.h"
 #include "api/video/video_frame.h"
@@ -26,12 +29,18 @@
 #include "rtc_base/strings/string_builder.h"
 #include "rtc_tools/frame_analyzer/video_geometry_aligner.h"
 #include "system_wrappers/include/sleep.h"
+#include "test/gmock.h"
 #include "test/gtest.h"
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer_shared_objects.h"
+#include "test/time_controller/simulated_time_controller.h"
 
 namespace webrtc {
 namespace {
 
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::IsEmpty;
+using ::testing::Test;
 using ::testing::TestWithParam;
 using ::testing::ValuesIn;
 
@@ -95,6 +104,26 @@
   return out;
 }
 
+std::vector<double> GetTimeSortedValues(const SamplesStatsCounter& counter) {
+  rtc::ArrayView<const StatsSample> view = counter.GetTimedSamples();
+  std::vector<StatsSample> sorted(view.begin(), view.end());
+  std::sort(sorted.begin(), sorted.end(),
+            [](const StatsSample& a, const StatsSample& b) {
+              return a.time < b.time;
+            });
+  std::vector<double> out;
+  out.reserve(sorted.size());
+  for (const StatsSample& sample : sorted) {
+    out.push_back(sample.value);
+  }
+  return out;
+}
+
+void ExpectRateIs(const SamplesRateCounter& rate_couter, double expected_rate) {
+  ASSERT_FALSE(rate_couter.IsEmpty());
+  EXPECT_NEAR(rate_couter.GetEventsPerSecond(), expected_rate, 1e-5);
+}
+
 std::string ToString(const std::vector<StatsSample>& values) {
   rtc::StringBuilder out;
   for (const auto& v : values) {
@@ -112,13 +141,42 @@
   ASSERT_TRUE(std::is_sorted(temp.begin(), temp.end()));
 }
 
+void PassFramesThroughAnalyzerSenderOnly(
+    DefaultVideoQualityAnalyzer& analyzer,
+    absl::string_view sender,
+    absl::string_view stream_label,
+    std::vector<absl::string_view> receivers,
+    int frames_count,
+    test::FrameGeneratorInterface& frame_generator,
+    int interframe_delay_ms = 0,
+    TimeController* time_controller = nullptr) {
+  for (int i = 0; i < frames_count; ++i) {
+    VideoFrame frame = NextFrame(&frame_generator, /*timestamp_us=*/1);
+    uint16_t frame_id =
+        analyzer.OnFrameCaptured(sender, std::string(stream_label), frame);
+    frame.set_id(frame_id);
+    analyzer.OnFramePreEncode(sender, frame);
+    analyzer.OnFrameEncoded(sender, frame.id(), FakeEncode(frame),
+                            VideoQualityAnalyzerInterface::EncoderStats(),
+                            false);
+    if (i < frames_count - 1 && interframe_delay_ms > 0) {
+      if (time_controller == nullptr) {
+        SleepMs(interframe_delay_ms);
+      } else {
+        time_controller->AdvanceTime(TimeDelta::Millis(interframe_delay_ms));
+      }
+    }
+  }
+}
+
 void PassFramesThroughAnalyzer(DefaultVideoQualityAnalyzer& analyzer,
                                absl::string_view sender,
                                absl::string_view stream_label,
                                std::vector<absl::string_view> receivers,
                                int frames_count,
                                test::FrameGeneratorInterface& frame_generator,
-                               int interframe_delay_ms = 0) {
+                               int interframe_delay_ms = 0,
+                               TimeController* time_controller = nullptr) {
   for (int i = 0; i < frames_count; ++i) {
     VideoFrame frame = NextFrame(&frame_generator, /*timestamp_us=*/1);
     uint16_t frame_id =
@@ -137,7 +195,11 @@
       analyzer.OnFrameRendered(receiver, received_frame);
     }
     if (i < frames_count - 1 && interframe_delay_ms > 0) {
-      SleepMs(interframe_delay_ms);
+      if (time_controller == nullptr) {
+        SleepMs(interframe_delay_ms);
+      } else {
+        time_controller->AdvanceTime(TimeDelta::Millis(interframe_delay_ms));
+      }
     }
   }
 }
@@ -790,7 +852,7 @@
   }
 
   // Windows CPU clock has low accuracy. We need to fake some additional load to
-  // be sure that the clock ticks (https://crbug.com/webrtc/12249).
+  // be sure that the clock ticks (https://bugs.webrtc.org/12249).
   FakeCPULoad();
 
   for (size_t i = 1; i < frames_order.size(); i += 2) {
@@ -2200,5 +2262,169 @@
                          DefaultVideoQualityAnalyzerTimeBetweenFreezesTest,
                          ValuesIn({true, false}));
 
+class DefaultVideoQualityAnalyzerSimulatedTimeTest : public Test {
+ protected:
+  DefaultVideoQualityAnalyzerSimulatedTimeTest()
+      : time_controller_(std::make_unique<GlobalSimulatedTimeController>(
+            Timestamp::Seconds(1000))) {}
+
+  void AdvanceTime(TimeDelta time) { time_controller_->AdvanceTime(time); }
+
+  Clock* GetClock() { return time_controller_->GetClock(); }
+
+  TimeController* time_controller() { return time_controller_.get(); }
+
+  Timestamp Now() const { return time_controller_->GetClock()->CurrentTime(); }
+
+ private:
+  std::unique_ptr<TimeController> time_controller_;
+};
+
+TEST_F(DefaultVideoQualityAnalyzerSimulatedTimeTest,
+       PausedAndResumedStreamIsAccountedInStatsCorrectly) {
+  std::unique_ptr<test::FrameGeneratorInterface> frame_generator =
+      test::CreateSquareFrameGenerator(kFrameWidth, kFrameHeight,
+                                       /*type=*/absl::nullopt,
+                                       /*num_squares=*/absl::nullopt);
+
+  DefaultVideoQualityAnalyzerOptions options = AnalyzerOptionsForTest();
+  options.report_infra_metrics = false;
+  DefaultVideoQualityAnalyzer analyzer(GetClock(),
+                                       test::GetGlobalMetricsLogger(), options);
+  analyzer.Start("test_case",
+                 std::vector<std::string>{"alice", "bob", "charlie"},
+                 kAnalyzerMaxThreadsCount);
+
+  // Pass 20 frames as 20 fps.
+  PassFramesThroughAnalyzer(analyzer, "alice", "alice_video",
+                            {"bob", "charlie"},
+                            /*frames_count=*/20, *frame_generator,
+                            /*interframe_delay_ms=*/50, time_controller());
+  AdvanceTime(TimeDelta::Millis(50));
+
+  // Mark stream paused for Bob, but not for Charlie.
+  analyzer.OnPeerStoppedReceiveVideoStream("bob", "alice_video");
+  // Freeze for 1 second.
+  PassFramesThroughAnalyzerSenderOnly(
+      analyzer, "alice", "alice_video", {"bob", "charlie"},
+      /*frames_count=*/20, *frame_generator,
+      /*interframe_delay_ms=*/50, time_controller());
+  AdvanceTime(TimeDelta::Millis(50));
+  // Unpause stream for Bob.
+  analyzer.OnPeerStartedReceiveVideoStream("bob", "alice_video");
+
+  // Pass 20 frames as 20 fps.
+  PassFramesThroughAnalyzer(analyzer, "alice", "alice_video",
+                            {"bob", "charlie"},
+                            /*frames_count=*/20, *frame_generator,
+                            /*interframe_delay_ms=*/50, time_controller());
+
+  analyzer.Stop();
+
+  // Bob should have 20 fps without freeze and Charlie should have freeze of 1s
+  // and decreased fps.
+  std::map<StatsKey, StreamStats> streams_stats = analyzer.GetStats();
+  std::map<StatsKey, FrameCounters> frame_counters =
+      analyzer.GetPerStreamCounters();
+  StreamStats bob_stream_stats =
+      streams_stats.at(StatsKey("alice_video", "bob"));
+  FrameCounters bob_frame_counters =
+      frame_counters.at(StatsKey("alice_video", "bob"));
+  EXPECT_THAT(bob_frame_counters.dropped, Eq(0));
+  EXPECT_THAT(bob_frame_counters.rendered, Eq(40));
+  EXPECT_THAT(GetTimeSortedValues(bob_stream_stats.freeze_time_ms),
+              ElementsAre(0.0));
+  // TODO(bugs.webrtc.org/14995): value should exclude pause
+  EXPECT_THAT(GetTimeSortedValues(bob_stream_stats.time_between_freezes_ms),
+              ElementsAre(2950.0));
+  // TODO(bugs.webrtc.org/14995): Fix capture_frame_rate (has to be ~20.0)
+  ExpectRateIs(bob_stream_stats.capture_frame_rate, 13.559322);
+  // TODO(bugs.webrtc.org/14995): Fix encode_frame_rate (has to be ~20.0)
+  ExpectRateIs(bob_stream_stats.encode_frame_rate, 13.559322);
+  // TODO(bugs.webrtc.org/14995): Assert on harmonic fps
+
+  StreamStats charlie_stream_stats =
+      streams_stats.at(StatsKey("alice_video", "charlie"));
+  FrameCounters charlie_frame_counters =
+      frame_counters.at(StatsKey("alice_video", "charlie"));
+  EXPECT_THAT(charlie_frame_counters.dropped, Eq(20));
+  EXPECT_THAT(charlie_frame_counters.rendered, Eq(40));
+  EXPECT_THAT(GetTimeSortedValues(charlie_stream_stats.freeze_time_ms),
+              ElementsAre(1050.0));
+  EXPECT_THAT(GetTimeSortedValues(charlie_stream_stats.time_between_freezes_ms),
+              ElementsAre(950.0, 950.0));
+  // TODO(bugs.webrtc.org/14995): Assert on harmonic fps
+}
+
+TEST_F(DefaultVideoQualityAnalyzerSimulatedTimeTest,
+       PausedStreamIsAccountedInStatsCorrectly) {
+  std::unique_ptr<test::FrameGeneratorInterface> frame_generator =
+      test::CreateSquareFrameGenerator(kFrameWidth, kFrameHeight,
+                                       /*type=*/absl::nullopt,
+                                       /*num_squares=*/absl::nullopt);
+
+  DefaultVideoQualityAnalyzerOptions options = AnalyzerOptionsForTest();
+  options.report_infra_metrics = false;
+  DefaultVideoQualityAnalyzer analyzer(GetClock(),
+                                       test::GetGlobalMetricsLogger(), options);
+  analyzer.Start("test_case",
+                 std::vector<std::string>{"alice", "bob", "charlie"},
+                 kAnalyzerMaxThreadsCount);
+
+  // Pass 20 frames as 20 fps.
+  PassFramesThroughAnalyzer(analyzer, "alice", "alice_video",
+                            {"bob", "charlie"},
+                            /*frames_count=*/20, *frame_generator,
+                            /*interframe_delay_ms=*/50, time_controller());
+  AdvanceTime(TimeDelta::Millis(50));
+
+  // Mark stream paused for Bob, but not for Charlie.
+  analyzer.OnPeerStoppedReceiveVideoStream("bob", "alice_video");
+  // Freeze for 1 second.
+  PassFramesThroughAnalyzerSenderOnly(
+      analyzer, "alice", "alice_video", {"bob", "charlie"},
+      /*frames_count=*/20, *frame_generator,
+      /*interframe_delay_ms=*/50, time_controller());
+  AdvanceTime(TimeDelta::Millis(50));
+
+  // Pass 20 frames as 20 fps.
+  PassFramesThroughAnalyzer(analyzer, "alice", "alice_video", {"charlie"},
+                            /*frames_count=*/20, *frame_generator,
+                            /*interframe_delay_ms=*/50, time_controller());
+
+  analyzer.Stop();
+
+  // Bob should have 20 fps without freeze and Charlie should have freeze of 1s
+  // and decreased fps.
+  std::map<StatsKey, StreamStats> streams_stats = analyzer.GetStats();
+  std::map<StatsKey, FrameCounters> frame_counters =
+      analyzer.GetPerStreamCounters();
+  StreamStats bob_stream_stats =
+      streams_stats.at(StatsKey("alice_video", "bob"));
+  FrameCounters bob_frame_counters =
+      frame_counters.at(StatsKey("alice_video", "bob"));
+  EXPECT_THAT(bob_frame_counters.dropped, Eq(0));
+  EXPECT_THAT(bob_frame_counters.rendered, Eq(20));
+  EXPECT_THAT(GetTimeSortedValues(bob_stream_stats.freeze_time_ms),
+              ElementsAre(0.0));
+  EXPECT_THAT(GetTimeSortedValues(bob_stream_stats.time_between_freezes_ms),
+              ElementsAre(950.0));
+  ExpectRateIs(bob_stream_stats.capture_frame_rate, 21.052631);
+  ExpectRateIs(bob_stream_stats.encode_frame_rate, 21.052631);
+  // TODO(bugs.webrtc.org/14995): Assert on harmonic fps
+
+  StreamStats charlie_stream_stats =
+      streams_stats.at(StatsKey("alice_video", "charlie"));
+  FrameCounters charlie_frame_counters =
+      frame_counters.at(StatsKey("alice_video", "charlie"));
+  EXPECT_THAT(charlie_frame_counters.dropped, Eq(20));
+  EXPECT_THAT(charlie_frame_counters.rendered, Eq(40));
+  EXPECT_THAT(GetTimeSortedValues(charlie_stream_stats.freeze_time_ms),
+              ElementsAre(1050.0));
+  EXPECT_THAT(GetTimeSortedValues(charlie_stream_stats.time_between_freezes_ms),
+              ElementsAre(950.0, 950.0));
+  // TODO(bugs.webrtc.org/14995): Assert on harmonic fps
+}
+
 }  // namespace
 }  // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/dvqa/BUILD.gn b/test/pc/e2e/analyzer/video/dvqa/BUILD.gn
new file mode 100644
index 0000000..3aa25ee
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/dvqa/BUILD.gn
@@ -0,0 +1,65 @@
+# Copyright (c) 2023 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.
+
+import("../../../../../../webrtc.gni")
+
+if (!build_with_chromium) {
+  group("dvqa") {
+    testonly = true
+
+    deps = [ ":pausable_state" ]
+  }
+
+  if (rtc_include_tests) {
+    group("dvqa_unittests") {
+      testonly = true
+
+      deps = [ ":pausable_state_test" ]
+    }
+  }
+}
+
+# These targets contains implementation details of DefaultVideoQualityAnalyzer,
+# so headers exported by it shouldn't be used in other places.
+
+rtc_library("pausable_state") {
+  visibility = [
+    ":dvqa",
+    ":pausable_state_test",
+    "..:default_video_quality_analyzer_internal",
+  ]
+
+  testonly = true
+  sources = [
+    "pausable_state.cc",
+    "pausable_state.h",
+  ]
+
+  deps = [
+    "../../../../../../api/units:time_delta",
+    "../../../../../../api/units:timestamp",
+    "../../../../../../rtc_base:checks",
+    "../../../../../../system_wrappers",
+  ]
+}
+
+if (rtc_include_tests) {
+  rtc_library("pausable_state_test") {
+    testonly = true
+    sources = [ "pausable_state_test.cc" ]
+    deps = [
+      ":pausable_state",
+      "../../../../..:test_support",
+      "../../../../../../api:time_controller",
+      "../../../../../../api/units:time_delta",
+      "../../../../../../api/units:timestamp",
+      "../../../../../../system_wrappers",
+      "../../../../../time_controller",
+    ]
+  }
+}
diff --git a/test/pc/e2e/analyzer/video/dvqa/pausable_state.cc b/test/pc/e2e/analyzer/video/dvqa/pausable_state.cc
new file mode 100644
index 0000000..f416e9b
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/dvqa/pausable_state.cc
@@ -0,0 +1,99 @@
+/*
+ *  Copyright (c) 2023 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 "test/pc/e2e/analyzer/video/dvqa/pausable_state.h"
+
+#include <cstdint>
+
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+
+void PausableState::Pause() {
+  RTC_CHECK(!IsPaused());
+  events_.push_back(Event{.time = clock_->CurrentTime(), .is_paused = true});
+}
+
+void PausableState::Resume() {
+  RTC_CHECK(IsPaused());
+  events_.push_back(Event{.time = clock_->CurrentTime(), .is_paused = false});
+}
+
+bool PausableState::IsPaused() const {
+  return !events_.empty() && events_.back().is_paused;
+}
+
+bool PausableState::WasPausedAt(Timestamp time) const {
+  if (events_.empty()) {
+    return false;
+  }
+
+  int64_t pos = GetPos(time);
+  return pos != -1 && events_[pos].is_paused;
+}
+
+bool PausableState::WasResumedAfter(Timestamp time) const {
+  if (events_.empty()) {
+    return false;
+  }
+
+  int64_t pos = GetPos(time);
+  return (pos + 1 < static_cast<int64_t>(events_.size())) &&
+         !events_[pos + 1].is_paused;
+}
+
+Timestamp PausableState::GetLastEventTime() const {
+  if (events_.empty()) {
+    return Timestamp::PlusInfinity();
+  }
+
+  return events_.back().time;
+}
+
+TimeDelta PausableState::GetActiveDurationFrom(Timestamp time) const {
+  if (events_.empty()) {
+    return clock_->CurrentTime() - time;
+  }
+
+  int64_t pos = GetPos(time);
+  TimeDelta duration = TimeDelta::Zero();
+  for (int64_t i = pos; i < static_cast<int64_t>(events_.size()); ++i) {
+    if (i == -1 || !events_[i].is_paused) {
+      Timestamp start_time = (i == pos) ? time : events_[i].time;
+      Timestamp end_time = (i + 1 == static_cast<int64_t>(events_.size()))
+                               ? clock_->CurrentTime()
+                               : events_[i + 1].time;
+
+      duration += end_time - start_time;
+    }
+  }
+  return duration;
+}
+
+int64_t PausableState::GetPos(Timestamp time) const {
+  int64_t l = 0, r = events_.size() - 1;
+  while (l < r) {
+    int64_t pos = (l + r) / 2;
+    if (time < events_[pos].time) {
+      r = pos;
+    } else if (time >= events_[pos].time) {
+      l = pos + 1;
+    }
+  }
+  if (time < events_[l].time) {
+    return l - 1;
+  } else {
+    return l;
+  }
+}
+
+}  // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/dvqa/pausable_state.h b/test/pc/e2e/analyzer/video/dvqa/pausable_state.h
new file mode 100644
index 0000000..402b72c
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/dvqa/pausable_state.h
@@ -0,0 +1,89 @@
+/*
+ *  Copyright (c) 2023 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 TEST_PC_E2E_ANALYZER_VIDEO_DVQA_PAUSABLE_STATE_H_
+#define TEST_PC_E2E_ANALYZER_VIDEO_DVQA_PAUSABLE_STATE_H_
+
+#include <cstdint>
+#include <vector>
+
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "system_wrappers/include/clock.h"
+
+namespace webrtc {
+
+// Provides ability to pause and resume and tell at any point was state paused
+// or active.
+class PausableState {
+ public:
+  // Creates a state as active.
+  explicit PausableState(Clock* clock) : clock_(clock) {}
+  PausableState(const PausableState&) = delete;
+  PausableState& operator=(const PausableState&) = delete;
+  PausableState(PausableState&&) = default;
+  PausableState& operator=(PausableState&&) = default;
+
+  // Pauses current state. States MUST be active.
+  //
+  // Complexity: O(1)
+  void Pause();
+
+  // Activates current state. State MUST be paused.
+  //
+  // Complexity: O(1)
+  void Resume();
+
+  // Returns is state is paused right now.
+  //
+  // Complexity: O(1)
+  bool IsPaused() const;
+
+  // Returns if last event before `time` was "pause".
+  //
+  // Complexity: O(log(n))
+  bool WasPausedAt(Timestamp time) const;
+
+  // Returns if next event after `time` was "resume".
+  //
+  // Complexity: O(log(n))
+  bool WasResumedAfter(Timestamp time) const;
+
+  // Returns time of last event or plus infinity if no events happened.
+  //
+  // Complexity O(1)
+  Timestamp GetLastEventTime() const;
+
+  // Returns sum of durations during which state was active starting from
+  // time `time`.
+  //
+  // Complexity O(n)
+  TimeDelta GetActiveDurationFrom(Timestamp time) const;
+
+ private:
+  struct Event {
+    Timestamp time;
+    bool is_paused;
+  };
+
+  // Returns position in `events_` which has time:
+  // 1. Most right of the equals
+  // 2. The biggest which is smaller
+  // 3. -1 otherwise (first time is bigger than `time`)
+  int64_t GetPos(Timestamp time) const;
+
+  Clock* clock_;
+
+  std::vector<Event> events_;
+};
+
+}  // namespace webrtc
+
+#endif  // TEST_PC_E2E_ANALYZER_VIDEO_DVQA_PAUSABLE_STATE_H_
diff --git a/test/pc/e2e/analyzer/video/dvqa/pausable_state_test.cc b/test/pc/e2e/analyzer/video/dvqa/pausable_state_test.cc
new file mode 100644
index 0000000..fe2b37b
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/dvqa/pausable_state_test.cc
@@ -0,0 +1,373 @@
+/*
+ *  Copyright (c) 2023 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 "test/pc/e2e/analyzer/video/dvqa/pausable_state.h"
+
+#include <memory>
+
+#include "api/test/time_controller.h"
+#include "api/units/time_delta.h"
+#include "api/units/timestamp.h"
+#include "system_wrappers/include/clock.h"
+#include "test/gtest.h"
+#include "test/time_controller/simulated_time_controller.h"
+
+namespace webrtc {
+namespace {
+
+class PausableStateTest : public testing::Test {
+ protected:
+  PausableStateTest()
+      : time_controller_(std::make_unique<GlobalSimulatedTimeController>(
+            Timestamp::Seconds(1000))) {}
+
+  void AdvanceTime(TimeDelta time) { time_controller_->AdvanceTime(time); }
+
+  Clock* GetClock() { return time_controller_->GetClock(); }
+
+  Timestamp Now() const { return time_controller_->GetClock()->CurrentTime(); }
+
+ private:
+  std::unique_ptr<TimeController> time_controller_;
+};
+
+TEST_F(PausableStateTest, NewIsActive) {
+  PausableState state(GetClock());
+
+  EXPECT_FALSE(state.IsPaused());
+}
+
+TEST_F(PausableStateTest, IsPausedAfterPaused) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  EXPECT_TRUE(state.IsPaused());
+}
+
+TEST_F(PausableStateTest, IsActiveAfterResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  state.Resume();
+  EXPECT_FALSE(state.IsPaused());
+}
+
+TEST_F(PausableStateTest, WasPausedAtFalseWhenMultiplePauseResumeAtSameTime) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  state.Resume();
+  state.Pause();
+  state.Resume();
+  state.Pause();
+  state.Resume();
+  EXPECT_FALSE(state.WasPausedAt(Now()));
+}
+
+TEST_F(PausableStateTest,
+       WasPausedAtTrueWhenMultiplePauseResumeAtSameTimeAndThenPause) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  state.Resume();
+  state.Pause();
+  state.Resume();
+  state.Pause();
+  state.Resume();
+  state.Pause();
+  EXPECT_TRUE(state.WasPausedAt(Now()));
+}
+
+TEST_F(PausableStateTest, WasPausedAtFalseBeforeFirstPause) {
+  PausableState state(GetClock());
+
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+
+  EXPECT_FALSE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasPausedAtTrueAfterPauseBeforeResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_TRUE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasPausedAtFalseAfterResumeBeforePause) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_FALSE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasPausedAtTrueAtPauseBeforeResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_TRUE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasPausedAtFalseAfterPauseAtResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_FALSE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasPausedAtTrueAfterPause) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+
+  EXPECT_TRUE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasPausedAtFalseAfterResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+
+  EXPECT_FALSE(state.WasPausedAt(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterFalseBeforeFirstPause) {
+  PausableState state(GetClock());
+
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+
+  EXPECT_FALSE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterTrueAfterPauseBeforeResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_TRUE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterFalseAfterResumeBeforePause) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_FALSE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterTrueAtPauseBeforeResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  Timestamp test_time = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_TRUE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterFalseAfterPauseAtResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+
+  EXPECT_FALSE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterFalseAfterPause) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+
+  EXPECT_FALSE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, WasResumedAfterFalseAfterResume) {
+  PausableState state(GetClock());
+
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp test_time = Now();
+
+  EXPECT_FALSE(state.WasResumedAfter(test_time));
+}
+
+TEST_F(PausableStateTest, GetActiveDurationFromWithoutPausesReturnAllTime) {
+  PausableState state(GetClock());
+
+  Timestamp time_from = Now();
+  AdvanceTime(TimeDelta::Seconds(5));
+
+  EXPECT_EQ(state.GetActiveDurationFrom(time_from), TimeDelta::Seconds(5));
+}
+
+TEST_F(PausableStateTest, GetActiveDurationFromRespectsPauses) {
+  PausableState state(GetClock());
+
+  Timestamp time_from = Now();
+
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+
+  EXPECT_EQ(state.GetActiveDurationFrom(time_from), TimeDelta::Seconds(3));
+}
+
+TEST_F(PausableStateTest, GetActiveDurationFromMiddleOfPauseAccountOnlyActive) {
+  PausableState state(GetClock());
+
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp time_from = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+
+  EXPECT_EQ(state.GetActiveDurationFrom(time_from), TimeDelta::Seconds(2));
+}
+
+TEST_F(PausableStateTest, GetActiveDurationFromMiddleOfActiveAccountAllActive) {
+  PausableState state(GetClock());
+
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp time_from = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+
+  EXPECT_EQ(state.GetActiveDurationFrom(time_from), TimeDelta::Seconds(2));
+}
+
+TEST_F(PausableStateTest, GetActiveDurationFromWhenPauseReturnsZero) {
+  PausableState state(GetClock());
+
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp time_from = Now();
+
+  EXPECT_EQ(state.GetActiveDurationFrom(time_from), TimeDelta::Zero());
+}
+
+TEST_F(PausableStateTest, GetActiveDurationFromWhenActiveReturnsAllTime) {
+  PausableState state(GetClock());
+
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Pause();
+  AdvanceTime(TimeDelta::Seconds(1));
+  state.Resume();
+  AdvanceTime(TimeDelta::Seconds(1));
+  Timestamp time_from = Now();
+  AdvanceTime(TimeDelta::Seconds(1));
+
+  EXPECT_EQ(state.GetActiveDurationFrom(time_from), TimeDelta::Seconds(1));
+}
+
+}  // namespace
+}  // namespace webrtc