Add support for simulcast with Vp8 from caller into PC level quality tests.

Add support of negotiating simulcast offer/answer. Also fix some minor
issues around to make it finally work.

Bug: webrtc:10138
Change-Id: I382f5df04ca6ac04d8ed1e030e7b2ae5706dd10c
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/137425
Commit-Queue: Artem Titov <titovartem@webrtc.org>
Reviewed-by: Florent Castelli <orphis@webrtc.org>
Reviewed-by: Karl Wiberg <kwiberg@webrtc.org>
Reviewed-by: Amit Hilbuch <amithi@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#28274}
diff --git a/api/test/peerconnection_quality_test_fixture.h b/api/test/peerconnection_quality_test_fixture.h
index 29105ec..515dcfc 100644
--- a/api/test/peerconnection_quality_test_fixture.h
+++ b/api/test/peerconnection_quality_test_fixture.h
@@ -113,6 +113,31 @@
 
   enum VideoGeneratorType { kDefault, kI420A, kI010 };
 
+  struct VideoSimulcastConfig {
+    VideoSimulcastConfig(int simulcast_streams_count, int target_spatial_index)
+        : simulcast_streams_count(simulcast_streams_count),
+          target_spatial_index(target_spatial_index) {
+      RTC_CHECK_GT(simulcast_streams_count, 1);
+      RTC_CHECK_GE(target_spatial_index, 0);
+      RTC_CHECK_LT(target_spatial_index, simulcast_streams_count);
+    }
+
+    // Specified amount of simulcast streams/SVC layers, depending on which
+    // encoder is used.
+    int simulcast_streams_count;
+    // Specifies spatial index of the video stream to analyze.
+    // There are 2 cases:
+    // 1. simulcast encoder is used:
+    //    in such case |target_spatial_index| will specify the index of
+    //    simulcast stream, that should be analyzed. Other streams will be
+    //    dropped.
+    // 2. SVC encoder is used:
+    //    in such case |target_spatial_index| will specify the top interesting
+    //    spatial layer and all layers below, including target one will be
+    //    processed. All layers above target one will be dropped.
+    int target_spatial_index;
+  };
+
   // Contains properties of single video stream.
   struct VideoConfig {
     VideoConfig(size_t width, size_t height, int32_t fps)
@@ -136,19 +161,13 @@
     absl::optional<std::string> input_file_name;
     // If specified screen share video stream will be created as input.
     absl::optional<ScreenShareConfig> screen_share_config;
-    // Specifies spatial index of the video stream to analyze.
-    // There are 3 cases:
-    // 1. |target_spatial_index| omitted: in such case it will be assumed that
-    //    video stream has not spatial layers and simulcast streams.
-    // 2. |target_spatial_index| presented and simulcast encoder is used:
-    //    in such case |target_spatial_index| will specify the index of
-    //    simulcast stream, that should be analyzed. Other streams will be
-    //    dropped.
-    // 3. |target_spatial_index| presented and SVP encoder is used:
-    //    in such case |target_spatial_index| will specify the top interesting
-    //    spatial layer and all layers bellow, including target one will be
-    //    processed. All layers above target one will be dropped.
-    absl::optional<int> target_spatial_index;
+    // If presented video will be transfered in simulcast/SVC mode depending on
+    // which encoder is used.
+    //
+    // Simulcast is supported only from 1st added peer and for now only for
+    // Vp8 encoder. Also RTX doesn't supported with simulcast and will
+    // automatically disabled for tracks with simulcast.
+    absl::optional<VideoSimulcastConfig> simulcast_config;
     // If specified the input stream will be also copied to specified file.
     // It is actually one of the test's output file, which contains copy of what
     // was captured during the test for this video stream on sender side.
diff --git a/test/DEPS b/test/DEPS
index 80c4719..02810f2 100644
--- a/test/DEPS
+++ b/test/DEPS
@@ -57,5 +57,9 @@
   ],
   ".*peer_connection_quality_test\.(h|cc)": [
     "+pc",
+  ],
+  ".*sdp_changer\.(h|cc)": [
+    "+pc",
+    "+p2p",
   ]
 }
diff --git a/test/pc/e2e/BUILD.gn b/test/pc/e2e/BUILD.gn
index faae55c..6fbc1ca 100644
--- a/test/pc/e2e/BUILD.gn
+++ b/test/pc/e2e/BUILD.gn
@@ -118,6 +118,19 @@
   deps = []
 }
 
