Implement dual stream full stack test and loopback tool

Bug: webrtc:8588
Change-Id: I0abec4891a723c98001f4580f0cfa57a5d6d6bdb
Reviewed-on: https://webrtc-review.googlesource.com/34441
Commit-Queue: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#21416}
diff --git a/video/video_quality_test.cc b/video/video_quality_test.cc
index 2392e9f..bf2ec8c 100644
--- a/video/video_quality_test.cc
+++ b/video/video_quality_test.cc
@@ -1121,13 +1121,16 @@
 
 VideoQualityTest::Params::Params()
     : call({false, Call::Config::BitrateConfig(), 0}),
-      video({false, 640, 480, 30, 50, 800, 800, false, "VP8", 1, -1, 0, false,
-             false, ""}),
+      video{{false, 640, 480, 30, 50, 800, 800, false, "VP8", 1, -1, 0, false,
+             false, ""},
+            {false, 640, 480, 30, 50, 800, 800, false, "VP8", 1, -1, 0, false,
+             false, ""}},
       audio({false, false, false}),
-      screenshare({false, false, 10, 0}),
+      screenshare{{false, false, 10, 0}, {false, false, 10, 0}},
       analyzer({"", 0.0, 0.0, 0, "", ""}),
       pipe(),
-      ss({std::vector<VideoStream>(), 0, 0, -1, std::vector<SpatialLayer>()}),
+      ss{{std::vector<VideoStream>(), 0, 0, -1, std::vector<SpatialLayer>()},
+         {std::vector<VideoStream>(), 0, 0, -1, std::vector<SpatialLayer>()}},
       logging({false, "", "", ""}) {}
 
 VideoQualityTest::Params::~Params() = default;
@@ -1136,70 +1139,84 @@
 
 std::string VideoQualityTest::GenerateGraphTitle() const {
   std::stringstream ss;
-  ss << params_.video.codec;
-  ss << " (" << params_.video.target_bitrate_bps / 1000 << "kbps";
-  ss << ", " << params_.video.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 << params_.video[0].codec;
+  ss << " (" << params_.video[0].target_bitrate_bps / 1000 << "kbps";
+  ss << ", " << params_.video[0].fps << " FPS";
+  if (params_.screenshare[0].scroll_duration)
+    ss << ", " << params_.screenshare[0].scroll_duration << "s scroll";
+  if (params_.ss[0].streams.size() > 1)
+    ss << ", Stream #" << params_.ss[0].selected_stream;
+  if (params_.ss[0].num_spatial_layers > 1)
+    ss << ", Layer #" << params_.ss[0].selected_sl;
   ss << ")";
   return ss.str();
 }
 
 void VideoQualityTest::CheckParams() {
-  if (!params_.video.enabled)
-    return;
-  // 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;
+  for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+    // Iterate over primary and secondary video streams.
+    if (!params_.video[video_idx].enabled)
+      return;
+    // Add a default stream in none specified.
+    if (params_.ss[video_idx].streams.empty())
+      params_.ss[video_idx].streams.push_back(
+          VideoQualityTest::DefaultVideoStream(params_, video_idx));
+    if (params_.ss[video_idx].num_spatial_layers == 0)
+      params_.ss[video_idx].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.selected_sl == params_.ss.num_spatial_layers - 1);
-    RTC_CHECK(params_.video.selected_tl == -1 ||
-              params_.video.selected_tl ==
-                  params_.video.num_temporal_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[video_idx].selected_sl == -1 ||
+                params_.ss[video_idx].selected_sl ==
+                    params_.ss[video_idx].num_spatial_layers - 1);
+      RTC_CHECK(params_.video[video_idx].selected_tl == -1 ||
+                params_.video[video_idx].selected_tl ==
+                    params_.video[video_idx].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_.video.max_bitrate_bps, params_.video.target_bitrate_bps);
-  RTC_CHECK_GE(params_.video.target_bitrate_bps, params_.video.min_bitrate_bps);
-  RTC_CHECK_LT(params_.video.selected_tl, params_.video.num_temporal_layers);
-  RTC_CHECK_LE(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);
-  }
-  // 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_.video.codec == "VP8") {
-    RTC_CHECK_EQ(params_.ss.num_spatial_layers, 1);
-  } else if (params_.video.codec == "VP9") {
-    RTC_CHECK_EQ(params_.ss.streams.size(), 1);
-  }
-  RTC_CHECK_GE(params_.call.num_thumbnails, 0);
-  if (params_.call.num_thumbnails > 0) {
-    RTC_CHECK_EQ(params_.ss.num_spatial_layers, 1);
-    RTC_CHECK_EQ(params_.ss.streams.size(), 3);
-    RTC_CHECK_EQ(params_.video.num_temporal_layers, 3);
-    RTC_CHECK_EQ(params_.video.codec, "VP8");
+    // TODO(ivica): Should max_bitrate_bps == -1 represent inf max bitrate, as
+    // it does in some parts of the code?
+    RTC_CHECK_GE(params_.video[video_idx].max_bitrate_bps,
+                 params_.video[video_idx].target_bitrate_bps);
+    RTC_CHECK_GE(params_.video[video_idx].target_bitrate_bps,
+                 params_.video[video_idx].min_bitrate_bps);
+    RTC_CHECK_LT(params_.video[video_idx].selected_tl,
+                 params_.video[video_idx].num_temporal_layers);
+    RTC_CHECK_LE(params_.ss[video_idx].selected_stream,
+                 params_.ss[video_idx].streams.size());
+    for (const VideoStream& stream : params_.ss[video_idx].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);
+    }
+    // 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[video_idx].num_spatial_layers, 1);
+    RTC_CHECK_LE(params_.ss[video_idx].selected_sl,
+                 params_.ss[video_idx].num_spatial_layers);
+    RTC_CHECK(
+        params_.ss[video_idx].spatial_layers.empty() ||
+        params_.ss[video_idx].spatial_layers.size() ==
+            static_cast<size_t>(params_.ss[video_idx].num_spatial_layers));
+    if (params_.video[video_idx].codec == "VP8") {
+      RTC_CHECK_EQ(params_.ss[video_idx].num_spatial_layers, 1);
+    } else if (params_.video[video_idx].codec == "VP9") {
+      RTC_CHECK_EQ(params_.ss[video_idx].streams.size(), 1);
+    }
+    RTC_CHECK_GE(params_.call.num_thumbnails, 0);
+    if (params_.call.num_thumbnails > 0) {
+      RTC_CHECK_EQ(params_.ss[video_idx].num_spatial_layers, 1);
+      RTC_CHECK_EQ(params_.ss[video_idx].streams.size(), 3);
+      RTC_CHECK_EQ(params_.video[video_idx].num_temporal_layers, 3);
+      RTC_CHECK_EQ(params_.video[video_idx].codec, "VP8");
+    }
+    // Dual streams with FEC not supported in tests yet.
+    RTC_CHECK(!params_.video[video_idx].flexfec || num_video_streams_ == 1);
+    RTC_CHECK(!params_.video[video_idx].ulpfec || num_video_streams_ == 1);
   }
 }
 
@@ -1232,24 +1249,26 @@
 }
 
 // Static.
