Adding support for simulcast and spatial layers into VideoQualityTest

This is a re-land of https://codereview.webrtc.org/1353263005/
which was reverted because of perf-regressions. Changes since that CL:

* Change LayerFilteringTransport to send a padding packet instead of
  dropping it for data that should be filtered out. This prevents
  confusion due to changed sequence numbers.

* Changed timing of stats poller thread in VideoAnalyzer. Startup was
  racy wrt initializion of send_stream_.

* Minor formatting issues.

PERF NOTE: This change will affect some performance numbers slightly.
In particular, {encode_frame_rate, encode_time_ms,
encode_usage_percent, media_bitrate_bps} will change due to timing
of the measurements.

BUG=
R=pbos@webrtc.org
TBR=mflodman@webrtc.org

Review URL: https://codereview.webrtc.org/1412233003

Cr-Commit-Position: refs/heads/master@{#10483}
diff --git a/webrtc/video/full_stack.cc b/webrtc/video/full_stack.cc
index 8511b82..2810cd6 100644
--- a/webrtc/video/full_stack.cc
+++ b/webrtc/video/full_stack.cc
@@ -23,6 +23,15 @@
   }
 };
 
+// VideoQualityTest::Params params = {
+//   { ... },      // Common.
+//   { ... },      // Video-specific settings.
+//   { ... },      // Screenshare-specific settings.
+//   { ... },      // Analyzer settings.
+//   pipe,         // FakeNetworkPipe::Config
+//   { ... },      // Spatial scalability.
+//   logs          // bool
+// };
 
 TEST_F(FullStackTest, ParisQcifWithoutPacketLoss) {
   VideoQualityTest::Params paris_qcif = {
@@ -120,16 +129,16 @@
 
 TEST_F(FullStackTest, ScreenshareSlidesVP8_2TL) {
   VideoQualityTest::Params screenshare = {
-      {1850, 1110, 5, 50000, 200000, 2000000, "VP8", 2, 400000},
-      {},          // Video-specific.
-      {true, 10},  // Screenshare-specific.
+      {1850, 1110, 5, 50000, 200000, 2000000, "VP8", 2, 1, 400000},
+      {},
+      {true, 10},
       {"screenshare_slides", 0.0, 0.0, kFullStackTestDurationSecs}};
   RunTest(screenshare);
 }
 
 TEST_F(FullStackTest, ScreenshareSlidesVP8_2TL_Scroll) {
   VideoQualityTest::Params config = {
-      {1850, 1110 / 2, 5, 50000, 200000, 2000000, "VP8", 2, 400000},
+      {1850, 1110 / 2, 5, 50000, 200000, 2000000, "VP8", 2, 1, 400000},
       {},
       {true, 10, 2},
       {"screenshare_slides_scrolling", 0.0, 0.0, kFullStackTestDurationSecs}};
@@ -138,7 +147,7 @@
 
 TEST_F(FullStackTest, ScreenshareSlidesVP9_2TL) {
   VideoQualityTest::Params screenshare = {
-      {1850, 1110, 5, 50000, 200000, 2000000, "VP9", 2, 400000},
+      {1850, 1110, 5, 50000, 200000, 2000000, "VP9", 2, 1, 400000},
       {},
       {true, 10},
       {"screenshare_slides_vp9_2tl", 0.0, 0.0, kFullStackTestDurationSecs}};
diff --git a/webrtc/video/screenshare_loopback.cc b/webrtc/video/screenshare_loopback.cc
index 9897783..6479aa4e 100644
--- a/webrtc/video/screenshare_loopback.cc
+++ b/webrtc/video/screenshare_loopback.cc
@@ -20,6 +20,7 @@
 namespace webrtc {
 namespace flags {
 
+// Flags common with video loopback, with different default values.
 DEFINE_int32(width, 1850, "Video width (crops source).");
 size_t Width() {
   return static_cast<size_t>(FLAGS_width);
@@ -35,21 +36,6 @@
   return static_cast<int>(FLAGS_fps);
 }
 
-DEFINE_int32(slide_change_interval,
-             10,
-             "Interval (in seconds) between simulated slide changes.");
-int SlideChangeInterval() {
-  return static_cast<int>(FLAGS_slide_change_interval);
-}
-
-DEFINE_int32(
-    scroll_duration,
-    0,
-    "Duration (in seconds) during which a slide will be scrolled into place.");
-int ScrollDuration() {
-  return static_cast<int>(FLAGS_scroll_duration);
-}
-
 DEFINE_int32(min_bitrate, 50, "Call and stream min bitrate in kbps.");
 int MinBitrateKbps() {
   return static_cast<int>(FLAGS_min_bitrate);
@@ -71,28 +57,43 @@
 }
 
 DEFINE_int32(num_temporal_layers, 2, "Number of temporal layers to use.");
-size_t NumTemporalLayers() {
-  return static_cast<size_t>(FLAGS_num_temporal_layers);
+int NumTemporalLayers() {
+  return static_cast<int>(FLAGS_num_temporal_layers);
 }
 
-DEFINE_int32(
-    tl_discard_threshold,
-    0,
-    "Discard TLs with id greater or equal the threshold. 0 to disable.");
-size_t TLDiscardThreshold() {
-  return static_cast<size_t>(FLAGS_tl_discard_threshold);
-}
-
-DEFINE_int32(min_transmit_bitrate, 400, "Min transmit bitrate incl. padding.");
-int MinTransmitBitrateKbps() {
-  return FLAGS_min_transmit_bitrate;
-}
-
+// Flags common with video loopback, with equal default values.
 DEFINE_string(codec, "VP8", "Video codec to use.");
 std::string Codec() {
   return static_cast<std::string>(FLAGS_codec);
 }
 
+DEFINE_int32(selected_tl,
+             -1,
+             "Temporal layer to show or analyze. -1 to disable filtering.");
+int SelectedTL() {
+  return static_cast<int>(FLAGS_selected_tl);
+}
+
+DEFINE_int32(
+    duration,
+    0,
+    "Duration of the test in seconds. If 0, rendered will be shown instead.");
+int DurationSecs() {
+  return static_cast<int>(FLAGS_duration);
+}
+
+DEFINE_string(output_filename, "", "Target graph data filename.");
+std::string OutputFilename() {
+  return static_cast<std::string>(FLAGS_output_filename);
+}
+
+DEFINE_string(graph_title,
+              "",
+              "If empty, title will be generated automatically.");
+std::string GraphTitle() {
+  return static_cast<std::string>(FLAGS_graph_title);
+}
+
 DEFINE_int32(loss_percent, 0, "Percentage of packets randomly lost.");
 int LossPercent() {
   return static_cast<int>(FLAGS_loss_percent);
@@ -124,21 +125,53 @@
   return static_cast<int>(FLAGS_std_propagation_delay_ms);
 }
 
+DEFINE_int32(selected_stream, 0, "ID of the stream to show or analyze.");
+int SelectedStream() {
+  return static_cast<int>(FLAGS_selected_stream);
+}
+
+DEFINE_int32(num_spatial_layers, 1, "Number of spatial layers to use.");
+int NumSpatialLayers() {
+  return static_cast<int>(FLAGS_num_spatial_layers);
+}
+
+DEFINE_int32(selected_sl,
+             -1,
+             "Spatial layer to show or analyze. -1 to disable filtering.");
+int SelectedSL() {
+  return static_cast<int>(FLAGS_selected_sl);
+}
+
+DEFINE_string(stream0,
+              "",
+              "Comma separated values describing VideoStream for stream #0.");
+std::string Stream0() {
+  return static_cast<std::string>(FLAGS_stream0);
+}
+
+DEFINE_string(stream1,
+              "",
+              "Comma separated values describing VideoStream for stream #1.");
+std::string Stream1() {
+  return static_cast<std::string>(FLAGS_stream1);
+}
+
+DEFINE_string(sl0,
+              "",
+              "Comma separated values describing SpatialLayer for layer #0.");
+std::string SL0() {
+  return static_cast<std::string>(FLAGS_sl0);
+}
+
+DEFINE_string(sl1,
+              "",
+              "Comma separated values describing SpatialLayer for layer #1.");
+std::string SL1() {
+  return static_cast<std::string>(FLAGS_sl1);
+}
+
 DEFINE_bool(logs, false, "print logs to stderr");
 
-DEFINE_string(
-    output_filename,
-    "",
-    "Name of a target graph data file. If set, no preview will be shown.");
-std::string OutputFilename() {
-  return static_cast<std::string>(FLAGS_output_filename);
-}
-
-DEFINE_int32(duration, 60, "Duration of the test in seconds.");
-int DurationSecs() {
-  return static_cast<int>(FLAGS_duration);
-}
-
 DEFINE_bool(send_side_bwe, true, "Use send-side bandwidth estimation");
 
 DEFINE_string(
@@ -148,6 +181,28 @@
     "E.g. running with --force_fieldtrials=WebRTC-FooFeature/Enable/"
     " will assign the group Enable to field trial WebRTC-FooFeature. Multiple "
     "trials are separated by \"/\"");
+
+// Screenshare-specific flags.
+DEFINE_int32(min_transmit_bitrate, 400, "Min transmit bitrate incl. padding.");
+int MinTransmitBitrateKbps() {
+  return FLAGS_min_transmit_bitrate;
+}
+
+DEFINE_int32(slide_change_interval,
+             10,
+             "Interval (in seconds) between simulated slide changes.");
+int SlideChangeInterval() {
+  return static_cast<int>(FLAGS_slide_change_interval);
+}
+
+DEFINE_int32(
+    scroll_duration,
+    0,
+    "Duration (in seconds) during which a slide will be scrolled into place.");
+int ScrollDuration() {
+  return static_cast<int>(FLAGS_scroll_duration);
+}
+
 }  // namespace flags
 
 void Loopback() {
@@ -167,20 +222,32 @@
       {flags::Width(), flags::Height(), flags::Fps(),
        flags::MinBitrateKbps() * 1000, flags::TargetBitrateKbps() * 1000,
        flags::MaxBitrateKbps() * 1000, flags::Codec(),
-       flags::NumTemporalLayers(), flags::MinTransmitBitrateKbps() * 1000,
-       call_bitrate_config, flags::TLDiscardThreshold(),
+       flags::NumTemporalLayers(), flags::SelectedTL(),
+       flags::MinTransmitBitrateKbps() * 1000, call_bitrate_config,
        flags::FLAGS_send_side_bwe},
       {},  // Video specific.
       {true, flags::SlideChangeInterval(), flags::ScrollDuration()},
-      {"screenshare", 0.0, 0.0, flags::DurationSecs(), flags::OutputFilename()},
+      {"screenshare", 0.0, 0.0, flags::DurationSecs(), flags::OutputFilename(),
+       flags::GraphTitle()},
       pipe_config,
       flags::FLAGS_logs};
 
+  std::vector<std::string> stream_descriptors;
+  stream_descriptors.push_back(flags::Stream0());
+  stream_descriptors.push_back(flags::Stream1());
+  std::vector<std::string> SL_descriptors;
+  SL_descriptors.push_back(flags::SL0());
+  SL_descriptors.push_back(flags::SL1());
+  VideoQualityTest::FillScalabilitySettings(
+      &params, stream_descriptors, flags::SelectedStream(),
+      flags::NumSpatialLayers(), flags::SelectedSL(), SL_descriptors);
+
   VideoQualityTest test;
-  if (flags::OutputFilename().empty())
-    test.RunWithVideoRenderer(params);
-  else
+  if (flags::DurationSecs()) {
     test.RunWithAnalyzer(params);
+  } else {
+    test.RunWithVideoRenderer(params);
+  }
 }
 }  // namespace webrtc
 
diff --git a/webrtc/video/video_loopback.cc b/webrtc/video/video_loopback.cc
index 0c06f85..2338a84 100644
--- a/webrtc/video/video_loopback.cc
+++ b/webrtc/video/video_loopback.cc
@@ -20,6 +20,7 @@
 namespace webrtc {
 namespace flags {
 
+// Flags common with screenshare loopback, with different default values.
 DEFINE_int32(width, 640, "Video width.");
 size_t Width() {
   return static_cast<size_t>(FLAGS_width);
@@ -55,11 +56,46 @@
   return static_cast<int>(FLAGS_max_bitrate);
 }
 
+DEFINE_int32(num_temporal_layers,
+             1,
+             "Number of temporal layers. Set to 1-4 to override.");
+int NumTemporalLayers() {
+  return static_cast<int>(FLAGS_num_temporal_layers);
+}
+
+// Flags common with screenshare loopback, with equal default values.
 DEFINE_string(codec, "VP8", "Video codec to use.");
 std::string Codec() {
   return static_cast<std::string>(FLAGS_codec);
 }
 
+DEFINE_int32(selected_tl,
+             -1,
+             "Temporal layer to show or analyze. -1 to disable filtering.");
+int SelectedTL() {
+  return static_cast<int>(FLAGS_selected_tl);
+}
+
+DEFINE_int32(
+    duration,
+    0,
+    "Duration of the test in seconds. If 0, rendered will be shown instead.");
+int DurationSecs() {
+  return static_cast<int>(FLAGS_duration);
+}
+
+DEFINE_string(output_filename, "", "Target graph data filename.");
+std::string OutputFilename() {
+  return static_cast<std::string>(FLAGS_output_filename);
+}
+
+DEFINE_string(graph_title,
+              "",
+              "If empty, title will be generated automatically.");
+std::string GraphTitle() {
+  return static_cast<std::string>(FLAGS_graph_title);
+}
+
 DEFINE_int32(loss_percent, 0, "Percentage of packets randomly lost.");
 int LossPercent() {
   return static_cast<int>(FLAGS_loss_percent);
@@ -91,8 +127,55 @@
   return static_cast<int>(FLAGS_std_propagation_delay_ms);
 }
 
+DEFINE_int32(selected_stream, 0, "ID of the stream to show or analyze.");
+int SelectedStream() {
+  return static_cast<int>(FLAGS_selected_stream);
+}
+
+DEFINE_int32(num_spatial_layers, 1, "Number of spatial layers to use.");
+int NumSpatialLayers() {
+  return static_cast<int>(FLAGS_num_spatial_layers);
+}
+
+DEFINE_int32(selected_sl,
+             -1,
+             "Spatial layer to show or analyze. -1 to disable filtering.");
+int SelectedSL() {
+  return static_cast<int>(FLAGS_selected_sl);
+}
+
+DEFINE_string(stream0,
+              "",
+              "Comma separated values describing VideoStream for stream #0.");
+std::string Stream0() {
+  return static_cast<std::string>(FLAGS_stream0);
+}
+
+DEFINE_string(stream1,
+              "",
+              "Comma separated values describing VideoStream for stream #1.");
+std::string Stream1() {
+  return static_cast<std::string>(FLAGS_stream1);
+}
+
+DEFINE_string(sl0,
+              "",
+              "Comma separated values describing SpatialLayer for layer #0.");
+std::string SL0() {
+  return static_cast<std::string>(FLAGS_sl0);
+}
+
+DEFINE_string(sl1,
+              "",
+              "Comma separated values describing SpatialLayer for layer #1.");
+std::string SL1() {
+  return static_cast<std::string>(FLAGS_sl1);
+}
+
 DEFINE_bool(logs, false, "print logs to stderr");
 
+DEFINE_bool(send_side_bwe, true, "Use send-side bandwidth estimation");
+
 DEFINE_string(
     force_fieldtrials,
     "",
@@ -101,21 +184,7 @@
     " will assign the group Enable to field trial WebRTC-FooFeature. Multiple "
     "trials are separated by \"/\"");
 
-DEFINE_int32(num_temporal_layers,
-             1,
-             "Number of temporal layers. Set to 1-4 to override.");
-size_t NumTemporalLayers() {
-  return static_cast<size_t>(FLAGS_num_temporal_layers);
-}
-
-DEFINE_int32(
-    tl_discard_threshold,
-    0,
-    "Discard TLs with id greater or equal the threshold. 0 to disable.");
-size_t TLDiscardThreshold() {
-  return static_cast<size_t>(FLAGS_tl_discard_threshold);
-}
-
+// Video-specific flags.
 DEFINE_string(clip,
               "",
               "Name of the clip to show. If empty, using chroma generator.");
@@ -123,21 +192,6 @@
   return static_cast<std::string>(FLAGS_clip);
 }
 
-DEFINE_string(
-    output_filename,
-    "",
-    "Name of a target graph data file. If set, no preview will be shown.");
-std::string OutputFilename() {
-  return static_cast<std::string>(FLAGS_output_filename);
-}
-
-DEFINE_int32(duration, 60, "Duration of the test in seconds.");
-int DurationSecs() {
-  return static_cast<int>(FLAGS_duration);
-}
-
-DEFINE_bool(send_side_bwe, true, "Use send-side bandwidth estimation");
-
 }  // namespace flags
 
 void Loopback() {
@@ -153,27 +207,36 @@
   call_bitrate_config.start_bitrate_bps = flags::StartBitrateKbps() * 1000;
   call_bitrate_config.max_bitrate_bps = flags::MaxBitrateKbps() * 1000;
 
-  std::string clip = flags::Clip();
-  std::string graph_title = clip.empty() ? "" : "video " + clip;
   VideoQualityTest::Params params{
       {flags::Width(), flags::Height(), flags::Fps(),
        flags::MinBitrateKbps() * 1000, flags::TargetBitrateKbps() * 1000,
        flags::MaxBitrateKbps() * 1000, flags::Codec(),
-       flags::NumTemporalLayers(),
+       flags::NumTemporalLayers(), flags::SelectedTL(),
        0,  // No min transmit bitrate.
-       call_bitrate_config, flags::TLDiscardThreshold(),
-       flags::FLAGS_send_side_bwe},
-      {clip},
+       call_bitrate_config, flags::FLAGS_send_side_bwe},
+      {flags::Clip()},
       {},  // Screenshare specific.
-      {graph_title, 0.0, 0.0, flags::DurationSecs(), flags::OutputFilename()},
+      {"video", 0.0, 0.0, flags::DurationSecs(), flags::OutputFilename(),
+       flags::GraphTitle()},
       pipe_config,
       flags::FLAGS_logs};
 
+  std::vector<std::string> stream_descriptors;
+  stream_descriptors.push_back(flags::Stream0());
+  stream_descriptors.push_back(flags::Stream1());
+  std::vector<std::string> SL_descriptors;
+  SL_descriptors.push_back(flags::SL0());
+  SL_descriptors.push_back(flags::SL1());
+  VideoQualityTest::FillScalabilitySettings(
+      &params, stream_descriptors, flags::SelectedStream(),
+      flags::NumSpatialLayers(), flags::SelectedSL(), SL_descriptors);
+
   VideoQualityTest test;
-  if (flags::OutputFilename().empty())
-    test.RunWithVideoRenderer(params);
-  else
+  if (flags::DurationSecs()) {
     test.RunWithAnalyzer(params);
+  } else {
+    test.RunWithVideoRenderer(params);
+  }
 }
 }  // namespace webrtc
 
diff --git a/webrtc/video/video_quality_test.cc b/webrtc/video/video_quality_test.cc
index 0f45fa6..333f00d 100644
--- a/webrtc/video/video_quality_test.cc
+++ b/webrtc/video/video_quality_test.cc
@@ -12,6 +12,7 @@
 #include <algorithm>
 #include <deque>
 #include <map>
+#include <sstream>
 #include <vector>
 
 #include "testing/gtest/include/gtest/gtest.h"
@@ -21,7 +22,7 @@
 #include "webrtc/base/scoped_ptr.h"
 #include "webrtc/call.h"
 #include "webrtc/common_video/libyuv/include/webrtc_libyuv.h"
-#include "webrtc/modules/rtp_rtcp/interface/rtp_header_parser.h"
+#include "webrtc/modules/rtp_rtcp/source/rtp_utility.h"
 #include "webrtc/system_wrappers/include/cpu_info.h"
 #include "webrtc/test/layer_filtering_transport.h"
 #include "webrtc/test/run_loop.h"
@@ -43,18 +44,22 @@
                       public EncodedFrameObserver,
                       public EncodingTimeObserver {
  public:
-  VideoAnalyzer(Transport* transport,
+  VideoAnalyzer(test::LayerFilteringTransport* transport,
                 const std::string& test_label,
                 double avg_psnr_threshold,
                 double avg_ssim_threshold,
                 int duration_frames,
-                FILE* graph_data_output_file)
+                FILE* graph_data_output_file,
+                const std::string& graph_title,
+                uint32_t ssrc_to_analyze)
       : input_(nullptr),
         transport_(transport),
         receiver_(nullptr),
         send_stream_(nullptr),
         test_label_(test_label),
         graph_data_output_file_(graph_data_output_file),
+        graph_title_(graph_title),
+        ssrc_to_analyze_(ssrc_to_analyze),
         frames_to_process_(duration_frames),
         frames_recorded_(0),
         frames_processed_(0),
@@ -93,7 +98,6 @@
 
     stats_polling_thread_ =
         ThreadWrapper::CreateThread(&PollStatsThread, this, "StatsPoller");
-    EXPECT_TRUE(stats_polling_thread_->Start());
   }
 
   ~VideoAnalyzer() {
@@ -109,9 +113,9 @@
                                const uint8_t* packet,
                                size_t length,
                                const PacketTime& packet_time) override {
-    rtc::scoped_ptr<RtpHeaderParser> parser(RtpHeaderParser::Create());
+    RtpUtility::RtpHeaderParser parser(packet, length);
     RTPHeader header;
-    parser->Parse(packet, length, &header);
+    parser.Parse(header);
     {
       rtc::CritScope lock(&crit_);
       recv_times_[header.timestamp - rtp_timestamp_delta_] =
@@ -145,10 +149,13 @@
   bool SendRtp(const uint8_t* packet,
                size_t length,
                const PacketOptions& options) override {
-    rtc::scoped_ptr<RtpHeaderParser> parser(RtpHeaderParser::Create());
+    RtpUtility::RtpHeaderParser parser(packet, length);
     RTPHeader header;
-    parser->Parse(packet, length, &header);
+    parser.Parse(header);
 
+    int64_t current_time =
+        Clock::GetRealTimeClock()->CurrentNtpInMilliseconds();
+    bool result = transport_->SendRtp(packet, length, options);
     {
       rtc::CritScope lock(&crit_);
       if (rtp_timestamp_delta_ == 0) {
@@ -156,13 +163,14 @@
         first_send_frame_.Reset();
       }
       uint32_t timestamp = header.timestamp - rtp_timestamp_delta_;
-      send_times_[timestamp] =
-          Clock::GetRealTimeClock()->CurrentNtpInMilliseconds();
-      encoded_frame_sizes_[timestamp] +=
-          length - (header.headerLength + header.paddingLength);
+      send_times_[timestamp] = current_time;
+      if (!transport_->DiscardedLastPacket() &&
+          header.ssrc == ssrc_to_analyze_) {
+        encoded_frame_sizes_[timestamp] +=
+            length - (header.headerLength + header.paddingLength);
+      }
     }
-
-    return transport_->SendRtp(packet, length, options);
+    return result;
   }
 
   bool SendRtcp(const uint8_t* packet, size_t length) override {
@@ -192,6 +200,11 @@
     VideoFrame reference_frame = frames_.front();
     frames_.pop_front();
     assert(!reference_frame.IsZeroSize());
+    if (send_timestamp == reference_frame.timestamp() - 1) {
+      // TODO(ivica): Make this work for > 2 streams.
+      // Look at rtp_sender.c:RTPSender::BuildRTPHeader.
+      ++send_timestamp;
+    }
     EXPECT_EQ(reference_frame.timestamp(), send_timestamp);
     assert(reference_frame.timestamp() == send_timestamp);
 
@@ -207,6 +220,8 @@
     // at time-out check if frames_processed is going up. If so, give it more
     // time, otherwise fail. Hopefully this will reduce test flakiness.
 
+    EXPECT_TRUE(stats_polling_thread_->Start());
+
     int last_frames_processed = -1;
     EventTypeWrapper eventType;
     int iteration = 0;
@@ -245,7 +260,7 @@
   }
 
   VideoCaptureInput* input_;
-  Transport* const transport_;
+  test::LayerFilteringTransport* const transport_;
   PacketReceiver* receiver_;
   VideoSendStream* send_stream_;
 
@@ -320,8 +335,13 @@
     int64_t recv_time_ms = recv_times_[reference.timestamp()];
     recv_times_.erase(reference.timestamp());
 
-    size_t encoded_size = encoded_frame_sizes_[reference.timestamp()];
-    encoded_frame_sizes_.erase(reference.timestamp());
+    // TODO(ivica): Make this work for > 2 streams.
+    auto it = encoded_frame_sizes_.find(reference.timestamp());
+    if (it == encoded_frame_sizes_.end())
+      it = encoded_frame_sizes_.find(reference.timestamp() - 1);
+    size_t encoded_size = it == encoded_frame_sizes_.end() ? 0 : it->second;
+    if (it != encoded_frame_sizes_.end())
+      encoded_frame_sizes_.erase(it);
 
     VideoFrame reference_copy;
     VideoFrame render_copy;
@@ -509,7 +529,7 @@
                 return A.input_time_ms < B.input_time_ms;
               });
 
-    fprintf(out, "%s\n", test_label_.c_str());
+    fprintf(out, "%s\n", graph_title_.c_str());
     fprintf(out, "%" PRIuS "\n", samples_.size());
     fprintf(out,
             "dropped "
@@ -547,6 +567,8 @@
 
   const std::string test_label_;
   FILE* const graph_data_output_file_;
+  const std::string graph_title_;
+  const uint32_t ssrc_to_analyze_;
   std::vector<Sample> samples_ GUARDED_BY(comparison_lock_);
   std::map<int64_t, int> samples_encode_time_ms_ GUARDED_BY(comparison_lock_);
   test::Statistics sender_time_ GUARDED_BY(comparison_lock_);
@@ -588,28 +610,188 @@
 
 VideoQualityTest::VideoQualityTest() : clock_(Clock::GetRealTimeClock()) {}
 
-void VideoQualityTest::ValidateParams(const Params& params) {
-  RTC_CHECK_GE(params.common.max_bitrate_bps, params.common.target_bitrate_bps);
-  RTC_CHECK_GE(params.common.target_bitrate_bps, params.common.min_bitrate_bps);
-  RTC_CHECK_LT(params.common.tl_discard_threshold,
-               params.common.num_temporal_layers);
-}
-
 void VideoQualityTest::TestBody() {}
 
-void VideoQualityTest::SetupFullStack(const Params& params,
-                                      Transport* send_transport,
-                                      Transport* recv_transport) {
-  if (params.logs)
+std::string VideoQualityTest::GenerateGraphTitle() const {
+  std::stringstream ss;
+  ss << params_.common.codec;
+  ss << " (" << params_.common.target_bitrate_bps / 1000 << "kbps";
+  ss << ", " << params_.common.fps << " FPS";
+  if (params_.screenshare.scroll_duration)
+    ss << ", " << params_.screenshare.scroll_duration << "s scroll";
+  if (params_.ss.streams.size() > 1)
+    ss << ", Stream #" << params_.ss.selected_stream;
+  if (params_.ss.num_spatial_layers > 1)
+    ss << ", Layer #" << params_.ss.selected_sl;
+  ss << ")";
+  return ss.str();
+}
+
+void VideoQualityTest::CheckParams() {
+  // Add a default stream in none specified.
+  if (params_.ss.streams.empty())
+    params_.ss.streams.push_back(VideoQualityTest::DefaultVideoStream(params_));
+  if (params_.ss.num_spatial_layers == 0)
+    params_.ss.num_spatial_layers = 1;
+
+  if (params_.pipe.loss_percent != 0 ||
+      params_.pipe.queue_length_packets != 0) {
+    // Since LayerFilteringTransport changes the sequence numbers, we can't
+    // use that feature with pack loss, since the NACK request would end up
+    // retransmitting the wrong packets.
+    RTC_CHECK(params_.ss.selected_sl == -1 ||
+              params_.ss.num_spatial_layers == 1);
+    RTC_CHECK(params_.common.selected_tl == -1 ||
+              params_.common.num_temporal_layers == 1);
+  }
+
+  // TODO(ivica): Should max_bitrate_bps == -1 represent inf max bitrate, as it
+  // does in some parts of the code?
+  RTC_CHECK_GE(params_.common.max_bitrate_bps,
+               params_.common.target_bitrate_bps);
+  RTC_CHECK_GE(params_.common.target_bitrate_bps,
+               params_.common.min_bitrate_bps);
+  RTC_CHECK_LT(params_.common.selected_tl, params_.common.num_temporal_layers);
+  RTC_CHECK_LT(params_.ss.selected_stream, params_.ss.streams.size());
+  for (const VideoStream& stream : params_.ss.streams) {
+    RTC_CHECK_GE(stream.min_bitrate_bps, 0);
+    RTC_CHECK_GE(stream.target_bitrate_bps, stream.min_bitrate_bps);
+    RTC_CHECK_GE(stream.max_bitrate_bps, stream.target_bitrate_bps);
+    RTC_CHECK_EQ(static_cast<int>(stream.temporal_layer_thresholds_bps.size()),
+                 params_.common.num_temporal_layers - 1);
+  }
+  // TODO(ivica): Should we check if the sum of all streams/layers is equal to
+  // the total bitrate? We anyway have to update them in the case bitrate
+  // estimator changes the total bitrates.
+  RTC_CHECK_GE(params_.ss.num_spatial_layers, 1);
+  RTC_CHECK_LE(params_.ss.selected_sl, params_.ss.num_spatial_layers);
+  RTC_CHECK(params_.ss.spatial_layers.empty() ||
+            params_.ss.spatial_layers.size() ==
+                static_cast<size_t>(params_.ss.num_spatial_layers));
+  if (params_.common.codec == "VP8") {
+    RTC_CHECK_EQ(params_.ss.num_spatial_layers, 1);
+  } else if (params_.common.codec == "VP9") {
+    RTC_CHECK_EQ(params_.ss.streams.size(), 1u);
+  }
+}
+
+// Static.
+std::vector<int> VideoQualityTest::ParseCSV(const std::string& str) {
+  // Parse comma separated nonnegative integers, where some elements may be
+  // empty. The empty values are replaced with -1.
+  // E.g. "10,-20,,30,40" --> {10, 20, -1, 30,40}
+  // E.g. ",,10,,20," --> {-1, -1, 10, -1, 20, -1}
+  std::vector<int> result;
+  if (str.empty())
+    return result;
+
+  const char* p = str.c_str();
+  int value = -1;
+  int pos;
+  while (*p) {
+    if (*p == ',') {
+      result.push_back(value);
+      value = -1;
+      ++p;
+      continue;
+    }
+    RTC_CHECK_EQ(sscanf(p, "%d%n", &value, &pos), 1)
+        << "Unexpected non-number value.";
+    p += pos;
+  }
+  result.push_back(value);
+  return result;
+}
+
+// Static.
+VideoStream VideoQualityTest::DefaultVideoStream(const Params& params) {
+  VideoStream stream;
+  stream.width = params.common.width;
+  stream.height = params.common.height;
+  stream.max_framerate = params.common.fps;
+  stream.min_bitrate_bps = params.common.min_bitrate_bps;
+  stream.target_bitrate_bps = params.common.target_bitrate_bps;
+  stream.max_bitrate_bps = params.common.max_bitrate_bps;
+  stream.max_qp = 52;
+  if (params.common.num_temporal_layers == 2)
+    stream.temporal_layer_thresholds_bps.push_back(stream.target_bitrate_bps);
+  return stream;
+}
+
+// Static.
+void VideoQualityTest::FillScalabilitySettings(
+    Params* params,
+    const std::vector<std::string>& stream_descriptors,
+    size_t selected_stream,
+    int num_spatial_layers,
+    int selected_sl,
+    const std::vector<std::string>& sl_descriptors) {
+  // Read VideoStream and SpatialLayer elements from a list of comma separated
+  // lists. To use a default value for an element, use -1 or leave empty.
+  // Validity checks performed in CheckParams.
+
+  RTC_CHECK(params->ss.streams.empty());
+  for (auto descriptor : stream_descriptors) {
+    if (descriptor.empty())
+      continue;
+    VideoStream stream = VideoQualityTest::DefaultVideoStream(*params);
+    std::vector<int> v = VideoQualityTest::ParseCSV(descriptor);
+    if (v[0] != -1)
+      stream.width = static_cast<size_t>(v[0]);
+    if (v[1] != -1)
+      stream.height = static_cast<size_t>(v[1]);
+    if (v[2] != -1)
+      stream.max_framerate = v[2];
+    if (v[3] != -1)
+      stream.min_bitrate_bps = v[3];
+    if (v[4] != -1)
+      stream.target_bitrate_bps = v[4];
+    if (v[5] != -1)
+      stream.max_bitrate_bps = v[5];
+    if (v.size() > 6 && v[6] != -1)
+      stream.max_qp = v[6];
+    if (v.size() > 7) {
+      stream.temporal_layer_thresholds_bps.clear();
+      stream.temporal_layer_thresholds_bps.insert(
+          stream.temporal_layer_thresholds_bps.end(), v.begin() + 7, v.end());
+    } else {
+      // Automatic TL thresholds for more than two layers not supported.
+      RTC_CHECK_LE(params->common.num_temporal_layers, 2);
+    }
+    params->ss.streams.push_back(stream);
+  }
+  params->ss.selected_stream = selected_stream;
+
+  params->ss.num_spatial_layers = num_spatial_layers ? num_spatial_layers : 1;
+  params->ss.selected_sl = selected_sl;
+  RTC_CHECK(params->ss.spatial_layers.empty());
+  for (auto descriptor : sl_descriptors) {
+    if (descriptor.empty())
+      continue;
+    std::vector<int> v = VideoQualityTest::ParseCSV(descriptor);
+    RTC_CHECK_GT(v[2], 0);
+
+    SpatialLayer layer;
+    layer.scaling_factor_num = v[0] == -1 ? 1 : v[0];
+    layer.scaling_factor_den = v[1] == -1 ? 1 : v[1];
+    layer.target_bitrate_bps = v[2];
+    params->ss.spatial_layers.push_back(layer);
+  }
+}
+
+void VideoQualityTest::SetupCommon(Transport* send_transport,
+                                   Transport* recv_transport) {
+  if (params_.logs)
     trace_to_stderr_.reset(new test::TraceToStderr);
 
-  CreateSendConfig(1, send_transport);
+  size_t num_streams = params_.ss.streams.size();
+  CreateSendConfig(num_streams, send_transport);
 
   int payload_type;
-  if (params.common.codec == "VP8") {
+  if (params_.common.codec == "VP8") {
     encoder_.reset(VideoEncoder::Create(VideoEncoder::kVp8));
     payload_type = kPayloadTypeVP8;
-  } else if (params.common.codec == "VP9") {
+  } else if (params_.common.codec == "VP9") {
     encoder_.reset(VideoEncoder::Create(VideoEncoder::kVp9));
     payload_type = kPayloadTypeVP9;
   } else {
@@ -617,15 +799,15 @@
     return;
   }
   send_config_.encoder_settings.encoder = encoder_.get();
-  send_config_.encoder_settings.payload_name = params.common.codec;
+  send_config_.encoder_settings.payload_name = params_.common.codec;
   send_config_.encoder_settings.payload_type = payload_type;
-
   send_config_.rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
-  send_config_.rtp.rtx.ssrcs.push_back(kSendRtxSsrcs[0]);
   send_config_.rtp.rtx.payload_type = kSendRtxPayloadType;
+  for (size_t i = 0; i < num_streams; ++i)
+    send_config_.rtp.rtx.ssrcs.push_back(kSendRtxSsrcs[i]);
 
   send_config_.rtp.extensions.clear();
-  if (params.common.send_side_bwe) {
+  if (params_.common.send_side_bwe) {
     send_config_.rtp.extensions.push_back(
         RtpExtension(RtpExtension::kTransportSequenceNumber,
                      test::kTransportSequenceNumberExtensionId));
@@ -634,49 +816,41 @@
         RtpExtension::kAbsSendTime, test::kAbsSendTimeExtensionId));
   }
 
-  // Automatically fill out streams[0] with params.
-  VideoStream* stream = &encoder_config_.streams[0];
-  stream->width = params.common.width;
-  stream->height = params.common.height;
-  stream->min_bitrate_bps = params.common.min_bitrate_bps;
-  stream->target_bitrate_bps = params.common.target_bitrate_bps;
-  stream->max_bitrate_bps = params.common.max_bitrate_bps;
-  stream->max_framerate = static_cast<int>(params.common.fps);
-
-  stream->temporal_layer_thresholds_bps.clear();
-  if (params.common.num_temporal_layers > 1) {
-    stream->temporal_layer_thresholds_bps.push_back(stream->target_bitrate_bps);
-  }
+  encoder_config_.min_transmit_bitrate_bps = params_.common.min_transmit_bps;
+  encoder_config_.streams = params_.ss.streams;
+  encoder_config_.spatial_layers = params_.ss.spatial_layers;
 
   CreateMatchingReceiveConfigs(recv_transport);
 
-  receive_configs_[0].rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
-  receive_configs_[0].rtp.rtx[kSendRtxPayloadType].ssrc = kSendRtxSsrcs[0];
-  receive_configs_[0].rtp.rtx[kSendRtxPayloadType].payload_type =
-      kSendRtxPayloadType;
-
-  encoder_config_.min_transmit_bitrate_bps = params.common.min_transmit_bps;
+  for (size_t i = 0; i < num_streams; ++i) {
+    receive_configs_[i].rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
+    receive_configs_[i].rtp.rtx[kSendRtxPayloadType].ssrc = kSendRtxSsrcs[i];
+    receive_configs_[i].rtp.rtx[kSendRtxPayloadType].payload_type =
+        kSendRtxPayloadType;
+  }
 }
 
-void VideoQualityTest::SetupScreenshare(const Params& params) {
-  RTC_CHECK(params.screenshare.enabled);
+void VideoQualityTest::SetupScreenshare() {
+  RTC_CHECK(params_.screenshare.enabled);
 
   // Fill out codec settings.
   encoder_config_.content_type = VideoEncoderConfig::ContentType::kScreen;
-  if (params.common.codec == "VP8") {
+  if (params_.common.codec == "VP8") {
     codec_settings_.VP8 = VideoEncoder::GetDefaultVp8Settings();
     codec_settings_.VP8.denoisingOn = false;
     codec_settings_.VP8.frameDroppingOn = false;
     codec_settings_.VP8.numberOfTemporalLayers =
-        static_cast<unsigned char>(params.common.num_temporal_layers);
+        static_cast<unsigned char>(params_.common.num_temporal_layers);
     encoder_config_.encoder_specific_settings = &codec_settings_.VP8;
-  } else if (params.common.codec == "VP9") {
+  } else if (params_.common.codec == "VP9") {
     codec_settings_.VP9 = VideoEncoder::GetDefaultVp9Settings();
     codec_settings_.VP9.denoisingOn = false;
     codec_settings_.VP9.frameDroppingOn = false;
     codec_settings_.VP9.numberOfTemporalLayers =
-        static_cast<unsigned char>(params.common.num_temporal_layers);
+        static_cast<unsigned char>(params_.common.num_temporal_layers);
     encoder_config_.encoder_specific_settings = &codec_settings_.VP9;
+    codec_settings_.VP9.numberOfSpatialLayers =
+        static_cast<unsigned char>(params_.ss.num_spatial_layers);
   }
 
   // Setup frame generator.
@@ -688,71 +862,67 @@
   slides.push_back(test::ResourcePath("photo_1850_1110", "yuv"));
   slides.push_back(test::ResourcePath("difficult_photo_1850_1110", "yuv"));
 
-  if (params.screenshare.scroll_duration == 0) {
+  if (params_.screenshare.scroll_duration == 0) {
     // Cycle image every slide_change_interval seconds.
     frame_generator_.reset(test::FrameGenerator::CreateFromYuvFile(
         slides, kWidth, kHeight,
-        params.screenshare.slide_change_interval * params.common.fps));
+        params_.screenshare.slide_change_interval * params_.common.fps));
   } else {
-    RTC_CHECK_LE(params.common.width, kWidth);
-    RTC_CHECK_LE(params.common.height, kHeight);
-    RTC_CHECK_GT(params.screenshare.slide_change_interval, 0);
-    const int kPauseDurationMs = (params.screenshare.slide_change_interval -
-                                  params.screenshare.scroll_duration) * 1000;
-    RTC_CHECK_LE(params.screenshare.scroll_duration,
-                 params.screenshare.slide_change_interval);
+    RTC_CHECK_LE(params_.common.width, kWidth);
+    RTC_CHECK_LE(params_.common.height, kHeight);
+    RTC_CHECK_GT(params_.screenshare.slide_change_interval, 0);
+    const int kPauseDurationMs = (params_.screenshare.slide_change_interval -
+                                  params_.screenshare.scroll_duration) *
+                                 1000;
+    RTC_CHECK_LE(params_.screenshare.scroll_duration,
+                 params_.screenshare.slide_change_interval);
 
-    if (params.screenshare.scroll_duration) {
-      frame_generator_.reset(
-          test::FrameGenerator::CreateScrollingInputFromYuvFiles(
-              clock_, slides, kWidth, kHeight, params.common.width,
-              params.common.height, params.screenshare.scroll_duration * 1000,
-              kPauseDurationMs));
-    } else {
-      frame_generator_.reset(test::FrameGenerator::CreateFromYuvFile(
-              slides, kWidth, kHeight,
-              params.screenshare.slide_change_interval * params.common.fps));
-    }
+    frame_generator_.reset(
+        test::FrameGenerator::CreateScrollingInputFromYuvFiles(
+            clock_, slides, kWidth, kHeight, params_.common.width,
+            params_.common.height, params_.screenshare.scroll_duration * 1000,
+            kPauseDurationMs));
   }
 }
 
-void VideoQualityTest::CreateCapturer(const Params& params,
-                                      VideoCaptureInput* input) {
-  if (params.screenshare.enabled) {
-    test::FrameGeneratorCapturer *frame_generator_capturer =
+void VideoQualityTest::CreateCapturer(VideoCaptureInput* input) {
+  if (params_.screenshare.enabled) {
+    test::FrameGeneratorCapturer* frame_generator_capturer =
         new test::FrameGeneratorCapturer(
-            clock_, input, frame_generator_.release(), params.common.fps);
+            clock_, input, frame_generator_.release(), params_.common.fps);
     EXPECT_TRUE(frame_generator_capturer->Init());
     capturer_.reset(frame_generator_capturer);
   } else {
-    if (params.video.clip_name.empty()) {
-      capturer_.reset(test::VideoCapturer::Create(
-          input, params.common.width, params.common.height, params.common.fps,
-          clock_));
+    if (params_.video.clip_name.empty()) {
+      capturer_.reset(test::VideoCapturer::Create(input, params_.common.width,
+                                                  params_.common.height,
+                                                  params_.common.fps, clock_));
     } else {
       capturer_.reset(test::FrameGeneratorCapturer::CreateFromYuvFile(
-          input, test::ResourcePath(params.video.clip_name, "yuv"),
-          params.common.width, params.common.height, params.common.fps,
+          input, test::ResourcePath(params_.video.clip_name, "yuv"),
+          params_.common.width, params_.common.height, params_.common.fps,
           clock_));
       ASSERT_TRUE(capturer_.get() != nullptr)
-          << "Could not create capturer for " << params.video.clip_name
+          << "Could not create capturer for " << params_.video.clip_name
           << ".yuv. Is this resource file present?";
     }
   }
 }
 
 void VideoQualityTest::RunWithAnalyzer(const Params& params) {
+  params_ = params;
+
   // TODO(ivica): Merge with RunWithRenderer and use a flag / argument to
   // differentiate between the analyzer and the renderer case.
-  ValidateParams(params);
+  CheckParams();
 
   FILE* graph_data_output_file = nullptr;
-  if (!params.analyzer.graph_data_output_filename.empty()) {
+  if (!params_.analyzer.graph_data_output_filename.empty()) {
     graph_data_output_file =
-        fopen(params.analyzer.graph_data_output_filename.c_str(), "w");
+        fopen(params_.analyzer.graph_data_output_filename.c_str(), "w");
     RTC_CHECK(graph_data_output_file != nullptr)
-        << "Can't open the file "
-        << params.analyzer.graph_data_output_filename << "!";
+        << "Can't open the file " << params_.analyzer.graph_data_output_filename
+        << "!";
   }
 
   Call::Config call_config;
@@ -761,33 +931,60 @@
 
   test::LayerFilteringTransport send_transport(
       params.pipe, sender_call_.get(), kPayloadTypeVP8, kPayloadTypeVP9,
-      static_cast<uint8_t>(params.common.tl_discard_threshold), 0);
+      params.common.selected_tl, params_.ss.selected_sl);
   test::DirectTransport recv_transport(params.pipe, receiver_call_.get());
 
+  std::string graph_title = params_.analyzer.graph_title;
+  if (graph_title.empty())
+    graph_title = VideoQualityTest::GenerateGraphTitle();
+
+  // In the case of different resolutions, the functions calculating PSNR and
+  // SSIM return -1.0, instead of a positive value as usual. VideoAnalyzer
+  // aborts if the average psnr/ssim are below the given threshold, which is
+  // 0.0 by default. Setting the thresholds to -1.1 prevents the unnecessary
+  // abort.
+  VideoStream& selected_stream = params_.ss.streams[params_.ss.selected_stream];
+  int selected_sl = params_.ss.selected_sl != -1
+                        ? params_.ss.selected_sl
+                        : params_.ss.num_spatial_layers - 1;
+  bool disable_quality_check =
+      selected_stream.width != params_.common.width ||
+      selected_stream.height != params_.common.height ||
+      (!params_.ss.spatial_layers.empty() &&
+       params_.ss.spatial_layers[selected_sl].scaling_factor_num !=
+           params_.ss.spatial_layers[selected_sl].scaling_factor_den);
+  if (disable_quality_check) {
+    fprintf(stderr,
+            "Warning: Calculating PSNR and SSIM for downsized resolution "
+            "not implemented yet! Skipping PSNR and SSIM calculations!");
+  }
+
   VideoAnalyzer analyzer(
-      &send_transport, params.analyzer.test_label,
-      params.analyzer.avg_psnr_threshold, params.analyzer.avg_ssim_threshold,
-      params.analyzer.test_durations_secs * params.common.fps,
-      graph_data_output_file);
+      &send_transport, params_.analyzer.test_label,
+      disable_quality_check ? -1.1 : params_.analyzer.avg_psnr_threshold,
+      disable_quality_check ? -1.1 : params_.analyzer.avg_ssim_threshold,
+      params_.analyzer.test_durations_secs * params_.common.fps,
+      graph_data_output_file, graph_title,
+      kSendSsrcs[params_.ss.selected_stream]);
 
   analyzer.SetReceiver(receiver_call_->Receiver());
   send_transport.SetReceiver(&analyzer);
   recv_transport.SetReceiver(sender_call_->Receiver());
 
-  SetupFullStack(params, &analyzer, &recv_transport);
+  SetupCommon(&analyzer, &recv_transport);
   send_config_.encoding_time_observer = &analyzer;
-  receive_configs_[0].renderer = &analyzer;
+  receive_configs_[params_.ss.selected_stream].renderer = &analyzer;
   for (auto& config : receive_configs_)
     config.pre_decode_callback = &analyzer;
 
-  if (params.screenshare.enabled)
-    SetupScreenshare(params);
+  if (params_.screenshare.enabled)
+    SetupScreenshare();
 
   CreateStreams();
   analyzer.input_ = send_stream_->Input();
   analyzer.send_stream_ = send_stream_;
 
-  CreateCapturer(params, &analyzer);
+  CreateCapturer(&analyzer);
 
   send_stream_->Start();
   for (size_t i = 0; i < receive_streams_.size(); ++i)
@@ -811,40 +1008,49 @@
 }
 
 void VideoQualityTest::RunWithVideoRenderer(const Params& params) {
-  ValidateParams(params);
+  params_ = params;
+  CheckParams();
 
   rtc::scoped_ptr<test::VideoRenderer> local_preview(
-      test::VideoRenderer::Create("Local Preview", params.common.width,
-                                  params.common.height));
+      test::VideoRenderer::Create("Local Preview", params_.common.width,
+                                  params_.common.height));
+  size_t stream_id = params_.ss.selected_stream;
+  char title[32];
+  if (params_.ss.streams.size() == 1) {
+    sprintf(title, "Loopback Video");
+  } else {
+    sprintf(title, "Loopback Video - Stream #%" PRIuS, stream_id);
+  }
   rtc::scoped_ptr<test::VideoRenderer> loopback_video(
-      test::VideoRenderer::Create("Loopback Video", params.common.width,
-                                  params.common.height));
+      test::VideoRenderer::Create(title, params_.ss.streams[stream_id].width,
+                                  params_.ss.streams[stream_id].height));
 
   // TODO(ivica): Remove bitrate_config and use the default Call::Config(), to
   // match the full stack tests.
   Call::Config call_config;
-  call_config.bitrate_config = params.common.call_bitrate_config;
+  call_config.bitrate_config = params_.common.call_bitrate_config;
   rtc::scoped_ptr<Call> call(Call::Create(call_config));
 
   test::LayerFilteringTransport transport(
       params.pipe, call.get(), kPayloadTypeVP8, kPayloadTypeVP9,
-      static_cast<uint8_t>(params.common.tl_discard_threshold), 0);
+      params.common.selected_tl, params_.ss.selected_sl);
   // TODO(ivica): Use two calls to be able to merge with RunWithAnalyzer or at
   // least share as much code as possible. That way this test would also match
   // the full stack tests better.
   transport.SetReceiver(call->Receiver());
 
-  SetupFullStack(params, &transport, &transport);
-  send_config_.local_renderer = local_preview.get();
-  receive_configs_[0].renderer = loopback_video.get();
+  SetupCommon(&transport, &transport);
 
-  if (params.screenshare.enabled)
-    SetupScreenshare(params);
+  send_config_.local_renderer = local_preview.get();
+  receive_configs_[stream_id].renderer = loopback_video.get();
+
+  if (params_.screenshare.enabled)
+    SetupScreenshare();
 
   send_stream_ = call->CreateVideoSendStream(send_config_, encoder_config_);
   VideoReceiveStream* receive_stream =
-      call->CreateVideoReceiveStream(receive_configs_[0]);
-  CreateCapturer(params, send_stream_->Input());
+      call->CreateVideoReceiveStream(receive_configs_[stream_id]);
+  CreateCapturer(send_stream_->Input());
 
   receive_stream->Start();
   send_stream_->Start();
diff --git a/webrtc/video/video_quality_test.h b/webrtc/video/video_quality_test.h
index 7b62fb3..b88c513 100644
--- a/webrtc/video/video_quality_test.h
+++ b/webrtc/video/video_quality_test.h
@@ -33,11 +33,11 @@
       int target_bitrate_bps;
       int max_bitrate_bps;
       std::string codec;
-      size_t num_temporal_layers;
+      int num_temporal_layers;
+      int selected_tl;
       int min_transmit_bps;
 
       Call::Config::BitrateConfig call_bitrate_config;
-      size_t tl_discard_threshold;
       bool send_side_bwe;
     } common;
     struct {  // Video-specific settings.
@@ -50,30 +50,56 @@
     } screenshare;
     struct {  // Analyzer settings.
       std::string test_label;
-      double avg_psnr_threshold;
-      double avg_ssim_threshold;
+      double avg_psnr_threshold;  // (*)
+      double avg_ssim_threshold;  // (*)
       int test_durations_secs;
       std::string graph_data_output_filename;
+      std::string graph_title;
     } analyzer;
     FakeNetworkPipe::Config pipe;
     bool logs;
+    struct {                             // Spatial scalability.
+      std::vector<VideoStream> streams;  // If empty, one stream is assumed.
+      size_t selected_stream;
+      int num_spatial_layers;
+      int selected_sl;
+      // If empty, bitrates are generated in VP9Impl automatically.
+      std::vector<SpatialLayer> spatial_layers;
+    } ss;
   };
+  // (*) Set to -1.1 if generating graph data for simulcast or SVC and the
+  // selected stream/layer doesn't have the same resolution as the largest
+  // stream/layer (to ignore the PSNR and SSIM calculation errors).
 
   VideoQualityTest();
   void RunWithAnalyzer(const Params& params);
   void RunWithVideoRenderer(const Params& params);
 
+  static void FillScalabilitySettings(
+      Params* params,
+      const std::vector<std::string>& stream_descriptors,
+      size_t selected_stream,
+      int num_spatial_layers,
+      int selected_sl,
+      const std::vector<std::string>& sl_descriptors);
+
  protected:
   // No-op implementation to be able to instantiate this class from non-TEST_F
   // locations.
   void TestBody() override;
 
-  void CreateCapturer(const Params& params, VideoCaptureInput* input);
-  void ValidateParams(const Params& params);
-  void SetupFullStack(const Params& params,
-                      Transport* send_transport,
-                      Transport* recv_transport);
-  void SetupScreenshare(const Params& params);
+  // Helper methods accessing only params_.
+  std::string GenerateGraphTitle() const;
+  void CheckParams();
+
+  // Helper static methods.
+  static VideoStream DefaultVideoStream(const Params& params);
+  static std::vector<int> ParseCSV(const std::string& str);
+
+  // Helper methods for setting up the call.
+  void CreateCapturer(VideoCaptureInput* input);
+  void SetupCommon(Transport* send_transport, Transport* recv_transport);
+  void SetupScreenshare();
 
   // We need a more general capturer than the FrameGeneratorCapturer.
   rtc::scoped_ptr<test::VideoCapturer> capturer_;
@@ -82,6 +108,8 @@
   rtc::scoped_ptr<VideoEncoder> encoder_;
   VideoCodecUnion codec_settings_;
   Clock* const clock_;
+
+  Params params_;
 };
 
 }  // namespace webrtc