+rtc_source_set("simulcast_dummy_buffer_helper") {
+  visibility = [ "*" ]
+  testonly = true
+  sources = [
+    "analyzer/video/simulcast_dummy_buffer_helper.cc",
+    "analyzer/video/simulcast_dummy_buffer_helper.h",
+  ]
+  deps = [
+    "../../../api/video:video_frame",
+    "../../../api/video:video_frame_i420",
+  ]
+}
+
 rtc_source_set("quality_analyzing_video_decoder") {
   visibility = [ "*" ]
   testonly = true
@@ -128,6 +141,7 @@
   deps = [
     ":encoded_image_data_injector_api",
     ":id_generator",
+    ":simulcast_dummy_buffer_helper",
     "../../../api:video_quality_analyzer_api",
     "../../../api/video:encoded_image",
     "../../../api/video:video_frame",
@@ -177,6 +191,7 @@
       ":id_generator",
       ":quality_analyzing_video_decoder",
       ":quality_analyzing_video_encoder",
+      ":simulcast_dummy_buffer_helper",
       "../../../api:stats_observer_interface",
       "../../../api:video_quality_analyzer_api",
       "../../../api/video:video_frame",
@@ -198,7 +213,6 @@
     deps = [
       ":peer_connection_quality_test_params",
       ":video_quality_analyzer_injection_helper",
-      "../../../api:array_view",
       "../../../api:peer_connection_quality_test_fixture_api",
       "../../../api:scoped_refptr",
       "../../../api/rtc_event_log:rtc_event_log_factory",
@@ -472,9 +486,15 @@
     "sdp/sdp_changer.h",
   ]
   deps = [
+    "../../../api:array_view",
     "../../../api:libjingle_peerconnection_api",
     "../../../media:rtc_media_base",
+    "../../../p2p:rtc_p2p",
+    "../../../pc:peerconnection",
+    "../../../pc:rtc_pc_base",
     "../../../rtc_base:stringutils",
+    "//third_party/abseil-cpp/absl/memory:memory",
     "//third_party/abseil-cpp/absl/strings:strings",
+    "//third_party/abseil-cpp/absl/types:optional",
   ]
 }
diff --git a/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.cc b/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.cc
index 6d8ba0f..4216395 100644
--- a/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.cc
+++ b/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.cc
@@ -19,15 +19,10 @@
 #include "api/video/i420_buffer.h"
 #include "modules/video_coding/include/video_error_codes.h"
 #include "rtc_base/logging.h"
+#include "test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.h"
 
 namespace webrtc {
 namespace webrtc_pc_e2e {
-namespace {
-
-constexpr size_t kIrrelatedSimulcastStreamFrameWidth = 320;
-constexpr size_t kIrrelatedSimulcastStreamFrameHeight = 480;
-
-}  // namespace
 
 QualityAnalyzingVideoDecoder::QualityAnalyzingVideoDecoder(
     int id,
@@ -178,33 +173,26 @@
 int32_t
 QualityAnalyzingVideoDecoder::DecoderCallback::IrrelevantSimulcastStreamDecoded(
     uint16_t frame_id,
-    int64_t timestamp_ms) {
-  webrtc::VideoFrame black_frame =
+    uint32_t timestamp_ms) {
+  webrtc::VideoFrame dummy_frame =
       webrtc::VideoFrame::Builder()
-          .set_video_frame_buffer(
-              GetBlackFrameBuffer(kIrrelatedSimulcastStreamFrameWidth,
-                                  kIrrelatedSimulcastStreamFrameHeight))
-          .set_timestamp_ms(timestamp_ms)
+          .set_video_frame_buffer(GetDummyFrameBuffer())
+          .set_timestamp_rtp(timestamp_ms)
           .set_id(frame_id)
           .build();
   rtc::CritScope crit(&callback_lock_);
   RTC_DCHECK(delegate_callback_);
-  return delegate_callback_->Decoded(black_frame);
+  delegate_callback_->Decoded(dummy_frame, absl::nullopt, absl::nullopt);
+  return WEBRTC_VIDEO_CODEC_OK;
 }
 
 rtc::scoped_refptr<webrtc::VideoFrameBuffer>
-QualityAnalyzingVideoDecoder::DecoderCallback::GetBlackFrameBuffer(int width,
-                                                                   int height) {
-  if (!black_frame_buffer_ || black_frame_buffer_->width() != width ||
-      black_frame_buffer_->height() != height) {
-    // Use i420 buffer here as default one and supported by all codecs.
-    rtc::scoped_refptr<webrtc::I420Buffer> buffer =
-        webrtc::I420Buffer::Create(width, height);
-    webrtc::I420Buffer::SetBlack(buffer.get());
-    black_frame_buffer_ = buffer;
+QualityAnalyzingVideoDecoder::DecoderCallback::GetDummyFrameBuffer() {
+  if (!dummy_frame_buffer_) {
+    dummy_frame_buffer_ = CreateDummyFrameBuffer();
   }
 
-  return black_frame_buffer_;
+  return dummy_frame_buffer_;
 }
 
 void QualityAnalyzingVideoDecoder::OnFrameDecoded(
diff --git a/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.h b/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.h
index 94dd13b..5cbc882 100644
--- a/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.h
+++ b/test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.h
@@ -86,16 +86,14 @@
                  absl::optional<uint8_t> qp) override;
 
     int32_t IrrelevantSimulcastStreamDecoded(uint16_t frame_id,
-                                             int64_t timestamp_ms);
+                                             uint32_t timestamp_ms);
 
    private:
-    rtc::scoped_refptr<webrtc::VideoFrameBuffer> GetBlackFrameBuffer(
-        int width,
-        int height);
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> GetDummyFrameBuffer();
 
     QualityAnalyzingVideoDecoder* const decoder_;
 
-    rtc::scoped_refptr<webrtc::VideoFrameBuffer> black_frame_buffer_;
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> dummy_frame_buffer_;
 
     rtc::CriticalSection callback_lock_;
     DecodedImageCallback* delegate_callback_ RTC_GUARDED_BY(callback_lock_);
diff --git a/test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.cc b/test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.cc
new file mode 100644
index 0000000..d0548cb
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.cc
@@ -0,0 +1,57 @@
+/*
+ *  Copyright (c) 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.h"
+
+namespace webrtc {
+namespace webrtc_pc_e2e {
+namespace {
+
+constexpr char kIrrelatedSimulcastStreamFrameData[] = "Dummy!";
+
+}  // namespace
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer> CreateDummyFrameBuffer() {
+  // Use i420 buffer here as default one and supported by all codecs.
+  rtc::scoped_refptr<webrtc::I420Buffer> buffer =
+      webrtc::I420Buffer::Create(2, 2);
+  memcpy(buffer->MutableDataY(), kIrrelatedSimulcastStreamFrameData, 2);
+  memcpy(buffer->MutableDataY() + buffer->StrideY(),
+         kIrrelatedSimulcastStreamFrameData + 2, 2);
+  memcpy(buffer->MutableDataU(), kIrrelatedSimulcastStreamFrameData + 4, 1);
+  memcpy(buffer->MutableDataV(), kIrrelatedSimulcastStreamFrameData + 5, 1);
+  return buffer;
+}
+
+bool IsDummyFrameBuffer(
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> video_frame_buffer) {
+  rtc::scoped_refptr<webrtc::I420BufferInterface> buffer =
+      video_frame_buffer->ToI420();
+  if (buffer->width() != 2 || buffer->height() != 2) {
+    return false;
+  }
+  if (memcmp(buffer->DataY(), kIrrelatedSimulcastStreamFrameData, 2) != 0) {
+    return false;
+  }
+  if (memcmp(buffer->DataY() + buffer->StrideY(),
+             kIrrelatedSimulcastStreamFrameData + 2, 2) != 0) {
+    return false;
+  }
+  if (memcmp(buffer->DataU(), kIrrelatedSimulcastStreamFrameData + 4, 1) != 0) {
+    return false;
+  }
+  if (memcmp(buffer->DataV(), kIrrelatedSimulcastStreamFrameData + 5, 1) != 0) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace webrtc_pc_e2e
+}  // namespace webrtc
diff --git a/test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.h b/test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.h
new file mode 100644
index 0000000..84c5abe
--- /dev/null
+++ b/test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.h
@@ -0,0 +1,28 @@
+/*
+ *  Copyright (c) 2019 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef TEST_PC_E2E_ANALYZER_VIDEO_SIMULCAST_DUMMY_BUFFER_HELPER_H_
+#define TEST_PC_E2E_ANALYZER_VIDEO_SIMULCAST_DUMMY_BUFFER_HELPER_H_
+
+#include "api/video/i420_buffer.h"
+#include "api/video/video_frame_buffer.h"
+
+namespace webrtc {
+namespace webrtc_pc_e2e {
+
+rtc::scoped_refptr<webrtc::VideoFrameBuffer> CreateDummyFrameBuffer();
+
+bool IsDummyFrameBuffer(
+    rtc::scoped_refptr<webrtc::VideoFrameBuffer> video_frame_buffer);
+
+}  // namespace webrtc_pc_e2e
+}  // namespace webrtc
+
+#endif  // TEST_PC_E2E_ANALYZER_VIDEO_SIMULCAST_DUMMY_BUFFER_HELPER_H_
diff --git a/test/pc/e2e/analyzer/video/video_quality_analyzer_injection_helper.cc b/test/pc/e2e/analyzer/video/video_quality_analyzer_injection_helper.cc
index 6f8e64c..f56ea4f 100644
--- a/test/pc/e2e/analyzer/video/video_quality_analyzer_injection_helper.cc
+++ b/test/pc/e2e/analyzer/video/video_quality_analyzer_injection_helper.cc
@@ -15,6 +15,7 @@
 #include "absl/memory/memory.h"
 #include "test/pc/e2e/analyzer/video/quality_analyzing_video_decoder.h"
 #include "test/pc/e2e/analyzer/video/quality_analyzing_video_encoder.h"
+#include "test/pc/e2e/analyzer/video/simulcast_dummy_buffer_helper.h"
 
 namespace webrtc {
 namespace webrtc_pc_e2e {
@@ -71,6 +72,10 @@
   ~AnalyzingVideoSink() override = default;
 
   void OnFrame(const VideoFrame& frame) override {
+    if (IsDummyFrameBuffer(frame.video_frame_buffer()->ToI420())) {
+      // This is dummy frame, so we  don't need to process it further.
+      return;
+    }
     analyzer_->OnFrameRendered(frame);
     if (video_writer_) {
       bool result = video_writer_->WriteFrame(frame);
diff --git a/test/pc/e2e/network_quality_metrics_reporter.cc b/test/pc/e2e/network_quality_metrics_reporter.cc
index 70927fe..085fc78 100644
--- a/test/pc/e2e/network_quality_metrics_reporter.cc
+++ b/test/pc/e2e/network_quality_metrics_reporter.cc
@@ -62,8 +62,10 @@
   ReportResult("bytes_sent", network_label, stats.bytes_sent.bytes(),
                "sizeInBytes");
   ReportResult("packets_sent", network_label, stats.packets_sent, "unitless");
-  ReportResult("average_send_rate", network_label,
-               stats.AverageSendRate().bytes_per_sec(), "bytesPerSecond");
+  ReportResult(
+      "average_send_rate", network_label,
+      stats.packets_sent >= 2 ? stats.AverageSendRate().bytes_per_sec() : 0,
+      "bytesPerSecond");
   ReportResult("bytes_dropped", network_label, stats.bytes_dropped.bytes(),
                "sizeInBytes");
   ReportResult("packets_dropped", network_label, stats.packets_dropped,
@@ -73,7 +75,10 @@
   ReportResult("packets_received", network_label, stats.packets_received,
                "unitless");
   ReportResult("average_receive_rate", network_label,
-               stats.AverageReceiveRate().bytes_per_sec(), "bytesPerSecond");
+               stats.packets_received >= 2
+                   ? stats.AverageReceiveRate().bytes_per_sec()
+                   : 0,
+               "bytesPerSecond");
   ReportResult("sent_packets_loss", network_label, packet_loss, "unitless");
 }
 
diff --git a/test/pc/e2e/peer_connection_e2e_smoke_test.cc b/test/pc/e2e/peer_connection_e2e_smoke_test.cc
index fab4a90..b8b89516 100644
--- a/test/pc/e2e/peer_connection_e2e_smoke_test.cc
+++ b/test/pc/e2e/peer_connection_e2e_smoke_test.cc
@@ -25,14 +25,10 @@
 
 namespace webrtc {
 namespace webrtc_pc_e2e {
+namespace {
 
-// IOS debug builds can be quite slow, disabling to avoid issues with timeouts.
-#if defined(WEBRTC_IOS) && defined(WEBRTC_ARCH_ARM64) && !defined(NDEBUG)
-#define MAYBE_RunWithEmulatedNetwork DISABLED_RunWithEmulatedNetwork
-#else
-#define MAYBE_RunWithEmulatedNetwork RunWithEmulatedNetwork
-#endif
-TEST(PeerConnectionE2EQualityTestSmokeTest, MAYBE_RunWithEmulatedNetwork) {
+class PeerConnectionE2EQualityTestSmokeTest : public ::testing::Test {
+ public:
   using PeerConfigurer = PeerConnectionE2EQualityTestFixture::PeerConfigurer;
   using RunParams = PeerConnectionE2EQualityTestFixture::RunParams;
   using VideoConfig = PeerConnectionE2EQualityTestFixture::VideoConfig;
@@ -40,73 +36,120 @@
   using ScreenShareConfig =
       PeerConnectionE2EQualityTestFixture::ScreenShareConfig;
   using ScrollingParams = PeerConnectionE2EQualityTestFixture::ScrollingParams;
+  using VideoSimulcastConfig =
+      PeerConnectionE2EQualityTestFixture::VideoSimulcastConfig;
 
-  // Setup emulated network
-  std::unique_ptr<NetworkEmulationManager> network_emulation_manager =
-      CreateNetworkEmulationManager();
+  void RunTest(const std::string& test_case_name,
+               const RunParams& run_params,
+               rtc::FunctionView<void(PeerConfigurer*)> alice_configurer,
+               rtc::FunctionView<void(PeerConfigurer*)> bob_configurer) {
+    // Setup emulated network
+    std::unique_ptr<NetworkEmulationManager> network_emulation_manager =
+        CreateNetworkEmulationManager();
 
-  auto alice_network_behavior =
-      absl::make_unique<SimulatedNetwork>(BuiltInNetworkBehaviorConfig());
-  SimulatedNetwork* alice_network_behavior_ptr = alice_network_behavior.get();
-  EmulatedNetworkNode* alice_node =
-      network_emulation_manager->CreateEmulatedNode(
-          std::move(alice_network_behavior));
-  EmulatedNetworkNode* bob_node = network_emulation_manager->CreateEmulatedNode(
-      absl::make_unique<SimulatedNetwork>(BuiltInNetworkBehaviorConfig()));
-  EmulatedEndpoint* alice_endpoint =
-      network_emulation_manager->CreateEndpoint(EmulatedEndpointConfig());
-  EmulatedEndpoint* bob_endpoint =
-      network_emulation_manager->CreateEndpoint(EmulatedEndpointConfig());
-  network_emulation_manager->CreateRoute(alice_endpoint, {alice_node},
-                                         bob_endpoint);
-  network_emulation_manager->CreateRoute(bob_endpoint, {bob_node},
-                                         alice_endpoint);
+    auto alice_network_behavior =
+        absl::make_unique<SimulatedNetwork>(BuiltInNetworkBehaviorConfig());
+    SimulatedNetwork* alice_network_behavior_ptr = alice_network_behavior.get();
+    EmulatedNetworkNode* alice_node =
+        network_emulation_manager->CreateEmulatedNode(
+            std::move(alice_network_behavior));
+    EmulatedNetworkNode* bob_node =
+        network_emulation_manager->CreateEmulatedNode(
+            absl::make_unique<SimulatedNetwork>(
+                BuiltInNetworkBehaviorConfig()));
+    auto* alice_endpoint =
+        network_emulation_manager->CreateEndpoint(EmulatedEndpointConfig());
+    EmulatedEndpoint* bob_endpoint =
+        network_emulation_manager->CreateEndpoint(EmulatedEndpointConfig());
+    network_emulation_manager->CreateRoute(alice_endpoint, {alice_node},
+                                           bob_endpoint);
+    network_emulation_manager->CreateRoute(bob_endpoint, {bob_node},
+                                           alice_endpoint);
 
-  // Create analyzers.
-  std::unique_ptr<VideoQualityAnalyzerInterface> video_quality_analyzer =
-      absl::make_unique<DefaultVideoQualityAnalyzer>();
-  // This is only done for the sake of smoke testing. In general there should
-  // be no need to explicitly pull data from analyzers after the run.
-  auto* video_analyzer_ptr =
-      static_cast<DefaultVideoQualityAnalyzer*>(video_quality_analyzer.get());
+    // Create analyzers.
+    std::unique_ptr<VideoQualityAnalyzerInterface> video_quality_analyzer =
+        absl::make_unique<DefaultVideoQualityAnalyzer>();
+    // This is only done for the sake of smoke testing. In general there should
+    // be no need to explicitly pull data from analyzers after the run.
+    auto* video_analyzer_ptr =
+        static_cast<DefaultVideoQualityAnalyzer*>(video_quality_analyzer.get());
 
-  std::unique_ptr<AudioQualityAnalyzerInterface> audio_quality_analyzer =
-      absl::make_unique<DefaultAudioQualityAnalyzer>();
+    auto fixture = CreatePeerConnectionE2EQualityTestFixture(
+        test_case_name, /*audio_quality_analyzer=*/nullptr,
+        std::move(video_quality_analyzer));
+    fixture->ExecuteAt(TimeDelta::seconds(2),
+                       [alice_network_behavior_ptr](TimeDelta) {
+                         BuiltInNetworkBehaviorConfig config;
+                         config.loss_percent = 5;
+                         alice_network_behavior_ptr->SetConfig(config);
+                       });
 
-  auto fixture = CreatePeerConnectionE2EQualityTestFixture(
-      "smoke_test", std::move(audio_quality_analyzer),
-      std::move(video_quality_analyzer));
-  fixture->ExecuteAt(TimeDelta::seconds(2),
-                     [alice_network_behavior_ptr](TimeDelta) {
-                       BuiltInNetworkBehaviorConfig config;
-                       config.loss_percent = 5;
-                       alice_network_behavior_ptr->SetConfig(config);
-                     });
+    // Setup components. We need to provide rtc::NetworkManager compatible with
+    // emulated network layer.
+    EmulatedNetworkManagerInterface* alice_network =
+        network_emulation_manager->CreateEmulatedNetworkManagerInterface(
+            {alice_endpoint});
+    EmulatedNetworkManagerInterface* bob_network =
+        network_emulation_manager->CreateEmulatedNetworkManagerInterface(
+            {bob_endpoint});
 
-  // Setup components. We need to provide rtc::NetworkManager compatible with
-  // emulated network layer.
-  EmulatedNetworkManagerInterface* alice_network =
-      network_emulation_manager->CreateEmulatedNetworkManagerInterface(
-          {alice_endpoint});
-  fixture->AddPeer(alice_network->network_thread(),
-                   alice_network->network_manager(), [](PeerConfigurer* alice) {
-                     VideoConfig video(640, 360, 30);
-                     video.stream_label = "alice-video";
-                     alice->AddVideoConfig(std::move(video));
+    fixture->AddPeer(alice_network->network_thread(),
+                     alice_network->network_manager(), alice_configurer);
+    fixture->AddPeer(bob_network->network_thread(),
+                     bob_network->network_manager(), bob_configurer);
+    fixture->AddQualityMetricsReporter(
+        absl::make_unique<NetworkQualityMetricsReporter>(alice_network,
+                                                         bob_network));
 
-                     AudioConfig audio;
-                     audio.stream_label = "alice-audio";
-                     audio.mode = AudioConfig::Mode::kFile;
-                     audio.input_file_name = test::ResourcePath(
-                         "pc_quality_smoke_test_alice_source", "wav");
-                     alice->SetAudioConfig(std::move(audio));
-                   });
+    fixture->Run(run_params);
 
-  EmulatedNetworkManagerInterface* bob_network =
-      network_emulation_manager->CreateEmulatedNetworkManagerInterface(
-          {bob_endpoint});
-  fixture->AddPeer(
-      bob_network->network_thread(), bob_network->network_manager(),
+    EXPECT_GE(fixture->GetRealTestDuration(), run_params.run_duration);
+    for (auto stream_label : video_analyzer_ptr->GetKnownVideoStreams()) {
+      FrameCounters stream_conters =
+          video_analyzer_ptr->GetPerStreamCounters().at(stream_label);
+      // 150 = 30fps * 5s. On some devices pipeline can be too slow, so it can
+      // happen, that frames will stuck in the middle, so we actually can't
+      // force real constraints here, so lets just check, that at least 1 frame
+      // passed whole pipeline.
+      EXPECT_GE(stream_conters.captured, 150);
+      EXPECT_GE(stream_conters.pre_encoded, 1);
+      EXPECT_GE(stream_conters.encoded, 1);
+      EXPECT_GE(stream_conters.received, 1);
+      EXPECT_GE(stream_conters.decoded, 1);
+      EXPECT_GE(stream_conters.rendered, 1);
+    }
+  }
+};
+
+}  // namespace
+
+// IOS debug builds can be quite slow, disabling to avoid issues with timeouts.
+#if defined(WEBRTC_IOS) && defined(WEBRTC_ARCH_ARM64) && !defined(NDEBUG)
+#define MAYBE_RunWithEmulatedNetwork DISABLED_RunWithEmulatedNetwork
+#else
+#define MAYBE_RunWithEmulatedNetwork RunWithEmulatedNetwork
+#endif
+TEST_F(PeerConnectionE2EQualityTestSmokeTest, MAYBE_Smoke) {
+  RunParams run_params(TimeDelta::seconds(7));
+  run_params.video_codec_name = cricket::kVp9CodecName;
+  run_params.video_codec_required_params = {{"profile-id", "0"}};
+  run_params.use_flex_fec = true;
+  run_params.use_ulp_fec = true;
+  run_params.video_encoder_bitrate_multiplier = 1.1;
+  RunTest(
+      "smoke", run_params,
+      [](PeerConfigurer* alice) {
+        VideoConfig video(640, 360, 30);
+        video.stream_label = "alice-video";
+        alice->AddVideoConfig(std::move(video));
+
+        AudioConfig audio;
+        audio.stream_label = "alice-audio";
+        audio.mode = AudioConfig::Mode::kFile;
+        audio.input_file_name =
+            test::ResourcePath("pc_quality_smoke_test_alice_source", "wav");
+        alice->SetAudioConfig(std::move(audio));
+      },
       [](PeerConfigurer* bob) {
         VideoConfig video(640, 360, 30);
         video.stream_label = "bob-video";
@@ -127,34 +170,44 @@
             test::ResourcePath("pc_quality_smoke_test_bob_source", "wav");
         bob->SetAudioConfig(std::move(audio));
       });
+}
 
-  fixture->AddQualityMetricsReporter(
-      absl::make_unique<NetworkQualityMetricsReporter>(alice_network,
-                                                       bob_network));
-
+// IOS debug builds can be quite slow, disabling to avoid issues with timeouts.
+#if defined(WEBRTC_IOS) && defined(WEBRTC_ARCH_ARM64) && !defined(NDEBUG)
+#define MAYBE_RunWithEmulatedNetwork DISABLED_RunWithEmulatedNetwork
+#else
+#define MAYBE_RunWithEmulatedNetwork RunWithEmulatedNetwork
+#endif
+TEST_F(PeerConnectionE2EQualityTestSmokeTest, MAYBE_Simulcast) {
   RunParams run_params(TimeDelta::seconds(7));
-  run_params.video_codec_name = cricket::kVp9CodecName;
-  run_params.video_codec_required_params = {{"profile-id", "0"}};
-  run_params.use_flex_fec = true;
-  run_params.use_ulp_fec = true;
-  run_params.video_encoder_bitrate_multiplier = 1.1;
-  fixture->Run(run_params);
+  run_params.video_codec_name = cricket::kVp8CodecName;
+  RunTest(
+      "simulcast", run_params,
+      [](PeerConfigurer* alice) {
+        VideoConfig simulcast(1280, 720, 30);
+        simulcast.stream_label = "alice-simulcast";
+        simulcast.simulcast_config = VideoSimulcastConfig(3, 0);
+        alice->AddVideoConfig(std::move(simulcast));
 
-  EXPECT_GE(fixture->GetRealTestDuration(), run_params.run_duration);
-  for (auto stream_label : video_analyzer_ptr->GetKnownVideoStreams()) {
-    FrameCounters stream_conters =
-        video_analyzer_ptr->GetPerStreamCounters().at(stream_label);
-    // 150 = 30fps * 5s. On some devices pipeline can be too slow, so it can
-    // happen, that frames will stuck in the middle, so we actually can't force
-    // real constraints here, so lets just check, that at least 1 frame passed
-    // whole pipeline.
-    EXPECT_GE(stream_conters.captured, 150);
-    EXPECT_GE(stream_conters.pre_encoded, 1);
-    EXPECT_GE(stream_conters.encoded, 1);
-    EXPECT_GE(stream_conters.received, 1);
-    EXPECT_GE(stream_conters.decoded, 1);
-    EXPECT_GE(stream_conters.rendered, 1);
-  }
+        AudioConfig audio;
+        audio.stream_label = "alice-audio";
+        audio.mode = AudioConfig::Mode::kFile;
+        audio.input_file_name =
+            test::ResourcePath("pc_quality_smoke_test_alice_source", "wav");
+        alice->SetAudioConfig(std::move(audio));
+      },
+      [](PeerConfigurer* bob) {
+        VideoConfig video(640, 360, 30);
+        video.stream_label = "bob-video";
+        bob->AddVideoConfig(std::move(video));
+
+        AudioConfig audio;
+        audio.stream_label = "bob-audio";
+        audio.mode = AudioConfig::Mode::kFile;
+        audio.input_file_name =
+            test::ResourcePath("pc_quality_smoke_test_bob_source", "wav");
+        bob->SetAudioConfig(std::move(audio));
+      });
 }
 
 }  // namespace webrtc_pc_e2e
diff --git a/test/pc/e2e/peer_connection_quality_test.cc b/test/pc/e2e/peer_connection_quality_test.cc
index d3ddb2a..127818d 100644
--- a/test/pc/e2e/peer_connection_quality_test.cc
+++ b/test/pc/e2e/peer_connection_quality_test.cc
@@ -14,6 +14,7 @@
 #include <utility>
 
 #include "absl/memory/memory.h"
+#include "api/jsep.h"
 #include "api/media_stream_interface.h"
 #include "api/peer_connection_interface.h"
 #include "api/rtc_event_log_output_file.h"
@@ -31,7 +32,6 @@
 #include "system_wrappers/include/field_trial.h"
 #include "test/pc/e2e/analyzer/audio/default_audio_quality_analyzer.h"
 #include "test/pc/e2e/analyzer/video/default_video_quality_analyzer.h"
-#include "test/pc/e2e/sdp/sdp_changer.h"
 #include "test/pc/e2e/stats_poller.h"
 #include "test/testsupport/file_utils.h"
 
@@ -460,7 +460,8 @@
   std::set<std::string> audio_labels;
   int media_streams_count = 0;
 
-  for (Params* p : params) {
+  for (size_t i = 0; i < params.size(); ++i) {
+    Params* p = params[i];
     if (p->audio_config) {
       media_streams_count++;
     }
@@ -516,6 +517,13 @@
               video_config.height);
         }
       }
