Refactor video codec testing stats

This CL introduces VideoCodecStats and VideoCodecStatsImpl which provide baseline functionalities for storing, slicing and aggregation of encoded and/or decoded video frame statistics. To facilitate metrics logging (not implemented yet), SamplesStatsCounter is used for stream parameters.

VideoCodecStats/VideoCodecStatsImpl will replace existing VideoCodecTestStats/VideoCodecTestStatsImpl.

Bug: b/261160916, webrtc:14852
Change-Id: I0f96ce1ed9be3aee2a702804612524676c9882fd
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/291323
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Reviewed-by: Rasmus Brandt <brandtr@webrtc.org>
Reviewed-by: Mirko Bonadei <mbonadei@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#39248}
diff --git a/api/BUILD.gn b/api/BUILD.gn
index 33a6b0a..138d855 100644
--- a/api/BUILD.gn
+++ b/api/BUILD.gn
@@ -1003,6 +1003,20 @@
     absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
   }
 
+  rtc_library("video_codec_stats_api") {
+    visibility = [ "*" ]
+    testonly = true
+    sources = [ "test/video_codec_stats.h" ]
+    deps = [
+      "../api/numerics:numerics",
+      "../api/units:data_rate",
+      "../api/units:frequency",
+      "test/metrics:metric",
+      "test/metrics:metrics_logger",
+    ]
+    absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ]
+  }
+
   rtc_library("videocodec_test_fixture_api") {
     visibility = [ "*" ]
     testonly = true
@@ -1019,7 +1033,7 @@
     testonly = true
     sources = [ "test/video_codec_tester.h" ]
     deps = [
-      ":videocodec_test_stats_api",
+      ":video_codec_stats_api",
       "../modules/video_coding/svc:scalability_mode_util",
       "video:encoded_image",
       "video:resolution",
diff --git a/api/test/video_codec_stats.h b/api/test/video_codec_stats.h
new file mode 100644
index 0000000..b1dfee8
--- /dev/null
+++ b/api/test/video_codec_stats.h
@@ -0,0 +1,117 @@
+/*
+ *  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 API_TEST_VIDEO_CODEC_STATS_H_
+#define API_TEST_VIDEO_CODEC_STATS_H_
+
+#include <string>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/numerics/samples_stats_counter.h"
+#include "api/test/metrics/metric.h"
+#include "api/test/metrics/metrics_logger.h"
+#include "api/units/data_rate.h"
+#include "api/units/frequency.h"
+
+namespace webrtc {
+namespace test {
+
+// Interface for encoded and/or decoded video frame and stream statistics.
+class VideoCodecStats {
+ public:
+  // Filter for slicing frames.
+  struct Filter {
+    absl::optional<int> first_frame;
+    absl::optional<int> last_frame;
+    absl::optional<int> spatial_idx;
+    absl::optional<int> temporal_idx;
+  };
+
+  struct Frame {
+    int frame_num = 0;
+    uint32_t timestamp_rtp = 0;
+
+    int spatial_idx = 0;
+    int temporal_idx = 0;
+
+    int width = 0;
+    int height = 0;
+    int size_bytes = 0;
+    bool keyframe = false;
+    absl::optional<int> qp = absl::nullopt;
+    absl::optional<int> base_spatial_idx = absl::nullopt;
+
+    Timestamp encode_start = Timestamp::Zero();
+    TimeDelta encode_time = TimeDelta::Zero();
+    Timestamp decode_start = Timestamp::Zero();
+    TimeDelta decode_time = TimeDelta::Zero();
+
+    struct Psnr {
+      double y = 0.0;
+      double u = 0.0;
+      double v = 0.0;
+    };
+    absl::optional<Psnr> psnr = absl::nullopt;
+
+    bool encoded = false;
+    bool decoded = false;
+  };
+
+  struct Stream {
+    int num_frames = 0;
+    int num_keyframes = 0;
+
+    SamplesStatsCounter width;
+    SamplesStatsCounter height;
+    SamplesStatsCounter size_bytes;
+    SamplesStatsCounter qp;
+
+    SamplesStatsCounter encode_time_us;
+    SamplesStatsCounter decode_time_us;
+
+    DataRate bitrate = DataRate::Zero();
+    Frequency framerate = Frequency::Zero();
+    int bitrate_mismatch_pct = 0;
+    int framerate_mismatch_pct = 0;
+    SamplesStatsCounter transmission_time_us;
+
+    struct Psnr {
+      SamplesStatsCounter y;
+      SamplesStatsCounter u;
+      SamplesStatsCounter v;
+    } psnr;
+  };
+
+  virtual ~VideoCodecStats() = default;
+
+  // Returns frames from interval, spatial and temporal layer specified by given
+  // `filter`.
+  virtual std::vector<Frame> Slice(
+      absl::optional<Filter> filter = absl::nullopt) const = 0;
+
+  // Returns video statistics aggregated for given `frames`. If `bitrate` is
+  // provided, also performs rate control analysis. If `framerate` is provided,
+  // also calculates frame rate mismatch.
+  virtual Stream Aggregate(
+      const std::vector<Frame>& frames,
+      absl::optional<DataRate> bitrate = absl::nullopt,
+      absl::optional<Frequency> framerate = absl::nullopt) const = 0;
+
+  // Logs `Stream` metrics to provided `MetricsLogger`.
+  virtual void LogMetrics(MetricsLogger* logger,
+                          const Stream& stream,
+                          std::string test_case_name) const = 0;
+};
+
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // API_TEST_VIDEO_CODEC_STATS_H_
diff --git a/api/test/video_codec_tester.h b/api/test/video_codec_tester.h
index 0eaaa1b..b2ce88c 100644
--- a/api/test/video_codec_tester.h
+++ b/api/test/video_codec_tester.h
@@ -14,7 +14,8 @@
 #include <memory>
 
 #include "absl/functional/any_invocable.h"
-#include "api/test/videocodec_test_stats.h"
+#include "absl/types/optional.h"
+#include "api/test/video_codec_stats.h"
 #include "api/video/encoded_image.h"
 #include "api/video/resolution.h"
 #include "api/video/video_frame.h"
@@ -104,7 +105,7 @@
   // Pulls coded video frames from `video_source` and passes them to `decoder`.
   // Returns `VideoCodecTestStats` object that contains collected per-frame
   // metrics.
-  virtual std::unique_ptr<VideoCodecTestStats> RunDecodeTest(
+  virtual std::unique_ptr<VideoCodecStats> RunDecodeTest(
       std::unique_ptr<CodedVideoSource> video_source,
       std::unique_ptr<Decoder> decoder,
       const DecoderSettings& decoder_settings) = 0;
@@ -112,7 +113,7 @@
   // Pulls raw video frames from `video_source` and passes them to `encoder`.
   // Returns `VideoCodecTestStats` object that contains collected per-frame
   // metrics.
-  virtual std::unique_ptr<VideoCodecTestStats> RunEncodeTest(
+  virtual std::unique_ptr<VideoCodecStats> RunEncodeTest(
       std::unique_ptr<RawVideoSource> video_source,
       std::unique_ptr<Encoder> encoder,
       const EncoderSettings& encoder_settings) = 0;
@@ -120,7 +121,7 @@
   // Pulls raw video frames from `video_source`, passes them to `encoder` and
   // then passes encoded frames to `decoder`. Returns `VideoCodecTestStats`
   // object that contains collected per-frame metrics.
-  virtual std::unique_ptr<VideoCodecTestStats> RunEncodeDecodeTest(
+  virtual std::unique_ptr<VideoCodecStats> RunEncodeDecodeTest(
       std::unique_ptr<RawVideoSource> video_source,
       std::unique_ptr<Encoder> encoder,
       std::unique_ptr<Decoder> decoder,
diff --git a/api/test/videocodec_test_fixture.h b/api/test/videocodec_test_fixture.h
index dbf2099..8e66f72 100644
--- a/api/test/videocodec_test_fixture.h
+++ b/api/test/videocodec_test_fixture.h
@@ -54,6 +54,7 @@
 };
 
 // NOTE: This class is still under development and may change without notice.
+// TODO(webrtc:14852): Deprecated in favor VideoCodecTester.
 class VideoCodecTestFixture {
  public:
   class EncodedFrameChecker {
diff --git a/api/test/videocodec_test_stats.h b/api/test/videocodec_test_stats.h
index 12c6063..d620d31 100644
--- a/api/test/videocodec_test_stats.h
+++ b/api/test/videocodec_test_stats.h
@@ -27,6 +27,7 @@
 namespace test {
 
 // Statistics for a sequence of processed frames. This class is not thread safe.
+// TODO(webrtc:14852): Deprecated in favor VideoCodecStats.
 class VideoCodecTestStats {
  public:
   // Statistics for one processed frame.
diff --git a/modules/video_coding/BUILD.gn b/modules/video_coding/BUILD.gn
index fe63804..e2384bb 100644
--- a/modules/video_coding/BUILD.gn
+++ b/modules/video_coding/BUILD.gn
@@ -878,6 +878,8 @@
     sources = [
       "codecs/test/video_codec_analyzer.cc",
       "codecs/test/video_codec_analyzer.h",
+      "codecs/test/video_codec_stats_impl.cc",
+      "codecs/test/video_codec_stats_impl.h",
       "codecs/test/video_codec_unittest.cc",
       "codecs/test/video_codec_unittest.h",
       "codecs/test/videoprocessor.cc",
@@ -896,10 +898,13 @@
       "../../api:frame_generator_api",
       "../../api:scoped_refptr",
       "../../api:sequence_checker",
+      "../../api:video_codec_stats_api",
       "../../api:video_codec_tester_api",
       "../../api:videocodec_test_fixture_api",
+      "../../api/numerics:numerics",
       "../../api/task_queue",
       "../../api/task_queue:default_task_queue_factory",
+      "../../api/test/metrics:global_metrics_logger_and_exporter",
       "../../api/video:builtin_video_bitrate_allocator_factory",
       "../../api/video:encoded_image",
       "../../api/video:resolution",
@@ -1037,6 +1042,8 @@
     deps = [
       "../../api:videocodec_test_stats_api",
       "../../api/numerics",
+      "../../api/test/metrics:global_metrics_logger_and_exporter",
+      "../../api/test/metrics:metric",
       "../../rtc_base:checks",
       "../../rtc_base:rtc_numerics",
       "../../rtc_base:stringutils",
@@ -1162,6 +1169,7 @@
     sources = [
       "chain_diff_calculator_unittest.cc",
       "codecs/test/video_codec_analyzer_unittest.cc",
+      "codecs/test/video_codec_stats_impl_unittest.cc",
       "codecs/test/video_codec_tester_impl_unittest.cc",
       "codecs/test/videocodec_test_fixture_config_unittest.cc",
       "codecs/test/videocodec_test_stats_impl_unittest.cc",
diff --git a/modules/video_coding/codecs/test/video_codec_analyzer.cc b/modules/video_coding/codecs/test/video_codec_analyzer.cc
index 50af417..6521112 100644
--- a/modules/video_coding/codecs/test/video_codec_analyzer.cc
+++ b/modules/video_coding/codecs/test/video_codec_analyzer.cc
@@ -13,7 +13,6 @@
 #include <memory>
 
 #include "api/task_queue/default_task_queue_factory.h"
-#include "api/test/video_codec_tester.h"
 #include "api/video/i420_buffer.h"
 #include "api/video/video_codec_constants.h"
 #include "api/video/video_frame.h"
@@ -26,13 +25,7 @@
 namespace test {
 
 namespace {
-
-struct Psnr {
-  double y;
-  double u;
-  double v;
-  double yuv;
-};
+using Psnr = VideoCodecStats::Frame::Psnr;
 
 Psnr CalcPsnr(const I420BufferInterface& ref_buffer,
               const I420BufferInterface& dec_buffer) {
@@ -56,8 +49,7 @@
   psnr.y = libyuv::SumSquareErrorToPsnr(sse_y, num_y_samples);
   psnr.u = libyuv::SumSquareErrorToPsnr(sse_u, num_y_samples / 4);
   psnr.v = libyuv::SumSquareErrorToPsnr(sse_v, num_y_samples / 4);
-  psnr.yuv = libyuv::SumSquareErrorToPsnr(sse_y + sse_u + sse_v,
-                                          num_y_samples + num_y_samples / 2);
+
   return psnr;
 }
 
@@ -66,80 +58,101 @@
 VideoCodecAnalyzer::VideoCodecAnalyzer(
     rtc::TaskQueue& task_queue,
     ReferenceVideoSource* reference_video_source)
-    : task_queue_(task_queue), reference_video_source_(reference_video_source) {
+    : task_queue_(task_queue),
+      reference_video_source_(reference_video_source),
+      num_frames_(0) {
   sequence_checker_.Detach();
 }
 
 void VideoCodecAnalyzer::StartEncode(const VideoFrame& input_frame) {
-  int64_t encode_started_ns = rtc::TimeNanos();
+  int64_t encode_start_us = rtc::TimeMicros();
   task_queue_.PostTask(
-      [this, timestamp_rtp = input_frame.timestamp(), encode_started_ns]() {
+      [this, timestamp_rtp = input_frame.timestamp(), encode_start_us]() {
         RTC_DCHECK_RUN_ON(&sequence_checker_);
-        VideoCodecTestStats::FrameStatistics* fs =
-            stats_.GetOrAddFrame(timestamp_rtp, /*spatial_idx=*/0);
-        fs->encode_start_ns = encode_started_ns;
+
+        RTC_CHECK(frame_num_.find(timestamp_rtp) == frame_num_.end());
+        frame_num_[timestamp_rtp] = num_frames_++;
+        int frame_num = frame_num_[timestamp_rtp];
+
+        VideoCodecStats::Frame* fs =
+            stats_.AddFrame(frame_num, timestamp_rtp, /*spatial_idx=*/0);
+        fs->encode_start = Timestamp::Micros(encode_start_us);
       });
 }
 
 void VideoCodecAnalyzer::FinishEncode(const EncodedImage& frame) {
-  int64_t encode_finished_ns = rtc::TimeNanos();
+  int64_t encode_finished_us = rtc::TimeMicros();
 
   task_queue_.PostTask([this, timestamp_rtp = frame.Timestamp(),
                         spatial_idx = frame.SpatialIndex().value_or(0),
                         temporal_idx = frame.TemporalIndex().value_or(0),
-                        frame_type = frame._frameType, qp = frame.qp_,
-                        frame_size_bytes = frame.size(), encode_finished_ns]() {
+                        width = frame._encodedWidth,
+                        height = frame._encodedHeight,
+                        frame_type = frame._frameType,
+                        size_bytes = frame.size(), qp = frame.qp_,
+                        encode_finished_us]() {
     RTC_DCHECK_RUN_ON(&sequence_checker_);
-    VideoCodecTestStats::FrameStatistics* fs =
-        stats_.GetOrAddFrame(timestamp_rtp, spatial_idx);
-    VideoCodecTestStats::FrameStatistics* fs_base =
-        stats_.GetOrAddFrame(timestamp_rtp, 0);
 
-    fs->encode_start_ns = fs_base->encode_start_ns;
+    if (spatial_idx > 0) {
+      VideoCodecStats::Frame* fs0 =
+          stats_.GetFrame(timestamp_rtp, /*spatial_idx=*/0);
+      VideoCodecStats::Frame* fs =
+          stats_.AddFrame(fs0->frame_num, timestamp_rtp, spatial_idx);
+      fs->encode_start = fs0->encode_start;
+    }
+
+    VideoCodecStats::Frame* fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
     fs->spatial_idx = spatial_idx;
     fs->temporal_idx = temporal_idx;
-    fs->frame_type = frame_type;
+    fs->width = width;
+    fs->height = height;
+    fs->size_bytes = static_cast<int>(size_bytes);
     fs->qp = qp;
-
-    fs->encode_time_us = (encode_finished_ns - fs->encode_start_ns) /
-                         rtc::kNumNanosecsPerMicrosec;
-    fs->length_bytes = frame_size_bytes;
-
-    fs->encoding_successful = true;
+    fs->keyframe = frame_type == VideoFrameType::kVideoFrameKey;
+    fs->encode_time = Timestamp::Micros(encode_finished_us) - fs->encode_start;
+    fs->encoded = true;
   });
 }
 
 void VideoCodecAnalyzer::StartDecode(const EncodedImage& frame) {
-  int64_t decode_start_ns = rtc::TimeNanos();
+  int64_t decode_start_us = rtc::TimeMicros();
   task_queue_.PostTask([this, timestamp_rtp = frame.Timestamp(),
                         spatial_idx = frame.SpatialIndex().value_or(0),
-                        frame_size_bytes = frame.size(), decode_start_ns]() {
+                        size_bytes = frame.size(), decode_start_us]() {
     RTC_DCHECK_RUN_ON(&sequence_checker_);
-    VideoCodecTestStats::FrameStatistics* fs =
-        stats_.GetOrAddFrame(timestamp_rtp, spatial_idx);
-    if (fs->length_bytes == 0) {
-      // In encode-decode test the frame size is set in EncodeFinished. In
-      // decode-only test set it here.
-      fs->length_bytes = frame_size_bytes;
+
+    VideoCodecStats::Frame* fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
+    if (fs == nullptr) {
+      if (frame_num_.find(timestamp_rtp) == frame_num_.end()) {
+        frame_num_[timestamp_rtp] = num_frames_++;
+      }
+      int frame_num = frame_num_[timestamp_rtp];
+
+      fs = stats_.AddFrame(frame_num, timestamp_rtp, spatial_idx);
+      fs->spatial_idx = spatial_idx;
+      fs->size_bytes = size_bytes;
     }
-    fs->decode_start_ns = decode_start_ns;
+
+    fs->decode_start = Timestamp::Micros(decode_start_us);
   });
 }
 
 void VideoCodecAnalyzer::FinishDecode(const VideoFrame& frame,
                                       int spatial_idx) {
-  int64_t decode_finished_ns = rtc::TimeNanos();
+  int64_t decode_finished_us = rtc::TimeMicros();
   task_queue_.PostTask([this, timestamp_rtp = frame.timestamp(), spatial_idx,
                         width = frame.width(), height = frame.height(),
-                        decode_finished_ns]() {
+                        decode_finished_us]() {
     RTC_DCHECK_RUN_ON(&sequence_checker_);
-    VideoCodecTestStats::FrameStatistics* fs =
-        stats_.GetFrameWithTimestamp(timestamp_rtp, spatial_idx);
-    fs->decode_time_us = (decode_finished_ns - fs->decode_start_ns) /
-                         rtc::kNumNanosecsPerMicrosec;
-    fs->decoded_width = width;
-    fs->decoded_height = height;
-    fs->decoding_successful = true;
+    VideoCodecStats::Frame* fs = stats_.GetFrame(timestamp_rtp, spatial_idx);
+    fs->decode_time = Timestamp::Micros(decode_finished_us) - fs->decode_start;
+
+    if (!fs->encoded) {
+      fs->width = width;
+      fs->height = height;
+    }
+
+    fs->decoded = true;
   });
 
   if (reference_video_source_ != nullptr) {
@@ -158,24 +171,20 @@
           ref_frame.video_frame_buffer()->ToI420();
 
       Psnr psnr = CalcPsnr(*decoded_buffer, *ref_buffer);
-      VideoCodecTestStats::FrameStatistics* fs =
-          this->stats_.GetFrameWithTimestamp(timestamp_rtp, spatial_idx);
-      fs->psnr_y = static_cast<float>(psnr.y);
-      fs->psnr_u = static_cast<float>(psnr.u);
-      fs->psnr_v = static_cast<float>(psnr.v);
-      fs->psnr = static_cast<float>(psnr.yuv);
 
-      fs->quality_analysis_successful = true;
+      VideoCodecStats::Frame* fs =
+          this->stats_.GetFrame(timestamp_rtp, spatial_idx);
+      fs->psnr = psnr;
     });
   }
 }
 