-VideoStream VideoQualityTest::DefaultVideoStream(const Params& params) {
+VideoStream VideoQualityTest::DefaultVideoStream(const Params& params,
+                                                 size_t video_idx) {
   VideoStream stream;
-  stream.width = params.video.width;
-  stream.height = params.video.height;
-  stream.max_framerate = params.video.fps;
-  stream.min_bitrate_bps = params.video.min_bitrate_bps;
-  stream.target_bitrate_bps = params.video.target_bitrate_bps;
-  stream.max_bitrate_bps = params.video.max_bitrate_bps;
+  stream.width = params.video[video_idx].width;
+  stream.height = params.video[video_idx].height;
+  stream.max_framerate = params.video[video_idx].fps;
+  stream.min_bitrate_bps = params.video[video_idx].min_bitrate_bps;
+  stream.target_bitrate_bps = params.video[video_idx].target_bitrate_bps;
+  stream.max_bitrate_bps = params.video[video_idx].max_bitrate_bps;
   stream.max_qp = kDefaultMaxQp;
   // TODO(sprang): Can we make this less of a hack?
-  if (params.video.num_temporal_layers == 2) {
+  if (params.video[video_idx].num_temporal_layers == 2) {
     stream.temporal_layer_thresholds_bps.push_back(stream.target_bitrate_bps);
-  } else if (params.video.num_temporal_layers == 3) {
+  } else if (params.video[video_idx].num_temporal_layers == 3) {
     stream.temporal_layer_thresholds_bps.push_back(stream.max_bitrate_bps / 4);
     stream.temporal_layer_thresholds_bps.push_back(stream.target_bitrate_bps);
   } else {
-    RTC_CHECK_LE(params.video.num_temporal_layers, kMaxTemporalStreams);
-    for (int i = 0; i < params.video.num_temporal_layers - 1; ++i) {
+    RTC_CHECK_LE(params.video[video_idx].num_temporal_layers,
+                 kMaxTemporalStreams);
+    for (int i = 0; i < params.video[video_idx].num_temporal_layers - 1; ++i) {
       stream.temporal_layer_thresholds_bps.push_back(static_cast<int>(
           stream.max_bitrate_bps * kVp8LayerRateAlloction[0][i] + 0.5));
     }
@@ -1273,39 +1292,44 @@
 // Static.
 void VideoQualityTest::FillScalabilitySettings(
     Params* params,
+    size_t video_idx,
     const std::vector<std::string>& stream_descriptors,
     int num_streams,
     size_t selected_stream,
     int num_spatial_layers,
     int selected_sl,
     const std::vector<std::string>& sl_descriptors) {
-  if (params->ss.streams.empty() && params->ss.infer_streams) {
+  if (params->ss[video_idx].streams.empty() &&
+      params->ss[video_idx].infer_streams) {
     webrtc::VideoEncoderConfig encoder_config;
     encoder_config.content_type =
-        params->screenshare.enabled
+        params->screenshare[video_idx].enabled
             ? webrtc::VideoEncoderConfig::ContentType::kScreen
             : webrtc::VideoEncoderConfig::ContentType::kRealtimeVideo;
-    encoder_config.max_bitrate_bps = params->video.max_bitrate_bps;
-    encoder_config.min_transmit_bitrate_bps = params->video.min_transmit_bps;
+    encoder_config.max_bitrate_bps = params->video[video_idx].max_bitrate_bps;
+    encoder_config.min_transmit_bitrate_bps =
+        params->video[video_idx].min_transmit_bps;
     encoder_config.number_of_streams = num_streams;
-    encoder_config.spatial_layers = params->ss.spatial_layers;
+    encoder_config.spatial_layers = params->ss[video_idx].spatial_layers;
     encoder_config.video_stream_factory =
         new rtc::RefCountedObject<cricket::EncoderStreamFactory>(
-            params->video.codec, kDefaultMaxQp, params->video.fps,
-            params->screenshare.enabled, true);
-    params->ss.streams =
+            params->video[video_idx].codec, kDefaultMaxQp,
+            params->video[video_idx].fps,
+            params->screenshare[video_idx].enabled, true);
+    params->ss[video_idx].streams =
         encoder_config.video_stream_factory->CreateEncoderStreams(
-            static_cast<int>(params->video.width),
-            static_cast<int>(params->video.height), encoder_config);
+            static_cast<int>(params->video[video_idx].width),
+            static_cast<int>(params->video[video_idx].height), encoder_config);
   } else {
     // 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());
+    RTC_CHECK(params->ss[video_idx].streams.empty());
     for (auto descriptor : stream_descriptors) {
       if (descriptor.empty())
         continue;
-      VideoStream stream = VideoQualityTest::DefaultVideoStream(*params);
+      VideoStream stream =
+          VideoQualityTest::DefaultVideoStream(*params, video_idx);
       std::vector<int> v = VideoQualityTest::ParseCSV(descriptor);
       if (v[0] != -1)
         stream.width = static_cast<size_t>(v[0]);
@@ -1327,17 +1351,17 @@
             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->video.num_temporal_layers, 2);
+        RTC_CHECK_LE(params->video[video_idx].num_temporal_layers, 2);
       }
-      params->ss.streams.push_back(stream);
+      params->ss[video_idx].streams.push_back(stream);
     }
   }
 
-  params->ss.num_spatial_layers = std::max(1, num_spatial_layers);
-  params->ss.selected_stream = selected_stream;
+  params->ss[video_idx].num_spatial_layers = std::max(1, num_spatial_layers);
+  params->ss[video_idx].selected_stream = selected_stream;
 
-  params->ss.selected_sl = selected_sl;
-  RTC_CHECK(params->ss.spatial_layers.empty());
+  params->ss[video_idx].selected_sl = selected_sl;
+  RTC_CHECK(params->ss[video_idx].spatial_layers.empty());
   for (auto descriptor : sl_descriptors) {
     if (descriptor.empty())
       continue;
@@ -1348,120 +1372,202 @@
     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);
+    params->ss[video_idx].spatial_layers.push_back(layer);
   }
 }
 
 void VideoQualityTest::SetupVideo(Transport* send_transport,
                                   Transport* recv_transport) {
-  size_t num_video_streams = params_.ss.streams.size();
-  size_t num_flexfec_streams = params_.video.flexfec ? 1 : 0;
-  CreateSendConfig(num_video_streams, 0, num_flexfec_streams, send_transport);
+  size_t total_streams_used = 0;
+  size_t num_flexfec_streams = params_.video[0].flexfec ? 1 : 0;
+  CreateAudioAndFecSendConfigs(0, num_flexfec_streams, send_transport);
+  CreateMatchingAudioAndFecConfigs(recv_transport);
+  video_receive_configs_.clear();
+  video_send_configs_.clear();
+  video_encoders_.clear();
+  video_encoder_configs_.clear();
+  allocated_decoders_.clear();
+  bool decode_all_receive_streams = true;
+  size_t num_video_substreams = params_.ss[0].streams.size();
+  RTC_CHECK(num_video_streams_ > 0);
+  video_encoder_configs_.resize(num_video_streams_);
+  for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+    video_send_configs_.push_back(VideoSendStream::Config(send_transport));
+    video_encoders_.push_back(nullptr);
+    video_encoder_configs_.push_back(VideoEncoderConfig());
+    num_video_substreams = params_.ss[video_idx].streams.size();
+    RTC_CHECK_GT(num_video_substreams, 0);
+    CreateVideoSendConfig(&video_send_configs_[video_idx], num_video_substreams,
+                          total_streams_used, send_transport);
 
-  int payload_type;
-  if (params_.video.codec == "H264") {
-    video_encoder_ = H264Encoder::Create(cricket::VideoCodec("H264"));
-    payload_type = kPayloadTypeH264;
-  } else if (params_.video.codec == "VP8") {
-    if (params_.screenshare.enabled && params_.ss.streams.size() > 1) {
-      // Simulcast screenshare needs a simulcast encoder adapter to work, since
-      // encoders usually can't natively do simulcast with different frame rates
-      // for the different layers.
-      video_encoder_.reset(
-          new SimulcastEncoderAdapter(new InternalEncoderFactory()));
+    int payload_type;
+    if (params_.video[video_idx].codec == "H264") {
+      video_encoders_[video_idx] =
+          H264Encoder::Create(cricket::VideoCodec("H264"));
+      payload_type = kPayloadTypeH264;
+    } else if (params_.video[video_idx].codec == "VP8") {
+      if (params_.screenshare[video_idx].enabled &&
+          params_.ss[video_idx].streams.size() > 1) {
+        // Simulcast screenshare needs a simulcast encoder adapter to work,
+        // since encoders usually can't natively do simulcast with different
+        // frame rates for the different layers.
+        video_encoders_[video_idx].reset(
+            new SimulcastEncoderAdapter(new InternalEncoderFactory()));
+      } else {
+        video_encoders_[video_idx] = VP8Encoder::Create();
+      }
+      payload_type = kPayloadTypeVP8;
+    } else if (params_.video[video_idx].codec == "VP9") {
+      video_encoders_[video_idx] = VP9Encoder::Create();
+      payload_type = kPayloadTypeVP9;
     } else {
-      video_encoder_ = VP8Encoder::Create();
+      RTC_NOTREACHED() << "Codec not supported!";
+      return;
     }
-    payload_type = kPayloadTypeVP8;
-  } else if (params_.video.codec == "VP9") {
-    video_encoder_ = VP9Encoder::Create();
-    payload_type = kPayloadTypeVP9;
-  } else {
-    RTC_NOTREACHED() << "Codec not supported!";
-    return;
-  }
-  video_send_config_.encoder_settings.encoder = video_encoder_.get();
-  video_send_config_.encoder_settings.payload_name = params_.video.codec;
-  video_send_config_.encoder_settings.payload_type = payload_type;
-  video_send_config_.rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
-  video_send_config_.rtp.rtx.payload_type = kSendRtxPayloadType;
-  for (size_t i = 0; i < num_video_streams; ++i)
-    video_send_config_.rtp.rtx.ssrcs.push_back(kSendRtxSsrcs[i]);
-
-  video_send_config_.rtp.extensions.clear();
-  if (params_.call.send_side_bwe) {
-    video_send_config_.rtp.extensions.push_back(
-        RtpExtension(RtpExtension::kTransportSequenceNumberUri,
-                     test::kTransportSequenceNumberExtensionId));
-  } else {
-    video_send_config_.rtp.extensions.push_back(RtpExtension(
-        RtpExtension::kAbsSendTimeUri, test::kAbsSendTimeExtensionId));
-  }
-  video_send_config_.rtp.extensions.push_back(RtpExtension(
-      RtpExtension::kVideoContentTypeUri, test::kVideoContentTypeExtensionId));
-  video_send_config_.rtp.extensions.push_back(RtpExtension(
-      RtpExtension::kVideoTimingUri, test::kVideoTimingExtensionId));
-
-  video_encoder_config_.min_transmit_bitrate_bps =
-      params_.video.min_transmit_bps;
-
-  video_send_config_.suspend_below_min_bitrate =
-      params_.video.suspend_below_min_bitrate;
-
-  video_encoder_config_.number_of_streams = params_.ss.streams.size();
-  video_encoder_config_.max_bitrate_bps = 0;
-  for (size_t i = 0; i < params_.ss.streams.size(); ++i) {
-    video_encoder_config_.max_bitrate_bps +=
-        params_.ss.streams[i].max_bitrate_bps;
-  }
-  if (params_.ss.infer_streams) {
-    video_encoder_config_.video_stream_factory =
-        new rtc::RefCountedObject<cricket::EncoderStreamFactory>(
-            params_.video.codec, params_.ss.streams[0].max_qp,
-            params_.video.fps, params_.screenshare.enabled, true);
-  } else {
-    video_encoder_config_.video_stream_factory =
-        new rtc::RefCountedObject<VideoStreamFactory>(params_.ss.streams);
-  }
-
-  video_encoder_config_.spatial_layers = params_.ss.spatial_layers;
-
-  CreateMatchingReceiveConfigs(recv_transport);
-
-  const bool decode_all_receive_streams =
-      params_.ss.selected_stream == params_.ss.streams.size();
-
-  for (size_t i = 0; i < num_video_streams; ++i) {
-    video_receive_configs_[i].rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
-    video_receive_configs_[i].rtp.rtx_ssrc = kSendRtxSsrcs[i];
-    video_receive_configs_[i]
-        .rtp.rtx_associated_payload_types[kSendRtxPayloadType] = payload_type;
-    video_receive_configs_[i].rtp.transport_cc = params_.call.send_side_bwe;
-    video_receive_configs_[i].rtp.remb = !params_.call.send_side_bwe;
-    // Enable RTT calculation so NTP time estimator will work.
-    video_receive_configs_[i].rtp.rtcp_xr.receiver_reference_time_report = true;
-    // Force fake decoders on non-selected simulcast streams.
-    if (!decode_all_receive_streams && i != params_.ss.selected_stream) {
-      VideoReceiveStream::Decoder decoder;
-      decoder.decoder = new test::FakeDecoder();
-      decoder.payload_type = video_send_config_.encoder_settings.payload_type;
-      decoder.payload_name = video_send_config_.encoder_settings.payload_name;
-      video_receive_configs_[i].decoders.clear();
-      allocated_decoders_.emplace_back(decoder.decoder);
-      video_receive_configs_[i].decoders.push_back(decoder);
+    video_send_configs_[video_idx].encoder_settings.encoder =
+        video_encoders_[video_idx].get();
+    video_send_configs_[video_idx].encoder_settings.payload_name =
+        params_.video[video_idx].codec;
+    video_send_configs_[video_idx].encoder_settings.payload_type = payload_type;
+    video_send_configs_[video_idx].rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
+    video_send_configs_[video_idx].rtp.rtx.payload_type = kSendRtxPayloadType;
+    for (size_t i = 0; i < num_video_substreams; ++i) {
+      video_send_configs_[video_idx].rtp.rtx.ssrcs.push_back(
+          kSendRtxSsrcs[i + total_streams_used]);
     }
+    video_send_configs_[video_idx].rtp.extensions.clear();
+    if (params_.call.send_side_bwe) {
+      video_send_configs_[video_idx].rtp.extensions.push_back(
+          RtpExtension(RtpExtension::kTransportSequenceNumberUri,
+                       test::kTransportSequenceNumberExtensionId));
+    } else {
+      video_send_configs_[video_idx].rtp.extensions.push_back(RtpExtension(
+          RtpExtension::kAbsSendTimeUri, test::kAbsSendTimeExtensionId));
+    }
+    video_send_configs_[video_idx].rtp.extensions.push_back(
+        RtpExtension(RtpExtension::kVideoContentTypeUri,
+                     test::kVideoContentTypeExtensionId));
+    video_send_configs_[video_idx].rtp.extensions.push_back(RtpExtension(
+        RtpExtension::kVideoTimingUri, test::kVideoTimingExtensionId));
+
+    video_encoder_configs_[video_idx].min_transmit_bitrate_bps =
+        params_.video[video_idx].min_transmit_bps;
+
+    video_send_configs_[video_idx].suspend_below_min_bitrate =
+        params_.video[video_idx].suspend_below_min_bitrate;
+
+    video_encoder_configs_[video_idx].number_of_streams =
+        params_.ss[video_idx].streams.size();
+    video_encoder_configs_[video_idx].max_bitrate_bps = 0;
+    for (size_t i = 0; i < params_.ss[video_idx].streams.size(); ++i) {
+      video_encoder_configs_[video_idx].max_bitrate_bps +=
+          params_.ss[video_idx].streams[i].max_bitrate_bps;
+    }
+    if (params_.ss[video_idx].infer_streams) {
+      video_encoder_configs_[video_idx].video_stream_factory =
+          new rtc::RefCountedObject<cricket::EncoderStreamFactory>(
+              params_.video[video_idx].codec,
+              params_.ss[video_idx].streams[0].max_qp,
+              params_.video[video_idx].fps,
+              params_.screenshare[video_idx].enabled, true);
+    } else {
+      video_encoder_configs_[video_idx].video_stream_factory =
+          new rtc::RefCountedObject<VideoStreamFactory>(
+              params_.ss[video_idx].streams);
+    }
+
+    video_encoder_configs_[video_idx].spatial_layers =
+        params_.ss[video_idx].spatial_layers;
+
+    std::vector<VideoReceiveStream::Config> new_receive_configs =
+        CreateMatchingVideoReceiveConfigs(video_send_configs_[video_idx],
+                                          recv_transport);
+
+    decode_all_receive_streams = params_.ss[video_idx].selected_stream ==
+                                 params_.ss[video_idx].streams.size();
+
+    for (size_t i = 0; i < num_video_substreams; ++i) {
+      new_receive_configs[i].rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
+      new_receive_configs[i].rtp.rtx_ssrc =
+          kSendRtxSsrcs[i + total_streams_used];
+      new_receive_configs[i]
+          .rtp.rtx_associated_payload_types[kSendRtxPayloadType] = payload_type;
+      new_receive_configs[i].rtp.transport_cc = params_.call.send_side_bwe;
+      new_receive_configs[i].rtp.remb = !params_.call.send_side_bwe;
+      // Enable RTT calculation so NTP time estimator will work.
+      new_receive_configs[i].rtp.rtcp_xr.receiver_reference_time_report = true;
+      // Force fake decoders on non-selected simulcast streams.
+      if (!decode_all_receive_streams &&
+          i != params_.ss[video_idx].selected_stream) {
+        VideoReceiveStream::Decoder decoder;
+        decoder.decoder = new test::FakeDecoder();
+        decoder.payload_type =
+            video_send_configs_[video_idx].encoder_settings.payload_type;
+        decoder.payload_name =
+            video_send_configs_[video_idx].encoder_settings.payload_name;
+        new_receive_configs[i].decoders.clear();
+        allocated_decoders_.emplace_back(decoder.decoder);
+        new_receive_configs[i].decoders.push_back(decoder);
+      }
+    }
+
+    for (VideoReceiveStream::Config& config : new_receive_configs) {
+      video_receive_configs_.push_back(config.Copy());
+    }
+
+    if (params_.screenshare[video_idx].enabled) {
+      // Fill out codec settings.
+      video_encoder_configs_[video_idx].content_type =
+          VideoEncoderConfig::ContentType::kScreen;
+      degradation_preference_ =
+          VideoSendStream::DegradationPreference::kMaintainResolution;
+      if (params_.video[video_idx].codec == "VP8") {
+        VideoCodecVP8 vp8_settings = VideoEncoder::GetDefaultVp8Settings();
+        vp8_settings.denoisingOn = false;
+        vp8_settings.frameDroppingOn = false;
+        vp8_settings.numberOfTemporalLayers = static_cast<unsigned char>(
+            params_.video[video_idx].num_temporal_layers);
+        video_encoder_configs_[video_idx].encoder_specific_settings =
+            new rtc::RefCountedObject<
+                VideoEncoderConfig::Vp8EncoderSpecificSettings>(vp8_settings);
+      } else if (params_.video[video_idx].codec == "VP9") {
+        VideoCodecVP9 vp9_settings = VideoEncoder::GetDefaultVp9Settings();
+        vp9_settings.denoisingOn = false;
+        vp9_settings.frameDroppingOn = false;
+        vp9_settings.numberOfTemporalLayers = static_cast<unsigned char>(
+            params_.video[video_idx].num_temporal_layers);
+        vp9_settings.numberOfSpatialLayers = static_cast<unsigned char>(
+            params_.ss[video_idx].num_spatial_layers);
+        video_encoder_configs_[video_idx].encoder_specific_settings =
+            new rtc::RefCountedObject<
+                VideoEncoderConfig::Vp9EncoderSpecificSettings>(vp9_settings);
+      }
+    } else if (params_.ss[video_idx].num_spatial_layers > 1) {
+      // If SVC mode without screenshare, still need to set codec specifics.
+      RTC_CHECK(params_.video[video_idx].codec == "VP9");
+      VideoCodecVP9 vp9_settings = VideoEncoder::GetDefaultVp9Settings();
+      vp9_settings.numberOfTemporalLayers = static_cast<unsigned char>(
+          params_.video[video_idx].num_temporal_layers);
+      vp9_settings.numberOfSpatialLayers =
+          static_cast<unsigned char>(params_.ss[video_idx].num_spatial_layers);
+      video_encoder_configs_[video_idx].encoder_specific_settings =
+          new rtc::RefCountedObject<
+              VideoEncoderConfig::Vp9EncoderSpecificSettings>(vp9_settings);
+    }
+    total_streams_used += num_video_substreams;
   }
 
-  if (params_.video.flexfec) {
+  // FEC supported only for single video stream mode yet.
+  if (params_.video[0].flexfec) {
     // Override send config constructed by CreateSendConfig.
     if (decode_all_receive_streams) {
-      for (uint32_t media_ssrc : video_send_config_.rtp.ssrcs) {
-        video_send_config_.rtp.flexfec.protected_media_ssrcs.push_back(
+      for (uint32_t media_ssrc : video_send_configs_[0].rtp.ssrcs) {
+        video_send_configs_[0].rtp.flexfec.protected_media_ssrcs.push_back(
             media_ssrc);
       }
     } else {
-      video_send_config_.rtp.flexfec.protected_media_ssrcs = {
-          kVideoSendSsrcs[params_.ss.selected_stream]};
+      video_send_configs_[0].rtp.flexfec.protected_media_ssrcs = {
+          kVideoSendSsrcs[params_.ss[0].selected_stream]};
     }
 
     // The matching receive config is _not_ created by
@@ -1469,10 +1575,11 @@
     // Set up the receive config manually instead.
     FlexfecReceiveStream::Config flexfec_receive_config(recv_transport);
     flexfec_receive_config.payload_type =
-        video_send_config_.rtp.flexfec.payload_type;
-    flexfec_receive_config.remote_ssrc = video_send_config_.rtp.flexfec.ssrc;
+        video_send_configs_[0].rtp.flexfec.payload_type;
+    flexfec_receive_config.remote_ssrc =
+        video_send_configs_[0].rtp.flexfec.ssrc;
     flexfec_receive_config.protected_media_ssrcs =
-        video_send_config_.rtp.flexfec.protected_media_ssrcs;
+        video_send_configs_[0].rtp.flexfec.protected_media_ssrcs;
     flexfec_receive_config.local_ssrc = kReceiverLocalVideoSsrc;
     flexfec_receive_config.transport_cc = params_.call.send_side_bwe;
     if (params_.call.send_side_bwe) {
@@ -1484,37 +1591,38 @@
           RtpExtension::kAbsSendTimeUri, test::kAbsSendTimeExtensionId));
     }
     flexfec_receive_configs_.push_back(flexfec_receive_config);
-    if (num_video_streams > 0) {
+    if (num_video_substreams > 0) {
       video_receive_configs_[0].rtp.protected_by_flexfec = true;
     }
   }
 
-  if (params_.video.ulpfec) {
-    video_send_config_.rtp.ulpfec.red_payload_type = kRedPayloadType;
-    video_send_config_.rtp.ulpfec.ulpfec_payload_type = kUlpfecPayloadType;
-    video_send_config_.rtp.ulpfec.red_rtx_payload_type = kRtxRedPayloadType;
+  if (params_.video[0].ulpfec) {
+    video_send_configs_[0].rtp.ulpfec.red_payload_type = kRedPayloadType;
+    video_send_configs_[0].rtp.ulpfec.ulpfec_payload_type = kUlpfecPayloadType;
+    video_send_configs_[0].rtp.ulpfec.red_rtx_payload_type = kRtxRedPayloadType;
 
     if (decode_all_receive_streams) {
       for (auto it = video_receive_configs_.begin();
            it != video_receive_configs_.end(); ++it) {
         it->rtp.red_payload_type =
-            video_send_config_.rtp.ulpfec.red_payload_type;
+            video_send_configs_[0].rtp.ulpfec.red_payload_type;
         it->rtp.ulpfec_payload_type =
-            video_send_config_.rtp.ulpfec.ulpfec_payload_type;
-        it->rtp.rtx_associated_payload_types[video_send_config_.rtp.ulpfec
-                                                 .red_rtx_payload_type] =
-            video_send_config_.rtp.ulpfec.red_payload_type;
+            video_send_configs_[0].rtp.ulpfec.ulpfec_payload_type;
+        it->rtp.rtx_associated_payload_types
+            [video_send_configs_[0].rtp.ulpfec.red_rtx_payload_type] =
+            video_send_configs_[0].rtp.ulpfec.red_payload_type;
       }
     } else {
-      video_receive_configs_[params_.ss.selected_stream].rtp.red_payload_type =
-          video_send_config_.rtp.ulpfec.red_payload_type;
-      video_receive_configs_[params_.ss.selected_stream]
+      video_receive_configs_[params_.ss[0].selected_stream]
+          .rtp.red_payload_type =
+          video_send_configs_[0].rtp.ulpfec.red_payload_type;
+      video_receive_configs_[params_.ss[0].selected_stream]
           .rtp.ulpfec_payload_type =
-          video_send_config_.rtp.ulpfec.ulpfec_payload_type;
-      video_receive_configs_[params_.ss.selected_stream]
-          .rtp.rtx_associated_payload_types[video_send_config_.rtp.ulpfec
-                                                .red_rtx_payload_type] =
-          video_send_config_.rtp.ulpfec.red_payload_type;
+          video_send_configs_[0].rtp.ulpfec.ulpfec_payload_type;
+      video_receive_configs_[params_.ss[0].selected_stream]
+          .rtp.rtx_associated_payload_types
+              [video_send_configs_[0].rtp.ulpfec.red_rtx_payload_type] =
+          video_send_configs_[0].rtp.ulpfec.red_payload_type;
     }
   }
 }
@@ -1530,7 +1638,8 @@
     thumbnail_send_config.rtp.ssrcs.push_back(kThumbnailSendSsrcStart + i);
     thumbnail_send_config.encoder_settings.encoder =
         thumbnail_encoders_.back().get();
-    thumbnail_send_config.encoder_settings.payload_name = params_.video.codec;
+    thumbnail_send_config.encoder_settings.payload_name =
+        params_.video[0].codec;
     thumbnail_send_config.encoder_settings.payload_type = kPayloadTypeVP8;
     thumbnail_send_config.rtp.nack.rtp_history_ms = kNackRtpHistoryMs;
     thumbnail_send_config.rtp.rtx.payload_type = kSendRtxPayloadType;
@@ -1548,19 +1657,19 @@
     VideoEncoderConfig thumbnail_encoder_config;
     thumbnail_encoder_config.min_transmit_bitrate_bps = 7500;
     thumbnail_send_config.suspend_below_min_bitrate =
-        params_.video.suspend_below_min_bitrate;
+        params_.video[0].suspend_below_min_bitrate;
     thumbnail_encoder_config.number_of_streams = 1;
     thumbnail_encoder_config.max_bitrate_bps = 50000;
-    if (params_.ss.infer_streams) {
+    if (params_.ss[0].infer_streams) {
       thumbnail_encoder_config.video_stream_factory =
-          new rtc::RefCountedObject<VideoStreamFactory>(params_.ss.streams);
+          new rtc::RefCountedObject<VideoStreamFactory>(params_.ss[0].streams);
     } else {
       thumbnail_encoder_config.video_stream_factory =
           new rtc::RefCountedObject<cricket::EncoderStreamFactory>(
-              params_.video.codec, params_.ss.streams[0].max_qp,
-              params_.video.fps, params_.screenshare.enabled, true);
+              params_.video[0].codec, params_.ss[0].streams[0].max_qp,
+              params_.video[0].fps, params_.screenshare[0].enabled, true);
     }
-    thumbnail_encoder_config.spatial_layers = params_.ss.spatial_layers;
+    thumbnail_encoder_config.spatial_layers = params_.ss[0].spatial_layers;
 
     VideoReceiveStream::Config thumbnail_receive_config(send_transport);
     thumbnail_receive_config.rtp.remb = false;
@@ -1601,12 +1710,14 @@
 }
 
 void VideoQualityTest::DestroyThumbnailStreams() {
-  for (VideoSendStream* thumbnail_send_stream : thumbnail_send_streams_)
+  for (VideoSendStream* thumbnail_send_stream : thumbnail_send_streams_) {
     receiver_call_->DestroyVideoSendStream(thumbnail_send_stream);
+  }
   thumbnail_send_streams_.clear();
   for (VideoReceiveStream* thumbnail_receive_stream :
-       thumbnail_receive_streams_)
+       thumbnail_receive_streams_) {
     sender_call_->DestroyVideoReceiveStream(thumbnail_receive_stream);
+  }
   thumbnail_send_streams_.clear();
   thumbnail_receive_streams_.clear();
   for (std::unique_ptr<test::VideoCapturer>& video_caputurer :
@@ -1615,85 +1726,6 @@
   }
 }
 
-void VideoQualityTest::SetupScreenshareOrSVC() {
-  if (params_.screenshare.enabled) {
-    // Fill out codec settings.
-    video_encoder_config_.content_type =
-        VideoEncoderConfig::ContentType::kScreen;
-    degradation_preference_ =
-        VideoSendStream::DegradationPreference::kMaintainResolution;
-    if (params_.video.codec == "VP8") {
-      VideoCodecVP8 vp8_settings = VideoEncoder::GetDefaultVp8Settings();
-      vp8_settings.denoisingOn = false;
-      vp8_settings.frameDroppingOn = false;
-      vp8_settings.numberOfTemporalLayers =
-          static_cast<unsigned char>(params_.video.num_temporal_layers);
-      video_encoder_config_.encoder_specific_settings =
-          new rtc::RefCountedObject<
-              VideoEncoderConfig::Vp8EncoderSpecificSettings>(vp8_settings);
-    } else if (params_.video.codec == "VP9") {
-      VideoCodecVP9 vp9_settings = VideoEncoder::GetDefaultVp9Settings();
-      vp9_settings.denoisingOn = false;
-      vp9_settings.frameDroppingOn = false;
-      vp9_settings.numberOfTemporalLayers =
-          static_cast<unsigned char>(params_.video.num_temporal_layers);
-      vp9_settings.numberOfSpatialLayers =
-          static_cast<unsigned char>(params_.ss.num_spatial_layers);
-      video_encoder_config_.encoder_specific_settings =
-          new rtc::RefCountedObject<
-              VideoEncoderConfig::Vp9EncoderSpecificSettings>(vp9_settings);
-    }
-    // Setup frame generator.
-    const size_t kWidth = 1850;
-    const size_t kHeight = 1110;
-    if (params_.screenshare.generate_slides) {
-      frame_generator_ = test::FrameGenerator::CreateSlideGenerator(
-          kWidth, kHeight,
-          params_.screenshare.slide_change_interval * params_.video.fps);
-    } else {
-      std::vector<std::string> slides = params_.screenshare.slides;
-      if (slides.size() == 0) {
-        slides.push_back(test::ResourcePath("web_screenshot_1850_1110", "yuv"));
-        slides.push_back(test::ResourcePath("presentation_1850_1110", "yuv"));
-        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) {
-        // Cycle image every slide_change_interval seconds.
-        frame_generator_ = test::FrameGenerator::CreateFromYuvFile(
-            slides, kWidth, kHeight,
-            params_.screenshare.slide_change_interval * params_.video.fps);
-      } else {
-        RTC_CHECK_LE(params_.video.width, kWidth);
-        RTC_CHECK_LE(params_.video.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);
-
-        frame_generator_ =
-            test::FrameGenerator::CreateScrollingInputFromYuvFiles(
-                clock_, slides, kWidth, kHeight, params_.video.width,
-                params_.video.height,
-                params_.screenshare.scroll_duration * 1000, kPauseDurationMs);
-      }
-    }
-  } else if (params_.ss.num_spatial_layers > 1) {  // For non-screenshare case.
-    RTC_CHECK(params_.video.codec == "VP9");
-    VideoCodecVP9 vp9_settings = VideoEncoder::GetDefaultVp9Settings();
-    vp9_settings.numberOfTemporalLayers =
-        static_cast<unsigned char>(params_.video.num_temporal_layers);
-    vp9_settings.numberOfSpatialLayers =
-        static_cast<unsigned char>(params_.ss.num_spatial_layers);
-    video_encoder_config_.encoder_specific_settings = new rtc::RefCountedObject<
-        VideoEncoderConfig::Vp9EncoderSpecificSettings>(vp9_settings);
-  }
-}
-
 void VideoQualityTest::SetupThumbnailCapturers(size_t num_thumbnail_streams) {
   VideoStream thumbnail = DefaultThumbnailStream();
   for (size_t i = 0; i < num_thumbnail_streams; ++i) {
@@ -1704,47 +1736,106 @@
   }
 }
 
-void VideoQualityTest::CreateCapturer() {
-  if (params_.screenshare.enabled) {
-    test::FrameGeneratorCapturer* frame_generator_capturer =
-        new test::FrameGeneratorCapturer(clock_, std::move(frame_generator_),
-                                         params_.video.fps);
-    EXPECT_TRUE(frame_generator_capturer->Init());
-    video_capturer_.reset(frame_generator_capturer);
+std::unique_ptr<test::FrameGenerator> VideoQualityTest::CreateFrameGenerator(
+    size_t video_idx) {
+  // Setup frame generator.
+  const size_t kWidth = 1850;
+  const size_t kHeight = 1110;
+  std::unique_ptr<test::FrameGenerator> frame_generator;
+  if (params_.screenshare[video_idx].generate_slides) {
+    frame_generator = test::FrameGenerator::CreateSlideGenerator(
+        kWidth, kHeight,
+        params_.screenshare[video_idx].slide_change_interval *
+            params_.video[video_idx].fps);
   } else {
-    if (params_.video.clip_name == "Generator") {
-      video_capturer_.reset(test::FrameGeneratorCapturer::Create(
-          static_cast<int>(params_.video.width),
-          static_cast<int>(params_.video.height), params_.video.fps, clock_));
-    } else if (params_.video.clip_name.empty()) {
-      video_capturer_.reset(test::VcmCapturer::Create(
-          params_.video.width, params_.video.height, params_.video.fps,
-          params_.video.capture_device_index));
-      if (!video_capturer_) {
-        // Failed to get actual camera, use chroma generator as backup.
-        video_capturer_.reset(test::FrameGeneratorCapturer::Create(
-            static_cast<int>(params_.video.width),
-            static_cast<int>(params_.video.height), params_.video.fps, clock_));
-      }
+    std::vector<std::string> slides = params_.screenshare[video_idx].slides;
+    if (slides.size() == 0) {
+      slides.push_back(test::ResourcePath("web_screenshot_1850_1110", "yuv"));
+      slides.push_back(test::ResourcePath("presentation_1850_1110", "yuv"));
+      slides.push_back(test::ResourcePath("photo_1850_1110", "yuv"));
+      slides.push_back(test::ResourcePath("difficult_photo_1850_1110", "yuv"));
+    }
+    if (params_.screenshare[video_idx].scroll_duration == 0) {
+      // Cycle image every slide_change_interval seconds.
+      frame_generator = test::FrameGenerator::CreateFromYuvFile(
+          slides, kWidth, kHeight,
+          params_.screenshare[video_idx].slide_change_interval *
+              params_.video[video_idx].fps);
     } else {
-      video_capturer_.reset(test::FrameGeneratorCapturer::CreateFromYuvFile(
-          test::ResourcePath(params_.video.clip_name, "yuv"),
-          params_.video.width, params_.video.height, params_.video.fps,
-          clock_));
-      ASSERT_TRUE(video_capturer_) << "Could not create capturer for "
-                                   << params_.video.clip_name
-                                   << ".yuv. Is this resource file present?";
+      RTC_CHECK_LE(params_.video[video_idx].width, kWidth);
+      RTC_CHECK_LE(params_.video[video_idx].height, kHeight);
+      RTC_CHECK_GT(params_.screenshare[video_idx].slide_change_interval, 0);
+      const int kPauseDurationMs =
+          (params_.screenshare[video_idx].slide_change_interval -
+           params_.screenshare[video_idx].scroll_duration) *
+          1000;
+      RTC_CHECK_LE(params_.screenshare[video_idx].scroll_duration,
+                   params_.screenshare[video_idx].slide_change_interval);
+
+      frame_generator = test::FrameGenerator::CreateScrollingInputFromYuvFiles(
+          clock_, slides, kWidth, kHeight, params_.video[video_idx].width,
+          params_.video[video_idx].height,
+          params_.screenshare[video_idx].scroll_duration * 1000,
+          kPauseDurationMs);
     }
   }
-  RTC_DCHECK(video_capturer_.get());
+  return frame_generator;
+}
+
+void VideoQualityTest::CreateCapturers() {
+  video_capturers_.resize(num_video_streams_);
+  for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+    if (params_.screenshare[video_idx].enabled) {
+      std::unique_ptr<test::FrameGenerator> frame_generator =
+          CreateFrameGenerator(video_idx);
+      test::FrameGeneratorCapturer* frame_generator_capturer =
+          new test::FrameGeneratorCapturer(clock_, std::move(frame_generator),
+                                           params_.video[video_idx].fps);
+      EXPECT_TRUE(frame_generator_capturer->Init());
+      video_capturers_[video_idx].reset(frame_generator_capturer);
+    } else {
+      if (params_.video[video_idx].clip_name == "Generator") {
+        video_capturers_[video_idx].reset(test::FrameGeneratorCapturer::Create(
+            static_cast<int>(params_.video[video_idx].width),
+            static_cast<int>(params_.video[video_idx].height),
+            params_.video[video_idx].fps, clock_));
+      } else if (params_.video[video_idx].clip_name.empty()) {
+        video_capturers_[video_idx].reset(test::VcmCapturer::Create(
+            params_.video[video_idx].width, params_.video[video_idx].height,
+            params_.video[video_idx].fps,
+            params_.video[video_idx].capture_device_index));
+        if (!video_capturers_[video_idx]) {
+          // Failed to get actual camera, use chroma generator as backup.
+          video_capturers_[video_idx].reset(
+              test::FrameGeneratorCapturer::Create(
+                  static_cast<int>(params_.video[video_idx].width),
+                  static_cast<int>(params_.video[video_idx].height),
+                  params_.video[video_idx].fps, clock_));
+        }
+      } else {
+        video_capturers_[video_idx].reset(
+            test::FrameGeneratorCapturer::CreateFromYuvFile(
+                test::ResourcePath(params_.video[video_idx].clip_name, "yuv"),
+                params_.video[video_idx].width, params_.video[video_idx].height,
+                params_.video[video_idx].fps, clock_));
+        ASSERT_TRUE(video_capturers_[video_idx])
+            << "Could not create capturer for "
+            << params_.video[video_idx].clip_name
+            << ".yuv. Is this resource file present?";
+      }
+    }
+    RTC_DCHECK(video_capturers_[video_idx].get());
+  }
 }
 
 std::unique_ptr<test::LayerFilteringTransport>
 VideoQualityTest::CreateSendTransport() {
   return rtc::MakeUnique<test::LayerFilteringTransport>(
       &task_queue_, params_.pipe, sender_call_.get(), kPayloadTypeVP8,
-      kPayloadTypeVP9, params_.video.selected_tl, params_.ss.selected_sl,
-      payload_type_map_);
+      kPayloadTypeVP9, params_.video[0].selected_tl, params_.ss[0].selected_sl,
+      payload_type_map_, kVideoSendSsrcs[0],
+      static_cast<uint32_t>(kVideoSendSsrcs[0] + params_.ss[0].streams.size() -
+                            1));
 }
 
 std::unique_ptr<test::DirectTransport>
@@ -1753,7 +1844,31 @@
       &task_queue_, params_.pipe, receiver_call_.get(), payload_type_map_);
 }
 
+void VideoQualityTest::CreateVideoStreams() {
+  RTC_DCHECK(video_send_streams_.empty());
+  RTC_DCHECK(video_receive_streams_.empty());
+  RTC_DCHECK_EQ(video_send_configs_.size(), num_video_streams_);
+  for (size_t i = 0; i < video_send_configs_.size(); ++i) {
+    video_send_streams_.push_back(sender_call_->CreateVideoSendStream(
+        video_send_configs_[i].Copy(), video_encoder_configs_[i].Copy()));
+  }
+  for (size_t i = 0; i < video_receive_configs_.size(); ++i) {
+    video_receive_streams_.push_back(receiver_call_->CreateVideoReceiveStream(
+        video_receive_configs_[i].Copy()));
+  }
+
+  AssociateFlexfecStreamsWithVideoStreams();
+}
+
+void VideoQualityTest::DestroyStreams() {
+  CallTest::DestroyStreams();
+
+  for (VideoSendStream* video_send_stream : video_send_streams_)
+    sender_call_->DestroyVideoSendStream(video_send_stream);
+}
+
 void VideoQualityTest::RunWithAnalyzer(const Params& params) {
+  num_video_streams_ = params.call.dual_video ? 2 : 1;
   std::unique_ptr<test::LayerFilteringTransport> send_transport;
   std::unique_ptr<test::DirectTransport> recv_transport;
   FILE* graph_data_output_file = nullptr;
@@ -1819,13 +1934,13 @@
       params_.analyzer.avg_psnr_threshold, params_.analyzer.avg_ssim_threshold,
       is_quick_test_enabled
           ? kFramesSentInQuickTest
-          : params_.analyzer.test_durations_secs * params_.video.fps,
+          : params_.analyzer.test_durations_secs * params_.video[0].fps,
       graph_data_output_file, graph_title,
-      kVideoSendSsrcs[params_.ss.selected_stream],
-      kSendRtxSsrcs[params_.ss.selected_stream],
-      static_cast<size_t>(params_.ss.selected_stream), params.ss.selected_sl,
-      params_.video.selected_tl, is_quick_test_enabled, clock_,
-      params_.logging.rtp_dump_name);
+      kVideoSendSsrcs[params_.ss[0].selected_stream],
+      kSendRtxSsrcs[params_.ss[0].selected_stream],
+      static_cast<size_t>(params_.ss[0].selected_stream),
+      params.ss[0].selected_sl, params_.video[0].selected_tl,
+      is_quick_test_enabled, clock_, params_.logging.rtp_dump_name);
 
   task_queue_.SendTask([&]() {
     analyzer->SetCall(sender_call_.get());
@@ -1835,36 +1950,41 @@
 
     SetupVideo(analyzer.get(), recv_transport.get());
     SetupThumbnails(analyzer.get(), recv_transport.get());
-    video_receive_configs_[params_.ss.selected_stream].renderer =
+    video_receive_configs_[params_.ss[0].selected_stream].renderer =
         analyzer.get();
-    video_send_config_.pre_encode_callback = analyzer->pre_encode_proxy();
-    RTC_DCHECK(!video_send_config_.post_encode_callback);
-    video_send_config_.post_encode_callback = analyzer->encode_timing_proxy();
-
-    SetupScreenshareOrSVC();
+    video_send_configs_[0].pre_encode_callback = analyzer->pre_encode_proxy();
+    RTC_DCHECK(!video_send_configs_[0].post_encode_callback);
+    video_send_configs_[0].post_encode_callback =
+        analyzer->encode_timing_proxy();
 
     CreateFlexfecStreams();
     CreateVideoStreams();
-    analyzer->SetSendStream(video_send_stream_);
+    analyzer->SetSendStream(video_send_streams_[0]);
     if (video_receive_streams_.size() == 1)
       analyzer->SetReceiveStream(video_receive_streams_[0]);
 
-    video_send_stream_->SetSource(analyzer->OutputInterface(),
-                                  degradation_preference_);
-
+    video_send_streams_[0]->SetSource(analyzer->OutputInterface(),
+                                      degradation_preference_);
     SetupThumbnailCapturers(params_.call.num_thumbnails);
     for (size_t i = 0; i < thumbnail_send_streams_.size(); ++i) {
       thumbnail_send_streams_[i]->SetSource(thumbnail_capturers_[i].get(),
                                             degradation_preference_);
     }
 
-    CreateCapturer();
+    CreateCapturers();
 
-    analyzer->SetSource(video_capturer_.get(), params_.ss.infer_streams);
+    analyzer->SetSource(video_capturers_[0].get(), params_.ss[0].infer_streams);
 
-    StartEncodedFrameLogs(video_send_stream_);
-    StartEncodedFrameLogs(video_receive_streams_[params_.ss.selected_stream]);
-    video_send_stream_->Start();
+    for (size_t video_idx = 1; video_idx < num_video_streams_; ++video_idx) {
+      video_send_streams_[video_idx]->SetSource(
+          video_capturers_[video_idx].get(), degradation_preference_);
+    }
+
+    StartEncodedFrameLogs(video_send_streams_[0]);
+    StartEncodedFrameLogs(
+        video_receive_streams_[params_.ss[0].selected_stream]);
+    for (VideoSendStream* video_send_stream : video_send_streams_)
+      video_send_stream->Start();
     for (VideoSendStream* thumbnail_send_stream : thumbnail_send_streams_)
       thumbnail_send_stream->Start();
     for (VideoReceiveStream* receive_stream : video_receive_streams_)
@@ -1875,7 +1995,9 @@
 
     analyzer->StartMeasuringCpuProcessTime();
 
-    video_capturer_->Start();
+    for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+      video_capturers_[video_idx]->Start();
+    }
     for (std::unique_ptr<test::VideoCapturer>& video_caputurer :
          thumbnail_capturers_) {
       video_caputurer->Start();
@@ -1890,7 +2012,9 @@
     for (std::unique_ptr<test::VideoCapturer>& video_caputurer :
          thumbnail_capturers_)
       video_caputurer->Stop();
-    video_capturer_->Stop();
+    for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+      video_capturers_[video_idx]->Stop();
+    }
     for (VideoReceiveStream* thumbnail_receive_stream :
          thumbnail_receive_streams_)
       thumbnail_receive_stream->Stop();
@@ -1898,7 +2022,8 @@
       receive_stream->Stop();
     for (VideoSendStream* thumbnail_send_stream : thumbnail_send_streams_)
       thumbnail_send_stream->Stop();
-    video_send_stream_->Stop();
+    for (VideoSendStream* video_send_stream : video_send_streams_)
+      video_send_stream->Stop();
 
     DestroyStreams();
     DestroyThumbnailStreams();
@@ -1906,7 +2031,7 @@
     if (graph_data_output_file)
       fclose(graph_data_output_file);
 
-    video_capturer_.reset();
+    video_capturers_.clear();
     send_transport.reset();
     recv_transport.reset();
 
@@ -1950,7 +2075,7 @@
   audio_config.rtp.extensions = audio_send_config_.rtp.extensions;
   audio_config.decoder_factory = decoder_factory_;
   audio_config.decoder_map = {{kAudioSendPayloadType, {"OPUS", 48000, 2}}};
-  if (params_.video.enabled && params_.audio.sync_video)
+  if (params_.video[0].enabled && params_.audio.sync_video)
     audio_config.sync_group = kSyncGroup;
 
   *audio_receive_stream =
@@ -1958,6 +2083,7 @@
 }
 
 void VideoQualityTest::RunWithRenderers(const Params& params) {
+  num_video_streams_ = params.call.dual_video ? 2 : 1;
   std::unique_ptr<test::LayerFilteringTransport> send_transport;
   std::unique_ptr<test::DirectTransport> recv_transport;
   ::VoiceEngineState voe;
@@ -1996,13 +2122,9 @@
 
     // TODO(minyue): consider if this is a good transport even for audio only
     // calls.
-    send_transport = rtc::MakeUnique<test::LayerFilteringTransport>(
-        &task_queue_, params.pipe, sender_call_.get(), kPayloadTypeVP8,
-        kPayloadTypeVP9, params.video.selected_tl, params_.ss.selected_sl,
-        payload_type_map_);
+    send_transport = CreateSendTransport();
 
-    recv_transport = rtc::MakeUnique<test::DirectTransport>(
-        &task_queue_, params_.pipe, receiver_call_.get(), payload_type_map_);
+    recv_transport = CreateReceiveTransport();
 
     // 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
@@ -2010,53 +2132,56 @@
     send_transport->SetReceiver(receiver_call_->Receiver());
     recv_transport->SetReceiver(sender_call_->Receiver());
 
-    if (params_.video.enabled) {
+    if (params_.video[0].enabled) {
       // Create video renderers.
       local_preview.reset(test::VideoRenderer::Create(
-          "Local Preview", params_.video.width, params_.video.height));
-
-      const size_t selected_stream_id = params_.ss.selected_stream;
-      const size_t num_streams = params_.ss.streams.size();
-
-      if (selected_stream_id == num_streams) {
-        for (size_t stream_id = 0; stream_id < num_streams; ++stream_id) {
-          std::ostringstream oss;
-          oss << "Loopback Video - Stream #" << static_cast<int>(stream_id);
-          loopback_renderers.emplace_back(test::VideoRenderer::Create(
-              oss.str().c_str(), params_.ss.streams[stream_id].width,
-              params_.ss.streams[stream_id].height));
-        }
-      } else {
-        loopback_renderers.emplace_back(test::VideoRenderer::Create(
-            "Loopback Video", params_.ss.streams[selected_stream_id].width,
-            params_.ss.streams[selected_stream_id].height));
-      }
+          "Local Preview", params_.video[0].width, params_.video[0].height));
 
       SetupVideo(send_transport.get(), recv_transport.get());
+      video_send_configs_[0].pre_encode_callback = local_preview.get();
 
-      video_send_config_.pre_encode_callback = local_preview.get();
-      if (selected_stream_id == num_streams) {
-        for (size_t stream_id = 0; stream_id < num_streams; ++stream_id) {
-          video_receive_configs_[stream_id].renderer =
-              loopback_renderers[stream_id].get();
+      size_t num_streams_processed = 0;
+      for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+        const size_t selected_stream_id = params_.ss[video_idx].selected_stream;
+        const size_t num_streams = params_.ss[video_idx].streams.size();
+        if (selected_stream_id == num_streams) {
+          for (size_t stream_id = 0; stream_id < num_streams; ++stream_id) {
+            std::ostringstream oss;
+            oss << "Loopback Video #" << video_idx << " - Stream #"
+                << static_cast<int>(stream_id);
+            loopback_renderers.emplace_back(test::VideoRenderer::Create(
+                oss.str().c_str(),
+                params_.ss[video_idx].streams[stream_id].width,
+                params_.ss[video_idx].streams[stream_id].height));
+            video_receive_configs_[stream_id + num_streams_processed].renderer =
+                loopback_renderers.back().get();
+            if (params_.audio.enabled && params_.audio.sync_video)
+              video_receive_configs_[stream_id + num_streams_processed]
+                  .sync_group = kSyncGroup;
+          }
+        } else {
+          std::ostringstream oss;
+          oss << "Loopback Video #" << video_idx;
+          loopback_renderers.emplace_back(test::VideoRenderer::Create(
+              oss.str().c_str(),
+              params_.ss[video_idx].streams[selected_stream_id].width,
+              params_.ss[video_idx].streams[selected_stream_id].height));
+          video_receive_configs_[selected_stream_id + num_streams_processed]
+              .renderer = loopback_renderers.back().get();
           if (params_.audio.enabled && params_.audio.sync_video)
-            video_receive_configs_[stream_id].sync_group = kSyncGroup;
+            video_receive_configs_[num_streams_processed + selected_stream_id]
+                .sync_group = kSyncGroup;
         }
-      } else {
-        video_receive_configs_[selected_stream_id].renderer =
-            loopback_renderers.back().get();
-        if (params_.audio.enabled && params_.audio.sync_video)
-          video_receive_configs_[selected_stream_id].sync_group = kSyncGroup;
+        num_streams_processed += num_streams;
       }
-
-      SetupScreenshareOrSVC();
-
       CreateFlexfecStreams();
       CreateVideoStreams();
 
-      CreateCapturer();
-      video_send_stream_->SetSource(video_capturer_.get(),
-                                    degradation_preference_);
+      CreateCapturers();
+      for (size_t video_idx = 0; video_idx < num_video_streams_; ++video_idx) {
+        video_send_streams_[video_idx]->SetSource(
+            video_capturers_[video_idx].get(), degradation_preference_);
+      }
     }
 
     if (params_.audio.enabled) {
@@ -2066,15 +2191,16 @@
 
     for (VideoReceiveStream* receive_stream : video_receive_streams_)
       StartEncodedFrameLogs(receive_stream);
-    StartEncodedFrameLogs(video_send_stream_);
+    StartEncodedFrameLogs(video_send_streams_[0]);
 
     // Start sending and receiving video.
-    if (params_.video.enabled) {
+    if (params_.video[0].enabled) {
       for (VideoReceiveStream* video_receive_stream : video_receive_streams_)
         video_receive_stream->Start();
-
-      video_send_stream_->Start();
-      video_capturer_->Start();
+      for (VideoSendStream* video_send_stream : video_send_streams_)
+        video_send_stream->Start();
+      for (auto& video_capturer : video_capturers_)
+        video_capturer->Start();
     }
 
     if (params_.audio.enabled) {
@@ -2095,30 +2221,18 @@
 
       // Stop receiving audio.
       audio_receive_stream->Stop();
-      sender_call_->DestroyAudioSendStream(audio_send_stream_);
-      receiver_call_->DestroyAudioReceiveStream(audio_receive_stream);
     }
 
     // Stop receiving and sending video.
-    if (params_.video.enabled) {
-      video_capturer_->Stop();
-      video_send_stream_->Stop();
-      for (FlexfecReceiveStream* flexfec_receive_stream :
-           flexfec_receive_streams_) {
-        for (VideoReceiveStream* video_receive_stream :
-             video_receive_streams_) {
-          video_receive_stream->RemoveSecondarySink(flexfec_receive_stream);
-        }
-        receiver_call_->DestroyFlexfecReceiveStream(flexfec_receive_stream);
-      }
-      for (VideoReceiveStream* receive_stream : video_receive_streams_) {
-        receive_stream->Stop();
-        receiver_call_->DestroyVideoReceiveStream(receive_stream);
-      }
-      sender_call_->DestroyVideoSendStream(video_send_stream_);
+    if (params_.video[0].enabled) {
+      for (auto& video_capturer : video_capturers_)
+        video_capturer->Stop();
+      for (VideoSendStream* video_send_stream : video_send_streams_)
+        video_send_stream->Stop();
+      DestroyStreams();
     }
 
-    video_capturer_.reset();
+    video_capturers_.clear();
     send_transport.reset();
     recv_transport.reset();