+      if (video_config.simulcast_config) {
+        // We support simulcast only for Vp8 for now.
+        // RTC_CHECK_EQ(run_params.video_codec_name, cricket::kVp8CodecName);
+        // Also we support simulcast only from caller.
+        RTC_CHECK_EQ(i, 0)
+            << "Only simulcast stream from first peer is supported";
+      }
     }
     if (p->audio_config) {
       bool inserted =
@@ -589,16 +597,37 @@
   // forbidden to add new media sections in answer in Unified Plan.
   RtpTransceiverInit receive_only_transceiver_init;
   receive_only_transceiver_init.direction = RtpTransceiverDirection::kRecvOnly;
+  int alice_transceivers_counter = 0;
   if (bob_->params()->audio_config) {
     // Setup receive audio transceiver if Bob has audio to send. If we'll need
     // multiple audio streams, then we need transceiver for each Bob's audio
     // stream.
     alice_->AddTransceiver(cricket::MediaType::MEDIA_TYPE_AUDIO,
                            receive_only_transceiver_init);
+    alice_transceivers_counter++;
+  }
+
+  for (auto& video_config : alice_->params()->video_configs) {
+    if (video_config.simulcast_config) {
+      RtpTransceiverInit transceiver_params;
+      transceiver_params.direction = RtpTransceiverDirection::kSendOnly;
+      for (int i = 0;
+           i < video_config.simulcast_config->simulcast_streams_count; ++i) {
+        RtpEncodingParameters enc_params;
+        // We need to be sure, that all rids will be unique with all mids.
+        enc_params.rid = std::to_string(alice_transceivers_counter) + "000" +
+                         std::to_string(i);
+        transceiver_params.send_encodings.push_back(enc_params);
+      }
+      alice_->AddTransceiver(cricket::MediaType::MEDIA_TYPE_VIDEO,
+                             transceiver_params);
+      alice_transceivers_counter++;
+    }
   }
   for (size_t i = 0; i < bob_->params()->video_configs.size(); ++i) {
     alice_->AddTransceiver(cricket::MediaType::MEDIA_TYPE_VIDEO,
                            receive_only_transceiver_init);
+    alice_transceivers_counter++;
   }
   // Then add media for Alice and Bob
   alice_video_sources_ = MaybeAddMedia(alice_.get());
@@ -751,38 +780,115 @@
 void PeerConnectionE2EQualityTest::SetPeerCodecPreferences(
     TestPeer* peer,
     const RunParams& run_params) {
-  std::vector<RtpCodecCapability> video_capabilities = FilterCodecCapabilities(
-      run_params.video_codec_name, run_params.video_codec_required_params,
-      run_params.use_ulp_fec, run_params.use_flex_fec,
-      peer->pc_factory()
-          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_VIDEO)
-          .codecs);
+  std::vector<RtpCodecCapability> with_rtx_video_capabilities =
+      FilterCodecCapabilities(
+          run_params.video_codec_name, run_params.video_codec_required_params,
+          true, run_params.use_ulp_fec, run_params.use_flex_fec,
+          peer->pc_factory()
+              ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_VIDEO)
+              .codecs);
+  std::vector<RtpCodecCapability> without_rtx_video_capabilities =
+      FilterCodecCapabilities(
+          run_params.video_codec_name, run_params.video_codec_required_params,
+          false, run_params.use_ulp_fec, run_params.use_flex_fec,
+          peer->pc_factory()
+              ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_VIDEO)
+              .codecs);
 
   // Set codecs for transceivers
   for (auto transceiver : peer->pc()->GetTransceivers()) {
     if (transceiver->media_type() == cricket::MediaType::MEDIA_TYPE_VIDEO) {
-      transceiver->SetCodecPreferences(video_capabilities);
+      if (transceiver->sender()->init_send_encodings().size() > 1) {
+        // If transceiver's sender has more then 1 send encodings, it means it
+        // has multiple simulcast streams, so we need disable RTX on it.
+        transceiver->SetCodecPreferences(without_rtx_video_capabilities);
+      } else {
+        transceiver->SetCodecPreferences(with_rtx_video_capabilities);
+      }
     }
   }
 }
 
 void PeerConnectionE2EQualityTest::SetupCall() {
+  SignalingInterceptor signaling_interceptor;
   // Connect peers.
-  ASSERT_TRUE(alice_->ExchangeOfferAnswerWith(bob_.get()));
+  ExchangeOfferAnswer(&signaling_interceptor);
   // Do the SDP negotiation, and also exchange ice candidates.
   ASSERT_EQ_WAIT(alice_->signaling_state(), PeerConnectionInterface::kStable,
                  kDefaultTimeoutMs);
   ASSERT_TRUE_WAIT(alice_->IsIceGatheringDone(), kDefaultTimeoutMs);
   ASSERT_TRUE_WAIT(bob_->IsIceGatheringDone(), kDefaultTimeoutMs);
 
-  // Connect an ICE candidate pairs.
-  ASSERT_TRUE(bob_->AddIceCandidates(alice_->observer()->GetAllCandidates()));
-  ASSERT_TRUE(alice_->AddIceCandidates(bob_->observer()->GetAllCandidates()));
+  ExchangeIceCandidates(&signaling_interceptor);
   // This means that ICE and DTLS are connected.
   ASSERT_TRUE_WAIT(bob_->IsIceConnected(), kDefaultTimeoutMs);
   ASSERT_TRUE_WAIT(alice_->IsIceConnected(), kDefaultTimeoutMs);
 }
 