-std::unique_ptr<VideoCodecTestStats> VideoCodecAnalyzer::GetStats() {
-  std::unique_ptr<VideoCodecTestStats> stats;
+std::unique_ptr<VideoCodecStats> VideoCodecAnalyzer::GetStats() {
+  std::unique_ptr<VideoCodecStats> stats;
   rtc::Event ready;
   task_queue_.PostTask([this, &stats, &ready]() mutable {
     RTC_DCHECK_RUN_ON(&sequence_checker_);
-    stats.reset(new VideoCodecTestStatsImpl(stats_));
+    stats.reset(new VideoCodecStatsImpl(stats_));
     ready.Set();
   });
   ready.Wait(rtc::Event::kForever);
diff --git a/modules/video_coding/codecs/test/video_codec_analyzer.h b/modules/video_coding/codecs/test/video_codec_analyzer.h
index 63a864e..8cb1b54 100644
--- a/modules/video_coding/codecs/test/video_codec_analyzer.h
+++ b/modules/video_coding/codecs/test/video_codec_analyzer.h
@@ -11,14 +11,16 @@
 #ifndef MODULES_VIDEO_CODING_CODECS_TEST_VIDEO_CODEC_ANALYZER_H_
 #define MODULES_VIDEO_CODING_CODECS_TEST_VIDEO_CODEC_ANALYZER_H_
 
+#include <map>
 #include <memory>
 
 #include "absl/types/optional.h"
 #include "api/sequence_checker.h"
+#include "api/test/video_codec_tester.h"
 #include "api/video/encoded_image.h"
 #include "api/video/resolution.h"
 #include "api/video/video_frame.h"
-#include "modules/video_coding/codecs/test/videocodec_test_stats_impl.h"
+#include "modules/video_coding/codecs/test/video_codec_stats_impl.h"
 #include "rtc_base/synchronization/mutex.h"
 #include "rtc_base/system/no_unique_address.h"
 #include "rtc_base/task_queue_for_test.h"
@@ -50,12 +52,21 @@
 
   void FinishDecode(const VideoFrame& frame, int spatial_idx);
 
-  std::unique_ptr<VideoCodecTestStats> GetStats();
+  std::unique_ptr<VideoCodecStats> GetStats();
 
  protected:
   rtc::TaskQueue& task_queue_;
+
   ReferenceVideoSource* const reference_video_source_;
-  VideoCodecTestStatsImpl stats_ RTC_GUARDED_BY(sequence_checker_);
+
+  VideoCodecStatsImpl stats_ RTC_GUARDED_BY(sequence_checker_);
+
+  // Map from RTP timestamp to frame number.
+  std::map<uint32_t, int> frame_num_ RTC_GUARDED_BY(sequence_checker_);
+
+  // Processed frames counter.
+  int num_frames_ RTC_GUARDED_BY(sequence_checker_);
+
   RTC_NO_UNIQUE_ADDRESS SequenceChecker sequence_checker_;
 };
 
diff --git a/modules/video_coding/codecs/test/video_codec_analyzer_unittest.cc b/modules/video_coding/codecs/test/video_codec_analyzer_unittest.cc
index 3f9de6d..133a60f 100644
--- a/modules/video_coding/codecs/test/video_codec_analyzer_unittest.cc
+++ b/modules/video_coding/codecs/test/video_codec_analyzer_unittest.cc
@@ -22,9 +22,10 @@
 namespace {
 using ::testing::Return;
 using ::testing::Values;
+using Psnr = VideoCodecStats::Frame::Psnr;
 
-const size_t kTimestamp = 3000;
-const size_t kSpatialIdx = 2;
+const uint32_t kTimestamp = 3000;
+const int kSpatialIdx = 2;
 
 class MockReferenceVideoSource
     : public VideoCodecAnalyzer::ReferenceVideoSource {
@@ -57,17 +58,17 @@
 }
 }  // namespace
 
-TEST(VideoCodecAnalyzerTest, EncodeStartedCreatesFrameStats) {
+TEST(VideoCodecAnalyzerTest, StartEncode) {
   TaskQueueForTest task_queue;
   VideoCodecAnalyzer analyzer(task_queue);
   analyzer.StartEncode(CreateVideoFrame(kTimestamp));
 
-  auto fs = analyzer.GetStats()->GetFrameStatistics();
+  auto fs = analyzer.GetStats()->Slice();
   EXPECT_EQ(1u, fs.size());
-  EXPECT_EQ(fs[0].rtp_timestamp, kTimestamp);
+  EXPECT_EQ(fs[0].timestamp_rtp, kTimestamp);
 }
 
-TEST(VideoCodecAnalyzerTest, EncodeFinishedUpdatesFrameStats) {
+TEST(VideoCodecAnalyzerTest, FinishEncode) {
   TaskQueueForTest task_queue;
   VideoCodecAnalyzer analyzer(task_queue);
   analyzer.StartEncode(CreateVideoFrame(kTimestamp));
@@ -75,47 +76,35 @@
   EncodedImage encoded_frame = CreateEncodedImage(kTimestamp, kSpatialIdx);
   analyzer.FinishEncode(encoded_frame);
 
-  auto fs = analyzer.GetStats()->GetFrameStatistics();
+  auto fs = analyzer.GetStats()->Slice();
   EXPECT_EQ(2u, fs.size());
-  EXPECT_TRUE(fs[1].encoding_successful);
+  EXPECT_EQ(kSpatialIdx, fs[1].spatial_idx);
 }
 
-TEST(VideoCodecAnalyzerTest, DecodeStartedNoFrameStatsCreatesFrameStats) {
+TEST(VideoCodecAnalyzerTest, StartDecode) {
   TaskQueueForTest task_queue;
   VideoCodecAnalyzer analyzer(task_queue);
   analyzer.StartDecode(CreateEncodedImage(kTimestamp, kSpatialIdx));
 
-  auto fs = analyzer.GetStats()->GetFrameStatistics();
+  auto fs = analyzer.GetStats()->Slice();
   EXPECT_EQ(1u, fs.size());
-  EXPECT_EQ(fs[0].rtp_timestamp, kTimestamp);
+  EXPECT_EQ(kTimestamp, fs[0].timestamp_rtp);
 }
 
-TEST(VideoCodecAnalyzerTest, DecodeStartedFrameStatsExistsReusesFrameStats) {
-  TaskQueueForTest task_queue;
-  VideoCodecAnalyzer analyzer(task_queue);
-  analyzer.StartEncode(CreateVideoFrame(kTimestamp));
-  analyzer.StartDecode(CreateEncodedImage(kTimestamp, /*spatial_idx=*/0));
-
-  auto fs = analyzer.GetStats()->GetFrameStatistics();
-  EXPECT_EQ(1u, fs.size());
-}
-
-TEST(VideoCodecAnalyzerTest, DecodeFinishedUpdatesFrameStats) {
+TEST(VideoCodecAnalyzerTest, FinishDecode) {
   TaskQueueForTest task_queue;
   VideoCodecAnalyzer analyzer(task_queue);
   analyzer.StartDecode(CreateEncodedImage(kTimestamp, kSpatialIdx));
   VideoFrame decoded_frame = CreateVideoFrame(kTimestamp);
   analyzer.FinishDecode(decoded_frame, kSpatialIdx);
 
-  auto fs = analyzer.GetStats()->GetFrameStatistics();
+  auto fs = analyzer.GetStats()->Slice();
   EXPECT_EQ(1u, fs.size());
-
-  EXPECT_TRUE(fs[0].decoding_successful);
-  EXPECT_EQ(static_cast<int>(fs[0].decoded_width), decoded_frame.width());
-  EXPECT_EQ(static_cast<int>(fs[0].decoded_height), decoded_frame.height());
+  EXPECT_EQ(decoded_frame.width(), fs[0].width);
+  EXPECT_EQ(decoded_frame.height(), fs[0].height);
 }
 
-TEST(VideoCodecAnalyzerTest, DecodeFinishedComputesPsnr) {
+TEST(VideoCodecAnalyzerTest, ReferenceVideoSource) {
   TaskQueueForTest task_queue;
   MockReferenceVideoSource reference_video_source;
   VideoCodecAnalyzer analyzer(task_queue, &reference_video_source);
@@ -129,12 +118,14 @@
       CreateVideoFrame(kTimestamp, /*value_y=*/1, /*value_u=*/2, /*value_v=*/3),
       kSpatialIdx);
 
-  auto fs = analyzer.GetStats()->GetFrameStatistics();
+  auto fs = analyzer.GetStats()->Slice();
   EXPECT_EQ(1u, fs.size());
+  EXPECT_TRUE(fs[0].psnr.has_value());
 
-  EXPECT_NEAR(fs[0].psnr_y, 48, 1);
-  EXPECT_NEAR(fs[0].psnr_u, 42, 1);
-  EXPECT_NEAR(fs[0].psnr_v, 38, 1);
+  const Psnr& psnr = *fs[0].psnr;
+  EXPECT_NEAR(psnr.y, 48, 1);
+  EXPECT_NEAR(psnr.u, 42, 1);
+  EXPECT_NEAR(psnr.v, 38, 1);
 }
 
 }  // namespace test
diff --git a/modules/video_coding/codecs/test/video_codec_stats_impl.cc b/modules/video_coding/codecs/test/video_codec_stats_impl.cc
new file mode 100644
index 0000000..f56debf
--- /dev/null
+++ b/modules/video_coding/codecs/test/video_codec_stats_impl.cc
@@ -0,0 +1,185 @@
+/*
+ *  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 "modules/video_coding/codecs/test/video_codec_stats_impl.h"
+
+#include <algorithm>
+
+#include "api/numerics/samples_stats_counter.h"
+#include "api/test/metrics/global_metrics_logger_and_exporter.h"
+#include "rtc_base/checks.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+using Frame = VideoCodecStats::Frame;
+using Stream = VideoCodecStats::Stream;
+
+constexpr Frequency k90kHz = Frequency::Hertz(90000);
+}  // namespace
+
+std::vector<Frame> VideoCodecStatsImpl::Slice(
+    absl::optional<Filter> filter) const {
+  std::vector<Frame> frames;
+  for (const auto& [frame_id, f] : frames_) {
+    if (filter.has_value()) {
+      if (filter->first_frame.has_value() &&
+          f.frame_num < *filter->first_frame) {
+        continue;
+      }
+      if (filter->last_frame.has_value() && f.frame_num > *filter->last_frame) {
+        continue;
+      }
+      if (filter->spatial_idx.has_value() &&
+          f.spatial_idx != *filter->spatial_idx) {
+        continue;
+      }
+      if (filter->temporal_idx.has_value() &&
+          f.temporal_idx > *filter->temporal_idx) {
+        continue;
+      }
+    }
+    frames.push_back(f);
+  }
+  return frames;
+}
+
+Stream VideoCodecStatsImpl::Aggregate(
+    const std::vector<Frame>& frames,
+    absl::optional<DataRate> bitrate,
+    absl::optional<Frequency> framerate) const {
+  std::vector<Frame> superframes = Merge(frames);
+
+  Stream stream;
+  stream.num_frames = static_cast<int>(superframes.size());
+
+  for (const auto& f : superframes) {
+    Timestamp time = Timestamp::Micros((f.timestamp_rtp / k90kHz).us());
+    // TODO(webrtc:14852): Add AddSample(double value, Timestamp time) method to
+    // SamplesStatsCounter.
+    stream.decode_time_us.AddSample(SamplesStatsCounter::StatsSample(
+        {.value = static_cast<double>(f.decode_time.us()), .time = time}));
+
+    if (f.psnr) {
+      stream.psnr.y.AddSample(
+          SamplesStatsCounter::StatsSample({.value = f.psnr->y, .time = time}));
+      stream.psnr.u.AddSample(
+          SamplesStatsCounter::StatsSample({.value = f.psnr->u, .time = time}));
+      stream.psnr.v.AddSample(
+          SamplesStatsCounter::StatsSample({.value = f.psnr->v, .time = time}));
+    }
+
+    if (f.keyframe) {
+      ++stream.num_keyframes;
+    }
+
+    // TODO(webrtc:14852): Aggregate other metrics.
+  }
+
+  return stream;
+}
+
+void VideoCodecStatsImpl::LogMetrics(MetricsLogger* logger,
+                                     const Stream& stream,
+                                     std::string test_case_name) const {
+  logger->LogMetric("width", test_case_name, stream.width, Unit::kCount,
+                    webrtc::test::ImprovementDirection::kBiggerIsBetter);
+  // TODO(webrtc:14852): Log other metrics.
+}
+
+Frame* VideoCodecStatsImpl::AddFrame(int frame_num,
+                                     uint32_t timestamp_rtp,
+                                     int spatial_idx) {
+  Frame frame;
+  frame.frame_num = frame_num;
+  frame.timestamp_rtp = timestamp_rtp;
+  frame.spatial_idx = spatial_idx;
+
+  FrameId frame_id;
+  frame_id.frame_num = frame_num;
+  frame_id.spatial_idx = spatial_idx;
+
+  RTC_CHECK(frames_.find(frame_id) == frames_.end())
+      << "Frame with frame_num=" << frame_num
+      << " and spatial_idx=" << spatial_idx << " already exists";
+
+  frames_[frame_id] = frame;
+
+  if (frame_num_.find(timestamp_rtp) == frame_num_.end()) {
+    frame_num_[timestamp_rtp] = frame_num;
+  }
+
+  return &frames_[frame_id];
+}
+
+Frame* VideoCodecStatsImpl::GetFrame(uint32_t timestamp_rtp, int spatial_idx) {
+  if (frame_num_.find(timestamp_rtp) == frame_num_.end()) {
+    return nullptr;
+  }
+
+  FrameId frame_id;
+  frame_id.frame_num = frame_num_[timestamp_rtp];
+  frame_id.spatial_idx = spatial_idx;
+
+  if (frames_.find(frame_id) == frames_.end()) {
+    return nullptr;
+  }
+
+  return &frames_[frame_id];
+}
+
+std::vector<Frame> VideoCodecStatsImpl::Merge(
+    const std::vector<Frame>& frames) const {
+  std::vector<Frame> superframes;
+  // Map from frame_num to index in `superframes` vector.
+  std::map<int, int> index;
+
+  for (const auto& f : frames) {
+    if (f.encoded == false && f.decoded == false) {
+      continue;
+    }
+
+    if (index.find(f.frame_num) == index.end()) {
+      index[f.frame_num] = static_cast<int>(superframes.size());
+      superframes.push_back(f);
+      continue;
+    }
+
+    Frame& sf = superframes[index[f.frame_num]];
+
+    sf.width = std::max(sf.width, f.width);
+    sf.height = std::max(sf.height, f.height);
+    sf.size_bytes += f.size_bytes;
+    sf.keyframe |= f.keyframe;
+
+    sf.encode_time = std::max(sf.encode_time, f.encode_time);
+    sf.decode_time += f.decode_time;
+
+    if (f.spatial_idx > sf.spatial_idx) {
+      if (f.qp) {
+        sf.qp = f.qp;
+      }
+      if (f.psnr) {
+        sf.psnr = f.psnr;
+      }
+    }
+
+    sf.spatial_idx = std::max(sf.spatial_idx, f.spatial_idx);
+    sf.temporal_idx = std::max(sf.temporal_idx, f.temporal_idx);
+
+    sf.encoded |= f.encoded;
+    sf.decoded |= f.decoded;
+  }
+
+  return superframes;
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/modules/video_coding/codecs/test/video_codec_stats_impl.h b/modules/video_coding/codecs/test/video_codec_stats_impl.h
new file mode 100644
index 0000000..951f6e0
--- /dev/null
+++ b/modules/video_coding/codecs/test/video_codec_stats_impl.h
@@ -0,0 +1,77 @@
+/*
+ *  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 MODULES_VIDEO_CODING_CODECS_TEST_VIDEO_CODEC_STATS_IMPL_H_
+#define MODULES_VIDEO_CODING_CODECS_TEST_VIDEO_CODEC_STATS_IMPL_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/test/video_codec_stats.h"
+
+namespace webrtc {
+namespace test {
+
+// Implementation of `VideoCodecStats`. This class is not thread-safe.
+class VideoCodecStatsImpl : public VideoCodecStats {
+ public:
+  std::vector<Frame> Slice(
+      absl::optional<Filter> filter = absl::nullopt) const override;
+
+  Stream Aggregate(
+      const std::vector<Frame>& frames,
+      absl::optional<DataRate> bitrate = absl::nullopt,
+      absl::optional<Frequency> framerate = absl::nullopt) const override;
+
+  void LogMetrics(MetricsLogger* logger,
+                  const Stream& stream,
+                  std::string test_case_name) const override;
+
+  // Creates new frame, caches it and returns raw pointer to it.
+  Frame* AddFrame(int frame_num, uint32_t timestamp_rtp, int spatial_idx);
+
+  // Returns raw pointers to requested frame. If frame does not exist, returns
+  // `nullptr`.
+  Frame* GetFrame(uint32_t timestamp_rtp, int spatial_idx);
+
+ private:
+  struct FrameId {
+    int frame_num;
+    int spatial_idx;
+
+    bool operator==(const FrameId& o) const {
+      return frame_num == o.frame_num && spatial_idx == o.spatial_idx;
+    }
+
+    bool operator<(const FrameId& o) const {
+      if (frame_num < o.frame_num)
+        return true;
+      if (spatial_idx < o.spatial_idx)
+        return true;
+      return false;
+    }
+  };
+
+  // Merges frame stats from different spatial layers and returns vector of
+  // superframes.
+  std::vector<Frame> Merge(const std::vector<Frame>& frames) const;
+
+  // Map from RTP timestamp to frame number (`Frame::frame_num`).
+  std::map<uint32_t, int> frame_num_;
+
+  std::map<FrameId, Frame> frames_;
+};
+
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // MODULES_VIDEO_CODING_CODECS_TEST_VIDEO_CODEC_STATS_IMPL_H_
diff --git a/modules/video_coding/codecs/test/video_codec_stats_impl_unittest.cc b/modules/video_coding/codecs/test/video_codec_stats_impl_unittest.cc
new file mode 100644
index 0000000..f55ca3b
--- /dev/null
+++ b/modules/video_coding/codecs/test/video_codec_stats_impl_unittest.cc
@@ -0,0 +1,140 @@
+/*
+ *  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 "modules/video_coding/codecs/test/video_codec_stats_impl.h"
+
+#include "absl/types/optional.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+
+namespace webrtc {
+namespace test {
+
+namespace {
+using ::testing::Return;
+using ::testing::Values;
+}  // namespace
+
+TEST(VideoCodecStatsImpl, AddFrame) {
+  VideoCodecStatsImpl stats;
+  VideoCodecStatsImpl::Frame* fs =
+      stats.AddFrame(/*frame_num=*/0, /*timestamp_rtp=*/0, /*spatial_idx=*/0);
+  EXPECT_NE(nullptr, fs);
+  fs = stats.GetFrame(/*timestamp_rtp=*/0, /*spatial_idx=*/0);
+  EXPECT_NE(nullptr, fs);
+}
+
+TEST(VideoCodecStatsImpl, GetFrame) {
+  VideoCodecStatsImpl stats;
+  stats.AddFrame(/*frame_num=*/0, /*timestamp_rtp=*/0, /*spatial_idx=*/0);
+  VideoCodecStatsImpl::Frame* fs =
+      stats.GetFrame(/*timestamp_rtp=*/0, /*spatial_idx=*/0);
+  EXPECT_NE(nullptr, fs);
+}
+
+struct VideoCodecStatsSlicingTestParams {
+  VideoCodecStats::Filter slicer;
+  std::vector<VideoCodecStats::Frame> expected;
+};
+
+class VideoCodecStatsSlicingTest
+    : public ::testing::TestWithParam<VideoCodecStatsSlicingTestParams> {
+ public:
+  void SetUp() {
+    // TODO(ssikin): Hard codec 2x2 table would be better.
+    for (int frame_num = 0; frame_num < 2; ++frame_num) {
+      for (int spatial_idx = 0; spatial_idx < 2; ++spatial_idx) {
+        uint32_t timestamp_rtp = 3000 * frame_num;
+        VideoCodecStats::Frame* f =
+            stats_.AddFrame(frame_num, timestamp_rtp, spatial_idx);
+        f->temporal_idx = frame_num;
+      }
+    }
+  }
+
+ protected:
+  VideoCodecStatsImpl stats_;
+};
+
+TEST_P(VideoCodecStatsSlicingTest, Slice) {
+  VideoCodecStatsSlicingTestParams test_params = GetParam();
+  std::vector<VideoCodecStats::Frame> frames = stats_.Slice(test_params.slicer);
+  EXPECT_EQ(frames.size(), test_params.expected.size());
+}
+
+INSTANTIATE_TEST_SUITE_P(All,
+                         VideoCodecStatsSlicingTest,
+                         ::testing::ValuesIn({VideoCodecStatsSlicingTestParams(
+                             {.slicer = {.first_frame = 0, .last_frame = 1},
+                              .expected = {{.frame_num = 0},
+                                           {.frame_num = 1},
+                                           {.frame_num = 0},
+                                           {.frame_num = 1}}})}));
+
+struct VideoCodecStatsAggregationTestParams {
+  VideoCodecStats::Filter slicer;
+  struct Expected {
+    double decode_time_us;
+  } expected;
+};
+
+class VideoCodecStatsAggregationTest
+    : public ::testing::TestWithParam<VideoCodecStatsAggregationTestParams> {
+ public:
+  void SetUp() {
+    // TODO(ssikin): Hard codec 2x2 table would be better. Share with
+    // VideoCodecStatsSlicingTest
+    for (int frame_num = 0; frame_num < 2; ++frame_num) {
+      for (int spatial_idx = 0; spatial_idx < 2; ++spatial_idx) {
+        uint32_t timestamp_rtp = 3000 * frame_num;
+        VideoCodecStats::Frame* f =
+            stats_.AddFrame(frame_num, timestamp_rtp, spatial_idx);
+        f->temporal_idx = frame_num;
+        f->decode_time = TimeDelta::Micros(spatial_idx * 10 + frame_num);
+        f->encoded = true;
+        f->decoded = true;
+      }
+    }
+  }
+
+ protected:
+  VideoCodecStatsImpl stats_;
+};
+
+TEST_P(VideoCodecStatsAggregationTest, Aggregate) {
+  VideoCodecStatsAggregationTestParams test_params = GetParam();
+  std::vector<VideoCodecStats::Frame> frames = stats_.Slice(test_params.slicer);
+  VideoCodecStats::Stream stream = stats_.Aggregate(frames);
+  EXPECT_EQ(stream.decode_time_us.GetAverage(),
+            test_params.expected.decode_time_us);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    All,
+    VideoCodecStatsAggregationTest,
+    ::testing::ValuesIn(
+        {VideoCodecStatsAggregationTestParams(
+             {.slicer = {},
+              .expected = {.decode_time_us = (0.0 + 1.0 + 10.0 + 11.0) / 2}}),
+         // Slicing on frame number
+         VideoCodecStatsAggregationTestParams(
+             {.slicer = {.first_frame = 1, .last_frame = 1},
+              .expected = {.decode_time_us = 1.0 + 11.0}}),
+         // Slice on spatial index
+         VideoCodecStatsAggregationTestParams(
+             {.slicer = {.spatial_idx = 1},
+              .expected = {.decode_time_us = (10.0 + 11.0) / 2}}),
+         // Slice on temporal index
+         VideoCodecStatsAggregationTestParams(
+             {.slicer = {.temporal_idx = 0},
+              .expected = {.decode_time_us = 0.0 + 10.0}})}));
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/modules/video_coding/codecs/test/video_codec_test.cc b/modules/video_coding/codecs/test/video_codec_test.cc
index bd4c8e0..d2ad743 100644
--- a/modules/video_coding/codecs/test/video_codec_test.cc
+++ b/modules/video_coding/codecs/test/video_codec_test.cc
@@ -43,7 +43,6 @@
 namespace {
 using ::testing::Combine;
 using ::testing::Values;
-using Layer = std::pair<int, int>;
 
 struct VideoInfo {
   std::string name;
@@ -56,6 +55,23 @@
   std::string decoder;
 };
 
+struct LayerId {
+  int spatial_idx;
+  int temporal_idx;
+
+  bool operator==(const LayerId& o) const {
+    return spatial_idx == o.spatial_idx && temporal_idx == o.temporal_idx;
+  }
+
+  bool operator<(const LayerId& o) const {
+    if (spatial_idx < o.spatial_idx)
+      return true;
+    if (temporal_idx < o.temporal_idx)
+      return true;
+    return false;
+  }
+};
+
 struct EncodingSettings {
   ScalabilityMode scalability_mode;
   // Spatial layer resolution.
@@ -63,7 +79,7 @@
   // Top temporal layer frame rate.
   Frequency framerate;
   // Bitrate of spatial and temporal layers.
-  std::map<Layer, DataRate> bitrate;
+  std::map<LayerId, DataRate> bitrate;
 };
 
 struct EncodingTestSettings {
@@ -94,7 +110,8 @@
     .scalability_mode = ScalabilityMode::kL1T1,
     .resolution = {{0, {.width = 320, .height = 180}}},
     .framerate = Frequency::Hertz(30),
-    .bitrate = {{Layer(0, 0), DataRate::KilobitsPerSec(64)}}};
+    .bitrate = {
+        {{.spatial_idx = 0, .temporal_idx = 0}, DataRate::KilobitsPerSec(64)}}};
 
 const EncodingTestSettings kConstantRateQvga64Kbps30Fps = {
     .name = "ConstantRateQvga64Kbps30Fps",
@@ -277,10 +294,10 @@
         ScalabilityModeToNumSpatialLayers(es.scalability_mode);
     for (int sidx = 0; sidx < num_spatial_layers; ++sidx) {
       for (int tidx = 0; tidx < num_temporal_layers; ++tidx) {
-        RTC_CHECK(es.bitrate.find(Layer(sidx, tidx)) != es.bitrate.end())
+        LayerId layer_id = {.spatial_idx = sidx, .temporal_idx = tidx};
+        RTC_CHECK(es.bitrate.find(layer_id) != es.bitrate.end())
             << "Bitrate for layer S=" << sidx << " T=" << tidx << " is not set";
-        rc.bitrate.SetBitrate(sidx, tidx,
-                              es.bitrate.at(Layer(sidx, tidx)).bps());
+        rc.bitrate.SetBitrate(sidx, tidx, es.bitrate.at(layer_id).bps());
       }
     }
 
@@ -405,7 +422,7 @@
 };
 
 TEST_P(EncodeDecodeTest, DISABLED_TestEncodeDecode) {
-  std::unique_ptr<VideoCodecTestStats> stats = tester_->RunEncodeDecodeTest(
+  std::unique_ptr<VideoCodecStats> stats = tester_->RunEncodeDecodeTest(
       std::move(video_source_), std::move(encoder_), std::move(decoder_),
       test_params_.encoder_settings, test_params_.decoder_settings);
 
@@ -415,13 +432,11 @@
     int last_frame = std::next(fs) != frame_settings.end()
                          ? std::next(fs)->first - 1
                          : test_params_.encoding_settings.num_frames - 1;
-
-    const EncodingSettings& encoding_settings = fs->second;
-    auto metrics = stats->CalcVideoStatistic(
-        first_frame, last_frame, encoding_settings.bitrate.rbegin()->second,
-        encoding_settings.framerate);
-
-    EXPECT_GE(metrics.avg_psnr_y,
+    VideoCodecStats::Filter slicer = {.first_frame = first_frame,
+                                      .last_frame = last_frame};
+    std::vector<VideoCodecStats::Frame> frames = stats->Slice(slicer);
+    VideoCodecStats::Stream stream = stats->Aggregate(frames);
+    EXPECT_GE(stream.psnr.y.GetAverage(),
               test_params_.quality_expectations.min_apsnr_y);
   }
 }
diff --git a/modules/video_coding/codecs/test/video_codec_tester_impl.cc b/modules/video_coding/codecs/test/video_codec_tester_impl.cc
index 3000c1a..09c5978 100644
--- a/modules/video_coding/codecs/test/video_codec_tester_impl.cc
+++ b/modules/video_coding/codecs/test/video_codec_tester_impl.cc
@@ -171,9 +171,11 @@
     task_queue_.PostDelayedTask(
         [this, frame] {
           analyzer_->StartDecode(frame);
-          decoder_->Decode(frame, [this](const VideoFrame& decoded_frame) {
-            this->analyzer_->FinishDecode(decoded_frame, /*spatial_idx=*/0);
-          });
+          decoder_->Decode(
+              frame, [this, spatial_idx = frame.SpatialIndex().value_or(0)](
+                         const VideoFrame& decoded_frame) {
+                this->analyzer_->FinishDecode(decoded_frame, spatial_idx);
+              });
         },
         pacer_.Delay(timestamp));
   }
@@ -244,7 +246,7 @@
   }
 }
 