diff --git a/webrtc/video/video_send_stream.cc b/webrtc/video/video_send_stream.cc
index 4ec923f..67e8a9c 100644
--- a/webrtc/video/video_send_stream.cc
+++ b/webrtc/video/video_send_stream.cc
@@ -370,6 +370,16 @@
       static_cast<unsigned char>(streams.size());
   video_codec.minBitrate = streams[0].min_bitrate_bps / 1000;
   RTC_DCHECK_LE(streams.size(), static_cast<size_t>(kMaxSimulcastStreams));
+  if (video_codec.codecType == kVideoCodecVP9) {
+    // If the vector is empty, bitrates will be configured automatically.
+    RTC_DCHECK(config.spatial_layers.empty() ||
+               config.spatial_layers.size() ==
+                   video_codec.codecSpecific.VP9.numberOfSpatialLayers);
+    RTC_DCHECK_LE(video_codec.codecSpecific.VP9.numberOfSpatialLayers,
+                  kMaxSimulcastStreams);
+    for (size_t i = 0; i < config.spatial_layers.size(); ++i)
+      video_codec.spatialLayers[i] = config.spatial_layers[i];
+  }
   for (size_t i = 0; i < streams.size(); ++i) {
     SimulcastStream* sim_stream = &video_codec.simulcastStream[i];
     RTC_DCHECK_GT(streams[i].width, 0u);