+void PeerConnectionE2EQualityTest::ExchangeOfferAnswer(
+    SignalingInterceptor* signaling_interceptor) {
+  std::string log_output;
+
+  auto offer = alice_->CreateOffer();
+  RTC_CHECK(offer);
+  offer->ToString(&log_output);
+  RTC_LOG(INFO) << "Original offer: " << log_output;
+  LocalAndRemoteSdp patch_result =
+      signaling_interceptor->PatchOffer(std::move(offer));
+  patch_result.local_sdp->ToString(&log_output);
+  RTC_LOG(INFO) << "Offer to set as local description: " << log_output;
+  patch_result.remote_sdp->ToString(&log_output);
+  RTC_LOG(INFO) << "Offer to set as remote description: " << log_output;
+
+  bool set_local_offer =
+      alice_->SetLocalDescription(std::move(patch_result.local_sdp));
+  RTC_CHECK(set_local_offer);
+  bool set_remote_offer =
+      bob_->SetRemoteDescription(std::move(patch_result.remote_sdp));
+  RTC_CHECK(set_remote_offer);
+  auto answer = bob_->CreateAnswer();
+  RTC_CHECK(answer);
+  answer->ToString(&log_output);
+  RTC_LOG(INFO) << "Original answer: " << log_output;
+  patch_result = signaling_interceptor->PatchAnswer(std::move(answer));
+  patch_result.local_sdp->ToString(&log_output);
+  RTC_LOG(INFO) << "Answer to set as local description: " << log_output;
+  patch_result.remote_sdp->ToString(&log_output);
+  RTC_LOG(INFO) << "Answer to set as remote description: " << log_output;
+
+  bool set_local_answer =
+      bob_->SetLocalDescription(std::move(patch_result.local_sdp));
+  RTC_CHECK(set_local_answer);
+  bool set_remote_answer =
+      alice_->SetRemoteDescription(std::move(patch_result.remote_sdp));
+  RTC_CHECK(set_remote_answer);
+}
+
+void PeerConnectionE2EQualityTest::ExchangeIceCandidates(
+    SignalingInterceptor* signaling_interceptor) {
+  // Connect an ICE candidate pairs.
+  std::vector<std::unique_ptr<IceCandidateInterface>> alice_candidates =
+      signaling_interceptor->PatchOffererIceCandidates(
+          alice_->observer()->GetAllCandidates());
+  for (auto& candidate : alice_candidates) {
+    std::string candidate_str;
+    RTC_CHECK(candidate->ToString(&candidate_str));
+    RTC_LOG(INFO) << "Alice ICE candidate(mid= " << candidate->sdp_mid()
+                  << "): " << candidate_str;
+  }
+  ASSERT_TRUE(bob_->AddIceCandidates(std::move(alice_candidates)));
+  std::vector<std::unique_ptr<IceCandidateInterface>> bob_candidates =
+      signaling_interceptor->PatchAnswererIceCandidates(
+          bob_->observer()->GetAllCandidates());
+  for (auto& candidate : bob_candidates) {
+    std::string candidate_str;
+    RTC_CHECK(candidate->ToString(&candidate_str));
+    RTC_LOG(INFO) << "Bob ICE candidate(mid= " << candidate->sdp_mid()
+                  << "): " << candidate_str;
+  }
+  ASSERT_TRUE(alice_->AddIceCandidates(std::move(bob_candidates)));
+}
+
 void PeerConnectionE2EQualityTest::StartVideo(
     const std::vector<
         rtc::scoped_refptr<FrameGeneratorCapturerVideoTrackSource>>& sources) {
@@ -821,6 +927,7 @@
   if (!file_name) {
     return nullptr;
   }
+  // TODO(titovartem) create only one file writer for simulcast video track.
   auto video_writer = absl::make_unique<test::VideoFrameWriter>(
       file_name.value(), config.width, config.height, config.fps);
   test::VideoFrameWriter* out = video_writer.get();
diff --git a/test/pc/e2e/peer_connection_quality_test.h b/test/pc/e2e/peer_connection_quality_test.h
index 4dbdae7..7b17ffc 100644
--- a/test/pc/e2e/peer_connection_quality_test.h
+++ b/test/pc/e2e/peer_connection_quality_test.h
@@ -32,6 +32,7 @@
 #include "test/pc/e2e/analyzer/video/video_quality_analyzer_injection_helper.h"
 #include "test/pc/e2e/analyzer_helper.h"
 #include "test/pc/e2e/peer_connection_quality_test_params.h"
+#include "test/pc/e2e/sdp/sdp_changer.h"
 #include "test/pc/e2e/test_peer.h"
 #include "test/testsupport/video_frame_writer.h"
 
@@ -239,6 +240,8 @@
   void MaybeAddAudio(TestPeer* peer);
   void SetPeerCodecPreferences(TestPeer* peer, const RunParams& run_params);
   void SetupCall();
+  void ExchangeOfferAnswer(SignalingInterceptor* signaling_interceptor);
+  void ExchangeIceCandidates(SignalingInterceptor* signaling_interceptor);
   void StartVideo(
       const std::vector<
           rtc::scoped_refptr<FrameGeneratorCapturerVideoTrackSource>>& sources);
diff --git a/test/pc/e2e/sdp/sdp_changer.cc b/test/pc/e2e/sdp/sdp_changer.cc
index 2c7dc3c..0819f18 100644
--- a/test/pc/e2e/sdp/sdp_changer.cc
+++ b/test/pc/e2e/sdp/sdp_changer.cc
@@ -12,7 +12,11 @@
 
 #include <utility>
 
+#include "absl/memory/memory.h"
+#include "api/jsep_session_description.h"
 #include "media/base/media_constants.h"
+#include "p2p/base/p2p_constants.h"
+#include "pc/sdp_utils.h"
 #include "rtc_base/strings/string_builder.h"
 
 namespace webrtc {
@@ -33,8 +37,9 @@
 std::vector<RtpCodecCapability> FilterCodecCapabilities(
     absl::string_view codec_name,
     const std::map<std::string, std::string>& codec_required_params,
-    bool ulpfec,
-    bool flexfec,
+    bool use_rtx,
+    bool use_ulpfec,
+    bool use_flexfec,
     std::vector<RtpCodecCapability> supported_codecs) {
   std::vector<RtpCodecCapability> output_codecs;
   // Find main requested codecs among supported and add them to output.
@@ -66,13 +71,13 @@
 
   // Add required FEC and RTX codecs to output.
   for (auto& codec : supported_codecs) {
-    if (codec.name == cricket::kRtxCodecName) {
+    if (codec.name == cricket::kRtxCodecName && use_rtx) {
       output_codecs.push_back(codec);
-    } else if (codec.name == cricket::kFlexfecCodecName && flexfec) {
+    } else if (codec.name == cricket::kFlexfecCodecName && use_flexfec) {
       output_codecs.push_back(codec);
     } else if ((codec.name == cricket::kRedCodecName ||
                 codec.name == cricket::kUlpfecCodecName) &&
-               ulpfec) {
+               use_flexfec) {
       // Red and ulpfec should be enabled or disabled together.
       output_codecs.push_back(codec);
     }
@@ -80,5 +85,367 @@
   return output_codecs;
 }
 
+// If offer has no simulcast video sections - do nothing.
+//
+// If offer has simulcast video sections - for each section creates
+// SimulcastSectionInfo and put it into |context_|.
+void SignalingInterceptor::FillContext(SessionDescriptionInterface* offer) {
+  for (auto& content : offer->description()->contents()) {
+    context_.mids_order.push_back(content.mid());
+    cricket::MediaContentDescription* media_desc = content.media_description();
+    if (media_desc->type() != cricket::MediaType::MEDIA_TYPE_VIDEO) {
+      continue;
+    }
+    if (media_desc->HasSimulcast()) {
+      // We support only single stream simulcast sections with rids.
+      RTC_CHECK_EQ(media_desc->mutable_streams().size(), 1);
+      RTC_CHECK(media_desc->mutable_streams()[0].has_rids());
+
+      // Create SimulcastSectionInfo for this video section.
+      SimulcastSectionInfo info(content.mid(), content.type,
+                                media_desc->mutable_streams()[0].rids());
+
+      // Set new rids basing on created SimulcastSectionInfo.
+      std::vector<cricket::RidDescription> rids;
+      cricket::SimulcastDescription simulcast_description;
+      for (std::string& rid : info.rids) {
+        rids.emplace_back(rid, cricket::RidDirection::kSend);
+        simulcast_description.send_layers().AddLayer(
+            cricket::SimulcastLayer(rid, false));
+      }
+      media_desc->mutable_streams()[0].set_rids(rids);
+      media_desc->set_simulcast_description(simulcast_description);
+
+      info.simulcast_description = media_desc->simulcast_description();
+      for (auto extension : media_desc->rtp_header_extensions()) {
+        if (extension.uri == RtpExtension::kMidUri) {
+          info.mid_extension = extension;
+        } else if (extension.uri == RtpExtension::kRidUri) {
+          info.rid_extension = extension;
+        } else if (extension.uri == RtpExtension::kRepairedRidUri) {
+          info.rrid_extension = extension;
+        }
+      }
+      RTC_CHECK_NE(info.rid_extension.id, 0);
+      RTC_CHECK_NE(info.mid_extension.id, 0);
+      bool transport_description_found = false;
+      for (auto& transport_info : offer->description()->transport_infos()) {
+        if (transport_info.content_name == info.mid) {
+          info.transport_description = transport_info.description;
+          transport_description_found = true;
+          break;
+        }
+      }
+      RTC_CHECK(transport_description_found);
+
+      context_.AddSimulcastInfo(info);
+    }
+  }
+}
+
+LocalAndRemoteSdp SignalingInterceptor::PatchOffer(
+    std::unique_ptr<SessionDescriptionInterface> offer) {
+  FillContext(offer.get());
+  if (!context_.HasSimulcast()) {
+    auto offer_for_remote = CloneSessionDescription(offer.get());
+    return LocalAndRemoteSdp(std::move(offer), std::move(offer_for_remote));
+  }
+
+  // Clone original offer description. We mustn't access original offer after
+  // this point.
+  std::unique_ptr<cricket::SessionDescription> desc =
+      offer->description()->Clone();
+
+  for (auto& info : context_.simulcast_infos) {
+    // For each simulcast section we have to perform:
+    //   1. Swap MID and RID header extensions
+    //   2. Remove RIDs from streams and remove SimulcastDescription
+    //   3. For each RID duplicate media section
+    cricket::ContentInfo* simulcast_content = desc->GetContentByName(info.mid);
+
+    // Now we need to prepare common prototype for "m=video" sections, in which
+    // single simulcast section will be converted. Do it before removing content
+    // because otherwise description will be deleted.
+    std::unique_ptr<cricket::MediaContentDescription> prototype_media_desc =
+        absl::WrapUnique(simulcast_content->media_description()->Copy());
+
+    // Remove simulcast video section from offer.
+    RTC_CHECK(desc->RemoveContentByName(simulcast_content->mid()));
+    // Clear |simulcast_content|, because now it is pointing to removed object.
+    simulcast_content = nullptr;
+
+    // Swap mid and rid extensions, so remote peer will understand rid as mid.
+    // Also remove rid extension.
+    std::vector<webrtc::RtpExtension> extensions =
+        prototype_media_desc->rtp_header_extensions();
+    for (auto ext_it = extensions.begin(); ext_it != extensions.end();) {
+      if (ext_it->uri == RtpExtension::kRidUri) {
+        // We don't need rid extension for remote peer.
+        extensions.erase(ext_it);
+        continue;
+      }
+      if (ext_it->uri == RtpExtension::kRepairedRidUri) {
+        // We don't support RTX in simulcast.
+        extensions.erase(ext_it);
+        continue;
+      }
+      if (ext_it->uri == RtpExtension::kMidUri) {
+        ext_it->id = info.rid_extension.id;
+      }
+      ++ext_it;
+    }
+    prototype_media_desc->ClearRtpHeaderExtensions();
+    prototype_media_desc->set_rtp_header_extensions(extensions);
+
+    // We support only single stream inside video section with simulcast
+    RTC_CHECK_EQ(prototype_media_desc->mutable_streams().size(), 1);
+    // This stream must have rids.
+    RTC_CHECK(prototype_media_desc->mutable_streams()[0].has_rids());
+
+    // Remove rids and simulcast description from media description.
+    prototype_media_desc->mutable_streams()[0].set_rids({});
+    prototype_media_desc->set_simulcast_description(
+        cricket::SimulcastDescription());
+
+    // For each rid add separate video section.
+    for (std::string& rid : info.rids) {
+      desc->AddContent(rid, info.media_protocol_type,
+                       prototype_media_desc->Clone());
+    }
+  }
+
+  // Now we need to add bundle line to have all media bundled together.
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  for (auto& content : desc->contents()) {
+    bundle_group.AddContentName(content.mid());
+  }
+  if (desc->HasGroup(cricket::GROUP_TYPE_BUNDLE)) {
+    desc->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  }
+  desc->AddGroup(bundle_group);
+
+  // Update transport_infos to add TransportInfo for each new media section.
+  std::vector<cricket::TransportInfo> transport_infos = desc->transport_infos();
+  for (auto info_it = transport_infos.begin();
+       info_it != transport_infos.end();) {
+    if (context_.simulcast_infos_by_mid.find(info_it->content_name) !=
+        context_.simulcast_infos_by_mid.end()) {
+      // Remove transport infos that correspond to simulcast video sections.
+      transport_infos.erase(info_it);
+    } else {
+      ++info_it;
+    }
+  }
+  for (auto& info : context_.simulcast_infos) {
+    for (auto& rid : info.rids) {
+      transport_infos.emplace_back(rid, info.transport_description);
+    }
+  }
+  desc->set_transport_infos(transport_infos);
+
+  // Create patched offer.
+  auto patched_offer =
+      absl::make_unique<JsepSessionDescription>(SdpType::kOffer);
+  patched_offer->Initialize(std::move(desc), offer->session_id(),
+                            offer->session_version());
+  return LocalAndRemoteSdp(std::move(offer), std::move(patched_offer));
+}
+
+std::unique_ptr<cricket::SessionDescription>
+SignalingInterceptor::RestoreMediaSectionsOrder(
+    std::unique_ptr<cricket::SessionDescription> source) {
+  std::unique_ptr<cricket::SessionDescription> out = source->Clone();
+  for (auto& mid : context_.mids_order) {
+    RTC_CHECK(out->RemoveContentByName(mid));
+  }
+  RTC_CHECK_EQ(out->contents().size(), 0);
+  for (auto& mid : context_.mids_order) {
+    cricket::ContentInfo* content = source->GetContentByName(mid);
+    RTC_CHECK(content);
+    out->AddContent(mid, content->type, content->media_description()->Clone());
+  }
+  return out;
+}
+
+LocalAndRemoteSdp SignalingInterceptor::PatchAnswer(
+    std::unique_ptr<SessionDescriptionInterface> answer) {
+  if (!context_.HasSimulcast()) {
+    auto answer_for_remote = CloneSessionDescription(answer.get());
+    return LocalAndRemoteSdp(std::move(answer), std::move(answer_for_remote));
+  }
+
+  std::unique_ptr<cricket::SessionDescription> desc =
+      answer->description()->Clone();
+
+  for (auto& info : context_.simulcast_infos) {
+    cricket::ContentInfo* simulcast_content =
+        desc->GetContentByName(info.rids[0]);
+    RTC_CHECK(simulcast_content);
+
+    // Get media description, which will be converted to simulcast answer.
+    std::unique_ptr<cricket::MediaContentDescription> media_desc =
+        simulcast_content->media_description()->Clone();
+    // Set |simulcast_content| to nullptr, because then it will be removed, so
+    // it will point to deleted object.
+    simulcast_content = nullptr;
+
+    // Remove separate media sections for simulcast streams.
+    for (auto& rid : info.rids) {
+      RTC_CHECK(desc->RemoveContentByName(rid));
+    }
+
+    // Patch |media_desc| to make it simulcast answer description.
+    // Restore mid/rid rtp header extensions
+    std::vector<webrtc::RtpExtension> extensions =
+        media_desc->rtp_header_extensions();
+    // First remove existing rid/mid header extensions.
+    for (auto ext_it = extensions.begin(); ext_it != extensions.end();) {
+      if (ext_it->uri == RtpExtension::kMidUri ||
+          ext_it->uri == RtpExtension::kRidUri ||
+          ext_it->uri == RtpExtension::kRepairedRidUri) {
+        extensions.erase(ext_it);
+        continue;
+      }
+      ++ext_it;
+    }
+    // Then add right ones.
+    extensions.push_back(info.mid_extension);
+    extensions.push_back(info.rid_extension);
+    // extensions.push_back(info.rrid_extension);
+    media_desc->ClearRtpHeaderExtensions();
+    media_desc->set_rtp_header_extensions(extensions);
+
+    // Add StreamParams with rids for receive.
+    RTC_CHECK_EQ(media_desc->mutable_streams().size(), 0);
+    std::vector<cricket::RidDescription> rids;
+    for (auto& rid : info.rids) {
+      rids.emplace_back(rid, cricket::RidDirection::kReceive);
+    }
+    cricket::StreamParams stream_params;
+    stream_params.set_rids(rids);
+    media_desc->mutable_streams().push_back(stream_params);
+
+    // Restore SimulcastDescription. It should correspond to one from offer,
+    // but it have to have receive layers instead of send. So we need to put
+    // send layers from offer to receive layers in answer.
+    cricket::SimulcastDescription simulcast_description;
+    for (auto layer : info.simulcast_description.send_layers()) {
+      simulcast_description.receive_layers().AddLayerWithAlternatives(layer);
+    }
+    media_desc->set_simulcast_description(simulcast_description);
+
+    // Add simulcast media section.
+    desc->AddContent(info.mid, info.media_protocol_type, std::move(media_desc));
+  }
+
+  desc = RestoreMediaSectionsOrder(std::move(desc));
+
+  // Now we need to add bundle line to have all media bundled together.
+  cricket::ContentGroup bundle_group(cricket::GROUP_TYPE_BUNDLE);
+  for (auto& content : desc->contents()) {
+    bundle_group.AddContentName(content.mid());
+  }
+  if (desc->HasGroup(cricket::GROUP_TYPE_BUNDLE)) {
+    desc->RemoveGroupByName(cricket::GROUP_TYPE_BUNDLE);
+  }
+  desc->AddGroup(bundle_group);
+
+  // Fix transport_infos: it have to have single info for simulcast section.
+  std::vector<cricket::TransportInfo> transport_infos = desc->transport_infos();
+  std::map<std::string, cricket::TransportDescription>
+      mid_to_transport_description;
+  for (auto info_it = transport_infos.begin();
+       info_it != transport_infos.end();) {
+    auto it = context_.simulcast_infos_by_rid.find(info_it->content_name);
+    if (it != context_.simulcast_infos_by_rid.end()) {
+      // This transport info correspond to some extra added media section.
+      mid_to_transport_description.insert(
+          {it->second->mid, info_it->description});
+      transport_infos.erase(info_it);
+    } else {
+      ++info_it;
+    }
+  }
+  for (auto& info : context_.simulcast_infos) {
+    transport_infos.emplace_back(info.mid,
+                                 mid_to_transport_description.at(info.mid));
+  }
+  desc->set_transport_infos(transport_infos);
+
+  auto patched_answer =
+      absl::make_unique<JsepSessionDescription>(SdpType::kAnswer);
+  patched_answer->Initialize(std::move(desc), answer->session_id(),
+                             answer->session_version());
+  return LocalAndRemoteSdp(std::move(answer), std::move(patched_answer));
+}
+
+std::vector<std::unique_ptr<IceCandidateInterface>>
+SignalingInterceptor::PatchOffererIceCandidates(
+    rtc::ArrayView<const IceCandidateInterface* const> candidates) {
+  std::vector<std::unique_ptr<IceCandidateInterface>> out;
+  for (auto* candidate : candidates) {
+    auto simulcast_info_it =
+        context_.simulcast_infos_by_mid.find(candidate->sdp_mid());
+    if (simulcast_info_it != context_.simulcast_infos_by_mid.end()) {
+      // This is candidate for simulcast section, so it should be transformed
+      // into candidates for replicated sections
+      out.push_back(CreateIceCandidate(simulcast_info_it->second->rids[0], 0,
+                                       candidate->candidate()));
+    } else {
+      out.push_back(CreateIceCandidate(candidate->sdp_mid(),
+                                       candidate->sdp_mline_index(),
+                                       candidate->candidate()));
+    }
+  }
+  RTC_CHECK_GT(out.size(), 0);
+  return out;
+}
+
+std::vector<std::unique_ptr<IceCandidateInterface>>
+SignalingInterceptor::PatchAnswererIceCandidates(
+    rtc::ArrayView<const IceCandidateInterface* const> candidates) {
+  std::vector<std::unique_ptr<IceCandidateInterface>> out;
+  for (auto* candidate : candidates) {
+    auto simulcast_info_it =
+        context_.simulcast_infos_by_rid.find(candidate->sdp_mid());
+    if (simulcast_info_it != context_.simulcast_infos_by_rid.end()) {
+      // This is candidate for replicated section, created from single simulcast
+      // section, so it should be transformed into candidates for simulcast
+      // section.
+      out.push_back(CreateIceCandidate(simulcast_info_it->second->mid, 0,
+                                       candidate->candidate()));
+    } else {
+      out.push_back(CreateIceCandidate(candidate->sdp_mid(),
+                                       candidate->sdp_mline_index(),
+                                       candidate->candidate()));
+    }
+  }
+  RTC_CHECK_GT(out.size(), 0);
+  return out;
+}
+
+SignalingInterceptor::SimulcastSectionInfo::SimulcastSectionInfo(
+    const std::string& mid,
+    cricket::MediaProtocolType media_protocol_type,
+    const std::vector<cricket::RidDescription>& rids_desc)
+    : mid(mid), media_protocol_type(media_protocol_type) {
+  for (auto& rid : rids_desc) {
+    rids.push_back(rid.rid);
+  }
+}
+
+void SignalingInterceptor::SignalingContext::AddSimulcastInfo(
+    const SimulcastSectionInfo& info) {
+  simulcast_infos.push_back(info);
+  bool inserted =
+      simulcast_infos_by_mid.insert({info.mid, &simulcast_infos.back()}).second;
+  RTC_CHECK(inserted);
+  for (auto& rid : info.rids) {
+    inserted =
+        simulcast_infos_by_rid.insert({rid, &simulcast_infos.back()}).second;
+    RTC_CHECK(inserted);
+  }
+}
+
 }  // namespace webrtc_pc_e2e
 }  // namespace webrtc
diff --git a/test/pc/e2e/sdp/sdp_changer.h b/test/pc/e2e/sdp/sdp_changer.h
index dbc76b3..53abc57 100644
--- a/test/pc/e2e/sdp/sdp_changer.h
+++ b/test/pc/e2e/sdp/sdp_changer.h
@@ -16,7 +16,13 @@
 #include <vector>
 
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "api/array_view.h"
+#include "api/jsep.h"
 #include "api/rtp_parameters.h"
+#include "media/base/rid_description.h"
+#include "pc/session_description.h"
+#include "pc/simulcast_description.h"
 
 namespace webrtc {
 namespace webrtc_pc_e2e {
@@ -36,10 +42,79 @@
 std::vector<RtpCodecCapability> FilterCodecCapabilities(
     absl::string_view codec_name,
     const std::map<std::string, std::string>& codec_required_params,
-    bool ulpfec,
-    bool flexfec,
+    bool use_rtx,
+    bool use_ulpfec,
+    bool use_flexfec,
     std::vector<RtpCodecCapability> supported_codecs);
 
+struct LocalAndRemoteSdp {
+  LocalAndRemoteSdp(std::unique_ptr<SessionDescriptionInterface> local_sdp,
+                    std::unique_ptr<SessionDescriptionInterface> remote_sdp)
+      : local_sdp(std::move(local_sdp)), remote_sdp(std::move(remote_sdp)) {}
+
+  // Sdp, that should be as local description on the peer, that created it.
+  std::unique_ptr<SessionDescriptionInterface> local_sdp;
+  // Sdp, that should be set as remote description on the peer opposite to the
+  // one, who created it.
+  std::unique_ptr<SessionDescriptionInterface> remote_sdp;
+};
+
+class SignalingInterceptor {
+ public:
+  LocalAndRemoteSdp PatchOffer(
+      std::unique_ptr<SessionDescriptionInterface> offer);
+  LocalAndRemoteSdp PatchAnswer(
+      std::unique_ptr<SessionDescriptionInterface> offer);
+
+  std::vector<std::unique_ptr<IceCandidateInterface>> PatchOffererIceCandidates(
+      rtc::ArrayView<const IceCandidateInterface* const> candidates);
+  std::vector<std::unique_ptr<IceCandidateInterface>>
+  PatchAnswererIceCandidates(
+      rtc::ArrayView<const IceCandidateInterface* const> candidates);
+
+ private:
+  // Contains information about simulcast section, that is required to perform
+  // modified offer/answer and ice candidates exchange.
+  struct SimulcastSectionInfo {
+    SimulcastSectionInfo(const std::string& mid,
+                         cricket::MediaProtocolType media_protocol_type,
+                         const std::vector<cricket::RidDescription>& rids_desc);
+
+    const std::string mid;
+    const cricket::MediaProtocolType media_protocol_type;
+    std::vector<std::string> rids;
+    cricket::SimulcastDescription simulcast_description;
+    webrtc::RtpExtension mid_extension;
+    webrtc::RtpExtension rid_extension;
+    webrtc::RtpExtension rrid_extension;
+    cricket::TransportDescription transport_description;
+  };
+
+  struct SignalingContext {
+    SignalingContext() = default;
+    // SignalingContext is not copyable and movable.
+    SignalingContext(SignalingContext&) = delete;
+    SignalingContext& operator=(SignalingContext&) = delete;
+    SignalingContext(SignalingContext&&) = delete;
+    SignalingContext& operator=(SignalingContext&&) = delete;
+
+    void AddSimulcastInfo(const SimulcastSectionInfo& info);
+    bool HasSimulcast() const { return !simulcast_infos.empty(); }
+
+    std::vector<SimulcastSectionInfo> simulcast_infos;
+    std::map<std::string, SimulcastSectionInfo*> simulcast_infos_by_mid;
+    std::map<std::string, SimulcastSectionInfo*> simulcast_infos_by_rid;
+
+    std::vector<std::string> mids_order;
+  };
+
+  void FillContext(SessionDescriptionInterface* offer);
+  std::unique_ptr<cricket::SessionDescription> RestoreMediaSectionsOrder(
+      std::unique_ptr<cricket::SessionDescription> source);
+
+  SignalingContext context_;
+};
+
 }  // namespace webrtc_pc_e2e
 }  // namespace webrtc
 
diff --git a/test/pc/e2e/test_peer.cc b/test/pc/e2e/test_peer.cc
index 57bb6d7..cf36bb7 100644
--- a/test/pc/e2e/test_peer.cc
+++ b/test/pc/e2e/test_peer.cc
@@ -77,10 +77,14 @@
     for (auto& video_config : params.video_configs) {
       // Stream label should be set by fixture implementation here.
       RTC_DCHECK(video_config.stream_label);
-      bool res = stream_required_spatial_index
-                     .insert({*video_config.stream_label,
-                              video_config.target_spatial_index})
-                     .second;
+      bool res =
+          stream_required_spatial_index
+              .insert({*video_config.stream_label,
+                       video_config.simulcast_config
+                           ? absl::optional<int>(video_config.simulcast_config
+                                                     ->target_spatial_index)
+                           : absl::nullopt})
+              .second;
       RTC_DCHECK(res) << "Duplicate video_config.stream_label="
                       << *video_config.stream_label;
     }
@@ -314,16 +318,18 @@
 }
 
 bool TestPeer::AddIceCandidates(
-    rtc::ArrayView<const IceCandidateInterface* const> candidates) {
+    std::vector<std::unique_ptr<IceCandidateInterface>> candidates) {
   bool success = true;
-  for (const auto* candidate : candidates) {
-    if (!pc()->AddIceCandidate(candidate)) {
+  for (auto& candidate : candidates) {
+    if (!pc()->AddIceCandidate(candidate.get())) {
       std::string candidate_str;
       bool res = candidate->ToString(&candidate_str);
       RTC_CHECK(res);
       RTC_LOG(LS_ERROR) << "Failed to add ICE candidate, candidate_str="
                         << candidate_str;
       success = false;
+    } else {
+      remote_ice_candidates_.push_back(std::move(candidate));
     }
   }
   return success;
diff --git a/test/pc/e2e/test_peer.h b/test/pc/e2e/test_peer.h
index decc418..df335cc 100644
--- a/test/pc/e2e/test_peer.h
+++ b/test/pc/e2e/test_peer.h
@@ -13,9 +13,9 @@
 
 #include <memory>
 #include <string>
+#include <vector>
 
 #include "absl/memory/memory.h"
-#include "api/array_view.h"
 #include "api/test/peerconnection_quality_test_fixture.h"
 #include "media/base/media_engine.h"
 #include "modules/audio_device/include/test_audio_device.h"
@@ -63,7 +63,7 @@
 
   // Adds provided |candidates| to the owned peer connection.
   bool AddIceCandidates(
-      rtc::ArrayView<const IceCandidateInterface* const> candidates);
+      std::vector<std::unique_ptr<IceCandidateInterface>> candidates);
 
  private:
   TestPeer(rtc::scoped_refptr<PeerConnectionFactoryInterface> pc_factory,
@@ -74,6 +74,8 @@
 
   std::unique_ptr<Params> params_;
   rtc::scoped_refptr<AudioProcessing> audio_processing_;
+
+  std::vector<std::unique_ptr<IceCandidateInterface>> remote_ice_candidates_;
 };
 
 }  // namespace webrtc_pc_e2e