-std::unique_ptr<VideoCodecTestStats> VideoCodecTesterImpl::RunDecodeTest(
+std::unique_ptr<VideoCodecStats> VideoCodecTesterImpl::RunDecodeTest(
     std::unique_ptr<CodedVideoSource> video_source,
     std::unique_ptr<Decoder> decoder,
     const DecoderSettings& decoder_settings) {
@@ -266,7 +268,7 @@
   return perf_analyzer.GetStats();
 }
 
-std::unique_ptr<VideoCodecTestStats> VideoCodecTesterImpl::RunEncodeTest(
+std::unique_ptr<VideoCodecStats> VideoCodecTesterImpl::RunEncodeTest(
     std::unique_ptr<RawVideoSource> video_source,
     std::unique_ptr<Encoder> encoder,
     const EncoderSettings& encoder_settings) {
@@ -290,7 +292,7 @@
   return perf_analyzer.GetStats();
 }
 
-std::unique_ptr<VideoCodecTestStats> VideoCodecTesterImpl::RunEncodeDecodeTest(
+std::unique_ptr<VideoCodecStats> VideoCodecTesterImpl::RunEncodeDecodeTest(
     std::unique_ptr<RawVideoSource> video_source,
     std::unique_ptr<Encoder> encoder,
     std::unique_ptr<Decoder> decoder,
diff --git a/modules/video_coding/codecs/test/video_codec_tester_impl.h b/modules/video_coding/codecs/test/video_codec_tester_impl.h
index b64adeb..4ac61ee 100644
--- a/modules/video_coding/codecs/test/video_codec_tester_impl.h
+++ b/modules/video_coding/codecs/test/video_codec_tester_impl.h
@@ -25,17 +25,17 @@
   VideoCodecTesterImpl();
   explicit VideoCodecTesterImpl(TaskQueueFactory* task_queue_factory);
 
-  std::unique_ptr<VideoCodecTestStats> RunDecodeTest(
+  std::unique_ptr<VideoCodecStats> RunDecodeTest(
       std::unique_ptr<CodedVideoSource> video_source,
       std::unique_ptr<Decoder> decoder,
       const DecoderSettings& decoder_settings) override;
 
-  std::unique_ptr<VideoCodecTestStats> RunEncodeTest(
+  std::unique_ptr<VideoCodecStats> RunEncodeTest(
       std::unique_ptr<RawVideoSource> video_source,
       std::unique_ptr<Encoder> encoder,
       const EncoderSettings& encoder_settings) override;
 
-  std::unique_ptr<VideoCodecTestStats> RunEncodeDecodeTest(
+  std::unique_ptr<VideoCodecStats> RunEncodeDecodeTest(
       std::unique_ptr<RawVideoSource> video_source,
       std::unique_ptr<Encoder> encoder,
       std::unique_ptr<Decoder> decoder,
diff --git a/modules/video_coding/codecs/test/video_codec_tester_impl_unittest.cc b/modules/video_coding/codecs/test/video_codec_tester_impl_unittest.cc
index 29fb006..409e813 100644
--- a/modules/video_coding/codecs/test/video_codec_tester_impl_unittest.cc
+++ b/modules/video_coding/codecs/test/video_codec_tester_impl_unittest.cc
@@ -172,12 +172,11 @@
   auto fs = tester
                 .RunEncodeTest(std::move(video_source), std::move(encoder),
                                encoder_settings)
-                ->GetFrameStatistics();
+                ->Slice();
   ASSERT_EQ(fs.size(), num_frames_);
 
   for (size_t i = 0; i < fs.size(); ++i) {
-    int encode_start_ms = (fs[i].encode_start_ns - fs[0].encode_start_ns) /
-                          rtc::kNumNanosecsPerMillisec;
+    int encode_start_ms = (fs[i].encode_start - fs[0].encode_start).ms();
     EXPECT_NEAR(encode_start_ms, expected_frame_start_ms_[i], 10);
   }
 }
@@ -206,12 +205,11 @@
   auto fs = tester
                 .RunDecodeTest(std::move(video_source), std::move(decoder),
                                decoder_settings)
-                ->GetFrameStatistics();
+                ->Slice();
   ASSERT_EQ(fs.size(), num_frames_);
 
   for (size_t i = 0; i < fs.size(); ++i) {
-    int decode_start_ms = (fs[i].decode_start_ns - fs[0].decode_start_ns) /
-                          rtc::kNumNanosecsPerMillisec;
+    int decode_start_ms = (fs[i].decode_start - fs[0].decode_start).ms();
     EXPECT_NEAR(decode_start_ms, expected_frame_start_ms_[i], 10);
   }
 }
diff --git a/modules/video_coding/codecs/test/videoprocessor.h b/modules/video_coding/codecs/test/videoprocessor.h
index 0a5fdf8..502fa3d 100644
--- a/modules/video_coding/codecs/test/videoprocessor.h
+++ b/modules/video_coding/codecs/test/videoprocessor.h
@@ -50,6 +50,7 @@
 // measure times properly.
 // The class processes a frame at the time for the configured input file.
 // It maintains state of where in the source input file the processing is at.
+// TODO(webrtc:14852): Deprecated in favor VideoCodecTester.
 class VideoProcessor {
  public:
   using VideoDecoderList = std::vector<std::unique_ptr<VideoDecoder>>;