Adds optional video quality metrics to scenario tests.

Bug: webrtc:9510
Change-Id: I448e7156cc8f56930f58c4d25bd167df83a2ba85
Reviewed-on: https://webrtc-review.googlesource.com/c/114885
Commit-Queue: Sebastian Jansson <srte@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#26065}
diff --git a/test/scenario/BUILD.gn b/test/scenario/BUILD.gn
index b401947..9ae0ae6 100644
--- a/test/scenario/BUILD.gn
+++ b/test/scenario/BUILD.gn
@@ -16,12 +16,16 @@
       "audio_stream.h",
       "call_client.cc",
       "call_client.h",
+      "call_client.h",
       "column_printer.cc",
       "column_printer.h",
       "hardware_codecs.cc",
       "hardware_codecs.h",
       "network_node.cc",
       "network_node.h",
+      "quality_info.h",
+      "quality_stats.cc",
+      "quality_stats.h",
       "scenario.cc",
       "scenario.h",
       "scenario_config.cc",
@@ -90,6 +94,7 @@
       "../../system_wrappers",
       "../../system_wrappers:field_trial",
       "../../video",
+      "//third_party/abseil-cpp/absl/memory",
       "//third_party/abseil-cpp/absl/types:optional",
     ]
     if (is_android) {
@@ -125,4 +130,25 @@
       "//third_party/abseil-cpp/absl/memory",
     ]
   }
+  rtc_source_set("scenario_slow_tests") {
+    testonly = true
+    sources = [
+      "quality_stats_unittest.cc",
+    ]
+    if (!build_with_chromium && is_clang) {
+      suppressed_configs += [ "//build/config/clang:find_bad_constructs" ]
+    }
+    deps = [
+      ":scenario",
+      "../../logging:mocks",
+      "../../rtc_base:checks",
+      "../../rtc_base:rtc_base_approved",
+      "../../system_wrappers",
+      "../../system_wrappers:field_trial",
+      "../../test:field_trial",
+      "../../test:test_support",
+      "//testing/gmock",
+      "//third_party/abseil-cpp/absl/memory",
+    ]
+  }
 }
diff --git a/test/scenario/call_client.cc b/test/scenario/call_client.cc
index 27b8402..ccfcd55 100644
--- a/test/scenario/call_client.cc
+++ b/test/scenario/call_client.cc
@@ -134,17 +134,18 @@
 }
 
 CallClient::CallClient(Clock* clock,
+                       std::string name,
                        std::string log_filename,
                        CallClientConfig config)
     : clock_(clock),
+      name_(name),
       network_controller_factory_(log_filename, config.transport),
       fake_audio_setup_(InitAudio()),
       call_(CreateCall(config,
                        &network_controller_factory_,
                        fake_audio_setup_.audio_state)),
       transport_(clock_, call_.get()),
-      header_parser_(RtpHeaderParser::Create()) {
-}  // namespace test
+      header_parser_(RtpHeaderParser::Create()) {}
 
 CallClient::~CallClient() {
   delete header_parser_;
diff --git a/test/scenario/call_client.h b/test/scenario/call_client.h
index 69b7237..aa5fa93 100644
--- a/test/scenario/call_client.h
+++ b/test/scenario/call_client.h
@@ -58,7 +58,10 @@
 // stream session.
 class CallClient : public NetworkReceiverInterface {
  public:
-  CallClient(Clock* clock, std::string log_filename, CallClientConfig config);
+  CallClient(Clock* clock,
+             std::string name,
+             std::string log_filename,
+             CallClientConfig config);
   RTC_DISALLOW_COPY_AND_ASSIGN(CallClient);
 
   ~CallClient();
@@ -89,6 +92,7 @@
   void AddExtensions(std::vector<RtpExtension> extensions);
 
   Clock* clock_;
+  const std::string name_;
   LoggingNetworkControllerFactory network_controller_factory_;
   CallClientFakeAudio fake_audio_setup_;
   std::unique_ptr<Call> call_;
diff --git a/test/scenario/quality_info.h b/test/scenario/quality_info.h
new file mode 100644
index 0000000..9f17af0
--- /dev/null
+++ b/test/scenario/quality_info.h
@@ -0,0 +1,27 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_QUALITY_INFO_H_
+#define TEST_SCENARIO_QUALITY_INFO_H_
+
+#include "api/units/timestamp.h"
+
+namespace webrtc {
+namespace test {
+struct VideoFrameQualityInfo {
+  Timestamp capture_time;
+  Timestamp received_capture_time;
+  Timestamp render_time;
+  int width;
+  int height;
+  double psnr;
+};
+}  // namespace test
+}  // namespace webrtc
+#endif  // TEST_SCENARIO_QUALITY_INFO_H_
diff --git a/test/scenario/quality_stats.cc b/test/scenario/quality_stats.cc
new file mode 100644
index 0000000..2c73437
--- /dev/null
+++ b/test/scenario/quality_stats.cc
@@ -0,0 +1,183 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/quality_stats.h"
+
+#include <utility>
+
+#include "common_video/libyuv/include/webrtc_libyuv.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/event.h"
+
+namespace webrtc {
+namespace test {
+
+VideoQualityAnalyzer::VideoQualityAnalyzer(
+    std::string filename_or_empty,
+    std::function<void(const VideoFrameQualityInfo&)> frame_info_handler)
+    : task_queue_("VideoAnalyzer") {
+  if (!filename_or_empty.empty()) {
+    output_file_ = fopen(filename_or_empty.c_str(), "w");
+    RTC_CHECK(output_file_);
+    PrintHeaders();
+    frame_info_handlers_.push_back(
+        [this](const VideoFrameQualityInfo& info) { PrintFrameInfo(info); });
+  }
+  if (frame_info_handler)
+    frame_info_handlers_.push_back(frame_info_handler);
+}
+
+VideoQualityAnalyzer::~VideoQualityAnalyzer() {
+  rtc::Event event;
+  task_queue_.PostTask([&event] { event.Set(); });
+  event.Wait(rtc::Event::kForever);
+  if (output_file_)
+    fclose(output_file_);
+}
+
+void VideoQualityAnalyzer::OnCapturedFrame(const VideoFrame& frame) {
+  VideoFrame copy = frame;
+  task_queue_.PostTask([this, copy] {
+    if (!first_capture_ntp_time_ms_)
+      first_capture_ntp_time_ms_ = copy.ntp_time_ms();
+    captured_frames_.push_back(std::move(copy));
+  });
+}
+
+void VideoQualityAnalyzer::OnDecodedFrame(const VideoFrame& frame) {
+  VideoFrame decoded = frame;
+  RTC_CHECK(frame.ntp_time_ms());
+  RTC_CHECK(frame.timestamp());
+  task_queue_.PostTask([this, decoded] {
+    // If first frame never is received, this value will be wrong. However, that
+    // is something that is very unlikely to happen.
+    if (!first_decode_rtp_timestamp_)
+      first_decode_rtp_timestamp_ = decoded.timestamp();
+    RTC_CHECK(!captured_frames_.empty());
+    int64_t decoded_capture_time_ms = DecodedFrameCaptureTimeOffsetMs(decoded);
+    while (CapturedFrameCaptureTimeOffsetMs(captured_frames_.front()) <
+           decoded_capture_time_ms) {
+      VideoFrame lost = std::move(captured_frames_.front());
+      captured_frames_.pop_front();
+      VideoFrameQualityInfo lost_info =
+          VideoFrameQualityInfo{Timestamp::us(lost.timestamp_us()),
+                                Timestamp::PlusInfinity(),
+                                Timestamp::PlusInfinity(),
+                                lost.width(),
+                                lost.height(),
+                                NAN};
+      for (auto& handler : frame_info_handlers_)
+        handler(lost_info);
+      RTC_CHECK(!captured_frames_.empty());
+    }
+    RTC_CHECK(!captured_frames_.empty());
+    RTC_CHECK(CapturedFrameCaptureTimeOffsetMs(captured_frames_.front()) ==
+              DecodedFrameCaptureTimeOffsetMs(decoded));
+    VideoFrame captured = std::move(captured_frames_.front());
+    captured_frames_.pop_front();
+
+    VideoFrameQualityInfo decoded_info =
+        VideoFrameQualityInfo{Timestamp::us(captured.timestamp_us()),
+                              Timestamp::ms(decoded.timestamp() / 90.0),
+                              Timestamp::ms(decoded.render_time_ms()),
+                              decoded.width(),
+                              decoded.height(),
+                              I420PSNR(&captured, &decoded)};
+    for (auto& handler : frame_info_handlers_)
+      handler(decoded_info);
+  });
+}
+
+bool VideoQualityAnalyzer::Active() const {
+  return !frame_info_handlers_.empty();
+}
+
+int64_t VideoQualityAnalyzer::DecodedFrameCaptureTimeOffsetMs(
+    const VideoFrame& decoded) const {
+  // Assumes that the underlying resolution is ms.
+  // Note that we intentinally allow wraparound. The code is incorrect for
+  // durations of more than UINT32_MAX/90 ms.
+  RTC_DCHECK(first_decode_rtp_timestamp_);
+  return (decoded.timestamp() - *first_decode_rtp_timestamp_) / 90;
+}
+
+int64_t VideoQualityAnalyzer::CapturedFrameCaptureTimeOffsetMs(
+    const VideoFrame& captured) const {
+  RTC_DCHECK(first_capture_ntp_time_ms_);
+  return captured.ntp_time_ms() - *first_capture_ntp_time_ms_;
+}
+
+void VideoQualityAnalyzer::PrintHeaders() {
+  fprintf(output_file_, "capt recv_capt render width height psnr\n");
+}
+
+void VideoQualityAnalyzer::PrintFrameInfo(const VideoFrameQualityInfo& sample) {
+  fprintf(output_file_, "%.3f %.3f %.3f %i %i %.3f\n",
+          sample.capture_time.seconds<double>(),
+          sample.received_capture_time.seconds<double>(),
+          sample.render_time.seconds<double>(), sample.width, sample.height,
+          sample.psnr);
+}
+
+void VideoQualityStats::HandleFrameInfo(VideoFrameQualityInfo sample) {
+  total++;
+  if (sample.render_time.IsInfinite()) {
+    ++lost;
+  } else {
+    ++valid;
+    end_to_end_seconds.AddSample(
+        (sample.render_time - sample.capture_time).seconds<double>());
+    psnr.AddSample(sample.psnr);
+  }
+}
+
+ForwardingCapturedFrameTap::ForwardingCapturedFrameTap(
+    const Clock* clock,
+    VideoQualityAnalyzer* analyzer,
+    rtc::VideoSourceInterface<VideoFrame>* source)
+    : clock_(clock), analyzer_(analyzer), source_(source) {}
+
+ForwardingCapturedFrameTap::~ForwardingCapturedFrameTap() {}
+
+void ForwardingCapturedFrameTap::OnFrame(const VideoFrame& frame) {
+  RTC_CHECK(sink_);
+  VideoFrame copy = frame;
+  if (frame.ntp_time_ms() == 0)
+    copy.set_ntp_time_ms(clock_->CurrentNtpInMilliseconds());
+  copy.set_timestamp(copy.ntp_time_ms() * 90);
+  analyzer_->OnCapturedFrame(copy);
+  sink_->OnFrame(copy);
+}
+void ForwardingCapturedFrameTap::OnDiscardedFrame() {
+  RTC_CHECK(sink_);
+  discarded_count_++;
+  sink_->OnDiscardedFrame();
+}
+
+void ForwardingCapturedFrameTap::AddOrUpdateSink(
+    VideoSinkInterface<VideoFrame>* sink,
+    const rtc::VideoSinkWants& wants) {
+  sink_ = sink;
+  source_->AddOrUpdateSink(this, wants);
+}
+void ForwardingCapturedFrameTap::RemoveSink(
+    VideoSinkInterface<VideoFrame>* sink) {
+  source_->RemoveSink(this);
+  sink_ = nullptr;
+}
+
+DecodedFrameTap::DecodedFrameTap(VideoQualityAnalyzer* analyzer)
+    : analyzer_(analyzer) {}
+
+void DecodedFrameTap::OnFrame(const VideoFrame& frame) {
+  analyzer_->OnDecodedFrame(frame);
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/quality_stats.h b/test/scenario/quality_stats.h
new file mode 100644
index 0000000..5e0ee48
--- /dev/null
+++ b/test/scenario/quality_stats.h
@@ -0,0 +1,110 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_QUALITY_STATS_H_
+#define TEST_SCENARIO_QUALITY_STATS_H_
+
+#include <deque>
+#include <string>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/units/timestamp.h"
+#include "api/video/video_frame.h"
+#include "api/video/video_sink_interface.h"
+#include "api/video/video_source_interface.h"
+#include "rtc_base/task_queue.h"
+#include "rtc_base/timeutils.h"
+#include "system_wrappers/include/clock.h"
+#include "test/scenario/quality_info.h"
+#include "test/scenario/scenario_config.h"
+#include "test/statistics.h"
+
+namespace webrtc {
+namespace test {
+
+class VideoQualityAnalyzer {
+ public:
+  VideoQualityAnalyzer(
+      std::string filename_or_empty,
+      std::function<void(const VideoFrameQualityInfo&)> frame_info_handler);
+  ~VideoQualityAnalyzer();
+  void OnCapturedFrame(const VideoFrame& frame);
+  void OnDecodedFrame(const VideoFrame& frame);
+  void Synchronize();
+  bool Active() const;
+  const Clock* clock();
+
+ private:
+  int64_t DecodedFrameCaptureTimeOffsetMs(const VideoFrame& decoded) const;
+  int64_t CapturedFrameCaptureTimeOffsetMs(const VideoFrame& captured) const;
+  void PrintHeaders();
+  void PrintFrameInfo(const VideoFrameQualityInfo& sample);
+  std::vector<std::function<void(const VideoFrameQualityInfo&)>>
+      frame_info_handlers_;
+  std::deque<VideoFrame> captured_frames_;
+  absl::optional<int64_t> first_capture_ntp_time_ms_;
+  absl::optional<uint32_t> first_decode_rtp_timestamp_;
+  FILE* output_file_ = nullptr;
+  rtc::TaskQueue task_queue_;
+};
+
+struct VideoQualityStats {
+  int total = 0;
+  int valid = 0;
+  int lost = 0;
+  Statistics end_to_end_seconds;
+  Statistics frame_size;
+  Statistics psnr;
+  Statistics ssim;
+
+  void HandleFrameInfo(VideoFrameQualityInfo sample);
+};
+
+class ForwardingCapturedFrameTap
+    : public rtc::VideoSinkInterface<VideoFrame>,
+      public rtc::VideoSourceInterface<VideoFrame> {
+ public:
+  ForwardingCapturedFrameTap(const Clock* clock,
+                             VideoQualityAnalyzer* analyzer,
+                             rtc::VideoSourceInterface<VideoFrame>* source);
+  ForwardingCapturedFrameTap(ForwardingCapturedFrameTap&) = delete;
+  ForwardingCapturedFrameTap& operator=(ForwardingCapturedFrameTap&) = delete;
+  ~ForwardingCapturedFrameTap();
+
+  // VideoSinkInterface interface
+  void OnFrame(const VideoFrame& frame) override;
+  void OnDiscardedFrame() override;
+
+  // VideoSourceInterface interface
+  void AddOrUpdateSink(VideoSinkInterface<VideoFrame>* sink,
+                       const rtc::VideoSinkWants& wants) override;
+  void RemoveSink(VideoSinkInterface<VideoFrame>* sink) override;
+  VideoFrame PopFrame();
+
+ private:
+  const Clock* clock_;
+  VideoQualityAnalyzer* const analyzer_;
+  rtc::VideoSourceInterface<VideoFrame>* const source_;
+  VideoSinkInterface<VideoFrame>* sink_;
+  int discarded_count_ = 0;
+};
+
+class DecodedFrameTap : public rtc::VideoSinkInterface<VideoFrame> {
+ public:
+  explicit DecodedFrameTap(VideoQualityAnalyzer* analyzer);
+  // VideoSinkInterface interface
+  void OnFrame(const VideoFrame& frame) override;
+
+ private:
+  VideoQualityAnalyzer* const analyzer_;
+};
+}  // namespace test
+}  // namespace webrtc
+#endif  // TEST_SCENARIO_QUALITY_STATS_H_
diff --git a/test/scenario/quality_stats_unittest.cc b/test/scenario/quality_stats_unittest.cc
new file mode 100644
index 0000000..e3806cf
--- /dev/null
+++ b/test/scenario/quality_stats_unittest.cc
@@ -0,0 +1,62 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/gtest.h"
+#include "test/scenario/scenario.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+VideoStreamConfig AnalyzerVideoConfig(VideoQualityStats* stats) {
+  VideoStreamConfig config;
+  config.encoder.codec = VideoStreamConfig::Encoder::Codec::kVideoCodecVP8;
+  config.encoder.implementation =
+      VideoStreamConfig::Encoder::Implementation::kSoftware;
+  config.analyzer.frame_quality_handler = [stats](VideoFrameQualityInfo info) {
+    stats->HandleFrameInfo(info);
+  };
+  return config;
+}
+}  // namespace
+
+TEST(ScenarioAnalyzerTest, PsnrIsHighWhenNetworkIsGood) {
+  VideoQualityStats stats;
+  {
+    Scenario s;
+    NetworkNodeConfig good_network;
+    good_network.simulation.bandwidth = DataRate::kbps(1000);
+    auto route = s.CreateRoutes(s.CreateClient("caller", CallClientConfig()),
+                                {s.CreateSimulationNode(good_network)},
+                                s.CreateClient("callee", CallClientConfig()),
+                                {s.CreateSimulationNode(NetworkNodeConfig())});
+    s.CreateVideoStream(route->forward(), AnalyzerVideoConfig(&stats));
+    s.RunFor(TimeDelta::seconds(1));
+  }
+  EXPECT_GT(stats.psnr.Mean(), 46);
+}
+
+TEST(ScenarioAnalyzerTest, PsnrIsLowWhenNetworkIsBad) {
+  VideoQualityStats stats;
+  {
+    Scenario s;
+    NetworkNodeConfig bad_network;
+    bad_network.simulation.bandwidth = DataRate::kbps(100);
+    bad_network.simulation.loss_rate = 0.02;
+    auto route = s.CreateRoutes(s.CreateClient("caller", CallClientConfig()),
+                                {s.CreateSimulationNode(bad_network)},
+                                s.CreateClient("callee", CallClientConfig()),
+                                {s.CreateSimulationNode(NetworkNodeConfig())});
+
+    s.CreateVideoStream(route->forward(), AnalyzerVideoConfig(&stats));
+    s.RunFor(TimeDelta::seconds(2));
+  }
+  EXPECT_LT(stats.psnr.Mean(), 40);
+}
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/scenario.cc b/test/scenario/scenario.cc
index 6b0990d..161a832 100644
--- a/test/scenario/scenario.cc
+++ b/test/scenario/scenario.cc
@@ -107,7 +107,8 @@
 
 CallClient* Scenario::CreateClient(std::string name, CallClientConfig config) {
   RTC_DCHECK(real_time_mode_);
-  CallClient* client = new CallClient(clock_, GetFullPathOrEmpty(name), config);
+  CallClient* client =
+      new CallClient(clock_, name, GetFullPathOrEmpty(name), config);
   if (config.transport.state_log_interval.IsFinite()) {
     Every(config.transport.state_log_interval, [this, client]() {
       client->network_controller_factory_.LogCongestionControllerStats(Now());
@@ -266,8 +267,12 @@
 VideoStreamPair* Scenario::CreateVideoStream(
     std::pair<CallClient*, CallClient*> clients,
     VideoStreamConfig config) {
-  video_streams_.emplace_back(
-      new VideoStreamPair(clients.first, clients.second, config));
+  std::string quality_log_file_name;
+  if (config.analyzer.log_to_file)
+    quality_log_file_name =
+        GetFullPathOrEmpty(clients.first->name_ + ".video_quality.txt");
+  video_streams_.emplace_back(new VideoStreamPair(
+      clients.first, clients.second, config, quality_log_file_name));
   return video_streams_.back().get();
 }
 
diff --git a/test/scenario/scenario_config.h b/test/scenario/scenario_config.h
index 15db7bd..682a1cb 100644
--- a/test/scenario/scenario_config.h
+++ b/test/scenario/scenario_config.h
@@ -21,6 +21,7 @@
 #include "api/units/time_delta.h"
 #include "common_types.h"  // NOLINT(build/include)
 #include "test/frame_generator.h"
+#include "test/scenario/quality_info.h"
 
 namespace webrtc {
 namespace test {
@@ -138,6 +139,10 @@
   struct Renderer {
     enum Type { kFake } type = kFake;
   };
+  struct analyzer {
+    bool log_to_file = false;
+    std::function<void(const VideoFrameQualityInfo&)> frame_quality_handler;
+  } analyzer;
 };
 
 struct AudioStreamConfig {
diff --git a/test/scenario/video_stream.cc b/test/scenario/video_stream.cc
index 11c5ef1..b68858b 100644
--- a/test/scenario/video_stream.cc
+++ b/test/scenario/video_stream.cc
@@ -12,6 +12,7 @@
 #include <algorithm>
 #include <utility>
 
+#include "absl/memory/memory.h"
 #include "api/test/video/function_video_encoder_factory.h"
 #include "api/video/builtin_video_bitrate_allocator_factory.h"
 #include "media/base/mediaconstants.h"
@@ -175,7 +176,8 @@
 
 SendVideoStream::SendVideoStream(CallClient* sender,
                                  VideoStreamConfig config,
-                                 Transport* send_transport)
+                                 Transport* send_transport,
+                                 VideoQualityAnalyzer* analyzer)
     : sender_(sender), config_(config) {
   for (size_t i = 0; i < config.encoder.num_simulcast_streams; ++i) {
     ssrcs_.push_back(sender->GetNextVideoSsrc());
@@ -244,9 +246,20 @@
 
   send_stream_ = sender_->call_->CreateVideoSendStream(
       std::move(send_config), std::move(encoder_config));
+  std::vector<std::function<void(const VideoFrameQualityInfo&)> >
+      frame_info_handlers;
+  if (config.analyzer.frame_quality_handler)
+    frame_info_handlers.push_back(config.analyzer.frame_quality_handler);
 
-  send_stream_->SetSource(video_capturer_.get(),
-                          config.encoder.degradation_preference);
+  if (analyzer->Active()) {
+    frame_tap_.reset(new ForwardingCapturedFrameTap(sender_->clock_, analyzer,
+                                                    video_capturer_.get()));
+    send_stream_->SetSource(frame_tap_.get(),
+                            config.encoder.degradation_preference);
+  } else {
+    send_stream_->SetSource(video_capturer_.get(),
+                            config.encoder.degradation_preference);
+  }
 }
 
 SendVideoStream::~SendVideoStream() {
@@ -282,7 +295,6 @@
   RTC_CHECK(frame_generator_)
       << "Framerate change only implemented for generators";
   frame_generator_->ChangeFramerate(framerate);
-
 }
 
 VideoSendStream::Stats SendVideoStream::GetStats() const {
@@ -311,9 +323,14 @@
                                        VideoStreamConfig config,
                                        SendVideoStream* send_stream,
                                        size_t chosen_stream,
-                                       Transport* feedback_transport)
+                                       Transport* feedback_transport,
+                                       VideoQualityAnalyzer* analyzer)
     : receiver_(receiver), config_(config) {
-  renderer_ = absl::make_unique<FakeVideoRenderer>();
+  if (analyzer->Active()) {
+    renderer_ = absl::make_unique<DecodedFrameTap>(analyzer);
+  } else {
+    renderer_ = absl::make_unique<FakeVideoRenderer>();
+  }
   VideoReceiveStream::Config recv_config(feedback_transport);
   recv_config.rtp.remb = !config.stream.packet_feedback;
   recv_config.rtp.transport_cc = config.stream.packet_feedback;
@@ -384,14 +401,17 @@
 
 VideoStreamPair::VideoStreamPair(CallClient* sender,
                                  CallClient* receiver,
-                                 VideoStreamConfig config)
+                                 VideoStreamConfig config,
+                                 std::string quality_log_file_name)
     : config_(config),
-      send_stream_(sender, config, &sender->transport_),
+      analyzer_(quality_log_file_name, config.analyzer.frame_quality_handler),
+      send_stream_(sender, config, &sender->transport_, &analyzer_),
       receive_stream_(receiver,
                       config,
                       &send_stream_,
                       /*chosen_stream=*/0,
-                      &receiver->transport_) {}
+                      &receiver->transport_,
+                      &analyzer_) {}
 
 }  // namespace test
 }  // namespace webrtc
diff --git a/test/scenario/video_stream.h b/test/scenario/video_stream.h
index ebf9c95..6604576 100644
--- a/test/scenario/video_stream.h
+++ b/test/scenario/video_stream.h
@@ -19,6 +19,7 @@
 #include "test/scenario/call_client.h"
 #include "test/scenario/column_printer.h"
 #include "test/scenario/network_node.h"
+#include "test/scenario/quality_stats.h"
 #include "test/scenario/scenario_config.h"
 #include "test/test_video_capturer.h"
 
@@ -43,7 +44,8 @@
   // Handles RTCP feedback for this stream.
   SendVideoStream(CallClient* sender,
                   VideoStreamConfig config,
-                  Transport* send_transport);
+                  Transport* send_transport,
+                  VideoQualityAnalyzer* analyzer);
 
   rtc::CriticalSection crit_;
   std::vector<uint32_t> ssrcs_;
@@ -55,6 +57,7 @@
   std::vector<test::FakeEncoder*> fake_encoders_ RTC_GUARDED_BY(crit_);
   std::unique_ptr<VideoBitrateAllocatorFactory> bitrate_allocator_factory_;
   std::unique_ptr<TestVideoCapturer> video_capturer_;
+  std::unique_ptr<ForwardingCapturedFrameTap> frame_tap_;
   FrameGeneratorCapturer* frame_generator_ = nullptr;
   int next_local_network_id_ = 0;
   int next_remote_network_id_ = 0;
@@ -74,11 +77,12 @@
                      VideoStreamConfig config,
                      SendVideoStream* send_stream,
                      size_t chosen_stream,
-                     Transport* feedback_transport);
+                     Transport* feedback_transport,
+                     VideoQualityAnalyzer* analyzer);
 
   VideoReceiveStream* receive_stream_ = nullptr;
   FlexfecReceiveStream* flecfec_stream_ = nullptr;
-  std::unique_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> renderer_;
+  std::unique_ptr<rtc::VideoSinkInterface<VideoFrame>> renderer_;
   CallClient* const receiver_;
   const VideoStreamConfig config_;
   std::unique_ptr<VideoDecoderFactory> decoder_factory_;
@@ -93,15 +97,18 @@
   ~VideoStreamPair();
   SendVideoStream* send() { return &send_stream_; }
   ReceiveVideoStream* receive() { return &receive_stream_; }
+  VideoQualityAnalyzer* analyzer() { return &analyzer_; }
 
  private:
   friend class Scenario;
   VideoStreamPair(CallClient* sender,
                   CallClient* receiver,
-                  VideoStreamConfig config);
+                  VideoStreamConfig config,
+                  std::string quality_log_file_name);
 
   const VideoStreamConfig config_;
 
+  VideoQualityAnalyzer analyzer_;
   SendVideoStream send_stream_;
   ReceiveVideoStream receive_stream_;
 };