Adds scenario test framework.

Bug: webrtc:9510
Change-Id: I387aab4211f520a1c54832f82032ee724479e89e
Reviewed-on: https://webrtc-review.googlesource.com/89342
Commit-Queue: Sebastian Jansson <srte@webrtc.org>
Reviewed-by: Björn Terelius <terelius@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#24864}
diff --git a/modules/congestion_controller/goog_cc/test/goog_cc_printer.cc b/modules/congestion_controller/goog_cc/test/goog_cc_printer.cc
index 7eafece..3264b37 100644
--- a/modules/congestion_controller/goog_cc/test/goog_cc_printer.cc
+++ b/modules/congestion_controller/goog_cc/test/goog_cc_printer.cc
@@ -60,4 +60,18 @@
   return controller;
 }
 
+GoogCcFeedbackDebugFactory::GoogCcFeedbackDebugFactory(
+    RtcEventLog* event_log,
+    GoogCcStatePrinter* printer)
+    : GoogCcFeedbackNetworkControllerFactory(event_log), printer_(printer) {}
+
+std::unique_ptr<NetworkControllerInterface> GoogCcFeedbackDebugFactory::Create(
+    NetworkControllerConfig config) {
+  RTC_CHECK(controller_ == nullptr);
+  auto controller = GoogCcFeedbackNetworkControllerFactory::Create(config);
+  controller_ = static_cast<GoogCcNetworkController*>(controller.get());
+  printer_->Attach(controller_);
+  return controller;
+}
+
 }  // namespace webrtc
diff --git a/modules/congestion_controller/goog_cc/test/goog_cc_printer.h b/modules/congestion_controller/goog_cc/test/goog_cc_printer.h
index e5bd891..4dfc059 100644
--- a/modules/congestion_controller/goog_cc/test/goog_cc_printer.h
+++ b/modules/congestion_controller/goog_cc/test/goog_cc_printer.h
@@ -43,6 +43,19 @@
   GoogCcStatePrinter* printer_;
   GoogCcNetworkController* controller_ = nullptr;
 };
+
+class GoogCcFeedbackDebugFactory
+    : public GoogCcFeedbackNetworkControllerFactory {
+ public:
+  GoogCcFeedbackDebugFactory(RtcEventLog* event_log,
+                             GoogCcStatePrinter* printer);
+  std::unique_ptr<NetworkControllerInterface> Create(
+      NetworkControllerConfig config) override;
+
+ private:
+  GoogCcStatePrinter* printer_;
+  GoogCcNetworkController* controller_ = nullptr;
+};
 }  // namespace webrtc
 
 #endif  // MODULES_CONGESTION_CONTROLLER_GOOG_CC_TEST_GOOG_CC_PRINTER_H_
diff --git a/test/BUILD.gn b/test/BUILD.gn
index 3fa25cf..ec9e880 100644
--- a/test/BUILD.gn
+++ b/test/BUILD.gn
@@ -28,6 +28,7 @@
     deps += [
       ":test_main",
       ":test_support_unittests",
+      "scenario/scenario_tests",
     ]
   }
 }
@@ -316,6 +317,7 @@
       "../modules/video_coding:simulcast_test_fixture_impl",
       "../rtc_base:rtc_base_approved",
       "../test:single_threaded_task_queue",
+      "scenario:scenario_unittests",
       "//testing/gmock",
       "//testing/gtest",
       "//third_party/abseil-cpp/absl/memory",
diff --git a/test/DEPS b/test/DEPS
index 80f47b2..1106230 100644
--- a/test/DEPS
+++ b/test/DEPS
@@ -8,6 +8,7 @@
   "+media/base",
   "+media/engine",
   "+modules/audio_coding",
+  "+modules/congestion_controller",
   "+modules/audio_device",
   "+modules/audio_mixer",
   "+modules/audio_processing",
diff --git a/test/scenario/BUILD.gn b/test/scenario/BUILD.gn
new file mode 100644
index 0000000..16f3a78
--- /dev/null
+++ b/test/scenario/BUILD.gn
@@ -0,0 +1,122 @@
+# Copyright (c) 2018 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import("../../webrtc.gni")
+
+if (rtc_include_tests) {
+  rtc_source_set("scenario") {
+    testonly = true
+    sources = [
+      "audio_stream.cc",
+      "audio_stream.h",
+      "call_client.cc",
+      "call_client.h",
+      "column_printer.cc",
+      "column_printer.h",
+      "hardware_codecs.cc",
+      "hardware_codecs.h",
+      "network_node.cc",
+      "network_node.h",
+      "scenario.cc",
+      "scenario.h",
+      "scenario_config.cc",
+      "scenario_config.h",
+      "video_stream.cc",
+      "video_stream.h",
+    ]
+    deps = [
+      "../:fake_video_codecs",
+      "../:fileutils",
+      "../:test_common",
+      "../:test_support",
+      "../:video_test_common",
+      "../..:webrtc_common",
+      "../../api:libjingle_peerconnection_api",
+      "../../api:transport_api",
+      "../../api/audio_codecs:builtin_audio_decoder_factory",
+      "../../api/audio_codecs:builtin_audio_encoder_factory",
+      "../../api/units:data_rate",
+      "../../api/units:time_delta",
+      "../../api/units:timestamp",
+      "../../api/video:video_frame",
+      "../../api/video:video_frame_i420",
+      "../../api/video_codecs:video_codecs_api",
+      "../../audio",
+      "../../call",
+      "../../call:call_interfaces",
+      "../../call:rtp_sender",
+      "../../call:simulated_network",
+      "../../call:video_stream_api",
+      "../../common_video",
+      "../../logging:rtc_event_log_api",
+      "../../logging:rtc_event_log_impl_base",
+      "../../logging:rtc_event_log_impl_output",
+      "../../media:rtc_audio_video",
+      "../../media:rtc_internal_video_codecs",
+      "../../media:rtc_media_base",
+      "../../modules/audio_device",
+      "../../modules/audio_device:audio_device_impl",
+      "../../modules/audio_device:mock_audio_device",
+      "../../modules/audio_mixer:audio_mixer_impl",
+      "../../modules/audio_processing",
+      "../../modules/congestion_controller:test_controller_printer",
+      "../../modules/congestion_controller/bbr:test_bbr_printer",
+      "../../modules/congestion_controller/goog_cc:test_goog_cc_printer",
+      "../../modules/rtp_rtcp",
+      "../../modules/rtp_rtcp:mock_rtp_rtcp",
+      "../../modules/rtp_rtcp:rtp_rtcp_format",
+      "../../modules/video_coding:video_codec_interface",
+      "../../modules/video_coding:video_coding_utility",
+      "../../modules/video_coding:webrtc_h264",
+      "../../modules/video_coding:webrtc_multiplex",
+      "../../modules/video_coding:webrtc_vp8",
+      "../../modules/video_coding:webrtc_vp9",
+      "../../rtc_base:checks",
+      "../../rtc_base:rtc_base_approved",
+      "../../rtc_base:rtc_base_tests_utils",
+      "../../rtc_base:rtc_task_queue",
+      "../../rtc_base:sequenced_task_checker",
+      "../../rtc_base:stringutils",
+      "../../system_wrappers",
+      "../../system_wrappers:field_trial_api",
+      "../../system_wrappers:runtime_enabled_features_api",
+      "../../video",
+      "//third_party/abseil-cpp/absl/types:optional",
+    ]
+    if (is_android) {
+      deps += [ "../../modules/video_coding:android_codec_factory_helper" ]
+    } else if (is_ios || is_mac) {
+      deps += [ "../../modules/video_coding:objc_codec_factory_helper" ]
+    }
+    if (!build_with_chromium && is_clang) {
+      suppressed_configs += [ "//build/config/clang:find_bad_constructs" ]
+    }
+  }
+  rtc_source_set("scenario_unittests") {
+    testonly = true
+    sources = [
+      "scenario_unittest.cc",
+    ]
+    if (!build_with_chromium && is_clang) {
+      suppressed_configs += [ "//build/config/clang:find_bad_constructs" ]
+    }
+    deps = [
+      ":scenario",
+      "../../logging:mocks",
+      "../../rtc_base:checks",
+      "../../rtc_base:rtc_base_approved",
+      "../../rtc_base:rtc_base_tests_utils",
+      "../../system_wrappers",
+      "../../test:field_trial",
+      "../../test:test_support",
+      "//system_wrappers:field_trial_api",
+      "//testing/gmock",
+      "//third_party/abseil-cpp/absl/memory",
+    ]
+  }
+}
diff --git a/test/scenario/OWNERS b/test/scenario/OWNERS
new file mode 100644
index 0000000..53e076b
--- /dev/null
+++ b/test/scenario/OWNERS
@@ -0,0 +1 @@
+srte@webrtc.org
diff --git a/test/scenario/audio_stream.cc b/test/scenario/audio_stream.cc
new file mode 100644
index 0000000..e1b645e
--- /dev/null
+++ b/test/scenario/audio_stream.cc
@@ -0,0 +1,171 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/audio_stream.h"
+
+#include "test/call_test.h"
+
+namespace webrtc {
+namespace test {
+
+SendAudioStream::SendAudioStream(
+    CallClient* sender,
+    AudioStreamConfig config,
+    rtc::scoped_refptr<AudioEncoderFactory> encoder_factory,
+    Transport* send_transport)
+    : sender_(sender), config_(config) {
+  AudioSendStream::Config send_config(send_transport);
+  ssrc_ = sender->GetNextAudioSsrc();
+  send_config.rtp.ssrc = ssrc_;
+  SdpAudioFormat::Parameters sdp_params;
+  if (config.source.channels == 2)
+    sdp_params["stereo"] = "1";
+  if (config.encoder.initial_frame_length != TimeDelta::ms(20))
+    sdp_params["ptime"] =
+        std::to_string(config.encoder.initial_frame_length.ms());
+
+  // SdpAudioFormat::num_channels indicates that the encoder is capable of
+  // stereo, but the actual channel count used is based on the "stereo"
+  // parameter.
+  send_config.send_codec_spec = AudioSendStream::Config::SendCodecSpec(
+      CallTest::kAudioSendPayloadType, {"opus", 48000, 2, sdp_params});
+  RTC_DCHECK_LE(config.source.channels, 2);
+  send_config.encoder_factory = encoder_factory;
+
+  if (config.encoder.fixed_rate)
+    send_config.send_codec_spec->target_bitrate_bps =
+        config.encoder.fixed_rate->bps();
+
+  if (config.encoder.allocate_bitrate ||
+      config.stream.in_bandwidth_estimation) {
+    DataRate min_rate = DataRate::Infinity();
+    DataRate max_rate = DataRate::Infinity();
+    if (config.encoder.fixed_rate) {
+      min_rate = *config.encoder.fixed_rate;
+      max_rate = *config.encoder.fixed_rate;
+    } else {
+      min_rate = *config.encoder.min_rate;
+      max_rate = *config.encoder.max_rate;
+    }
+    if (field_trial::IsEnabled("WebRTC-SendSideBwe-WithOverhead")) {
+      TimeDelta frame_length = config.encoder.initial_frame_length;
+      DataSize rtp_overhead = DataSize::bytes(12);
+      DataSize total_overhead = config.stream.packet_overhead + rtp_overhead;
+      min_rate += total_overhead / frame_length;
+      max_rate += total_overhead / frame_length;
+    }
+    send_config.min_bitrate_bps = min_rate.bps();
+    send_config.max_bitrate_bps = max_rate.bps();
+  }
+
+  if (config.stream.in_bandwidth_estimation) {
+    send_config.send_codec_spec->transport_cc_enabled = true;
+    send_config.rtp.extensions = {
+        {RtpExtension::kTransportSequenceNumberUri, 8}};
+  }
+
+  if (config.stream.rate_allocation_priority) {
+    send_config.track_id = sender->GetNextPriorityId();
+  }
+  send_stream_ = sender_->call_->CreateAudioSendStream(send_config);
+  if (field_trial::IsEnabled("WebRTC-SendSideBwe-WithOverhead")) {
+    sender->call_->OnTransportOverheadChanged(
+        MediaType::AUDIO, config.stream.packet_overhead.bytes());
+  }
+}
+
+SendAudioStream::~SendAudioStream() {
+  sender_->call_->DestroyAudioSendStream(send_stream_);
+}
+
+void SendAudioStream::Start() {
+  send_stream_->Start();
+}
+
+bool SendAudioStream::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                       uint64_t receiver,
+                                       Timestamp at_time) {
+  // Removes added overhead before delivering RTCP packet to sender.
+  RTC_DCHECK_GE(packet.size(), config_.stream.packet_overhead.bytes());
+  packet.SetSize(packet.size() - config_.stream.packet_overhead.bytes());
+  sender_->DeliverPacket(MediaType::AUDIO, packet, at_time);
+  return true;
+}
+ReceiveAudioStream::ReceiveAudioStream(
+    CallClient* receiver,
+    AudioStreamConfig config,
+    SendAudioStream* send_stream,
+    rtc::scoped_refptr<AudioDecoderFactory> decoder_factory,
+    Transport* feedback_transport)
+    : receiver_(receiver), config_(config) {
+  AudioReceiveStream::Config recv_config;
+  recv_config.rtp.local_ssrc = CallTest::kReceiverLocalAudioSsrc;
+  recv_config.rtcp_send_transport = feedback_transport;
+  recv_config.rtp.remote_ssrc = send_stream->ssrc_;
+  if (config.stream.in_bandwidth_estimation) {
+    recv_config.rtp.transport_cc = true;
+    recv_config.rtp.extensions = {
+        {RtpExtension::kTransportSequenceNumberUri, 8}};
+  }
+  recv_config.decoder_factory = decoder_factory;
+  recv_config.decoder_map = {
+      {CallTest::kAudioSendPayloadType, {"opus", 48000, 2}}};
+  recv_config.sync_group = config.render.sync_group;
+  receive_stream_ = receiver_->call_->CreateAudioReceiveStream(recv_config);
+}
+ReceiveAudioStream::~ReceiveAudioStream() {
+  receiver_->call_->DestroyAudioReceiveStream(receive_stream_);
+}
+
+bool ReceiveAudioStream::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                          uint64_t receiver,
+                                          Timestamp at_time) {
+  RTC_DCHECK_GE(packet.size(), config_.stream.packet_overhead.bytes());
+  packet.SetSize(packet.size() - config_.stream.packet_overhead.bytes());
+  receiver_->DeliverPacket(MediaType::AUDIO, packet, at_time);
+  return true;
+}
+
+AudioStreamPair::~AudioStreamPair() = default;
+
+AudioStreamPair::AudioStreamPair(
+    CallClient* sender,
+    std::vector<NetworkNode*> send_link,
+    uint64_t send_receiver_id,
+    rtc::scoped_refptr<AudioEncoderFactory> encoder_factory,
+    CallClient* receiver,
+    std::vector<NetworkNode*> return_link,
+    uint64_t return_receiver_id,
+    rtc::scoped_refptr<AudioDecoderFactory> decoder_factory,
+    AudioStreamConfig config)
+    : config_(config),
+      send_link_(send_link),
+      return_link_(return_link),
+      send_transport_(sender,
+                      send_link.front(),
+                      send_receiver_id,
+                      config.stream.packet_overhead),
+      return_transport_(receiver,
+                        return_link.front(),
+                        return_receiver_id,
+                        config.stream.packet_overhead),
+      send_stream_(sender, config, encoder_factory, &send_transport_),
+      receive_stream_(receiver,
+                      config,
+                      &send_stream_,
+                      decoder_factory,
+                      &return_transport_) {
+  NetworkNode::Route(send_transport_.ReceiverId(), send_link_,
+                     &receive_stream_);
+  NetworkNode::Route(return_transport_.ReceiverId(), return_link_,
+                     &send_stream_);
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/audio_stream.h b/test/scenario/audio_stream.h
new file mode 100644
index 0000000..17b8bc3
--- /dev/null
+++ b/test/scenario/audio_stream.h
@@ -0,0 +1,110 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_AUDIO_STREAM_H_
+#define TEST_SCENARIO_AUDIO_STREAM_H_
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "rtc_base/constructormagic.h"
+#include "test/scenario/call_client.h"
+#include "test/scenario/column_printer.h"
+#include "test/scenario/network_node.h"
+#include "test/scenario/scenario_config.h"
+
+namespace webrtc {
+namespace test {
+
+// SendAudioStream represents sending of audio. It can be used for starting the
+// stream if neccessary.
+class SendAudioStream : public NetworkReceiverInterface {
+ public:
+  RTC_DISALLOW_COPY_AND_ASSIGN(SendAudioStream);
+  ~SendAudioStream();
+  void Start();
+
+ private:
+  friend class Scenario;
+  friend class AudioStreamPair;
+  friend class ReceiveAudioStream;
+  SendAudioStream(CallClient* sender,
+                  AudioStreamConfig config,
+                  rtc::scoped_refptr<AudioEncoderFactory> encoder_factory,
+                  Transport* send_transport);
+  // Handles RTCP feedback for this stream.
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+
+  AudioSendStream* send_stream_ = nullptr;
+  CallClient* const sender_;
+  const AudioStreamConfig config_;
+  uint32_t ssrc_;
+};
+
+// ReceiveAudioStream represents an audio receiver. It can't be used directly.
+class ReceiveAudioStream : public NetworkReceiverInterface {
+ public:
+  RTC_DISALLOW_COPY_AND_ASSIGN(ReceiveAudioStream);
+  ~ReceiveAudioStream();
+
+ private:
+  friend class Scenario;
+  friend class AudioStreamPair;
+  ReceiveAudioStream(CallClient* receiver,
+                     AudioStreamConfig config,
+                     SendAudioStream* send_stream,
+                     rtc::scoped_refptr<AudioDecoderFactory> decoder_factory,
+                     Transport* feedback_transport);
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+  AudioReceiveStream* receive_stream_ = nullptr;
+  CallClient* const receiver_;
+  const AudioStreamConfig config_;
+};
+
+// AudioStreamPair represents an audio streaming session. It can be used to
+// access underlying send and receive classes. It can also be used in calls to
+// the Scenario class.
+class AudioStreamPair {
+ public:
+  RTC_DISALLOW_COPY_AND_ASSIGN(AudioStreamPair);
+  ~AudioStreamPair();
+  SendAudioStream* send() { return &send_stream_; }
+  ReceiveAudioStream* receive() { return &receive_stream_; }
+
+ private:
+  friend class Scenario;
+  AudioStreamPair(CallClient* sender,
+                  std::vector<NetworkNode*> send_link,
+                  uint64_t send_receiver_id,
+                  rtc::scoped_refptr<AudioEncoderFactory> encoder_factory,
+
+                  CallClient* receiver,
+                  std::vector<NetworkNode*> return_link,
+                  uint64_t return_receiver_id,
+                  rtc::scoped_refptr<AudioDecoderFactory> decoder_factory,
+                  AudioStreamConfig config);
+
+ private:
+  const AudioStreamConfig config_;
+  std::vector<NetworkNode*> send_link_;
+  std::vector<NetworkNode*> return_link_;
+  NetworkNodeTransport send_transport_;
+  NetworkNodeTransport return_transport_;
+
+  SendAudioStream send_stream_;
+  ReceiveAudioStream receive_stream_;
+};
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_SCENARIO_AUDIO_STREAM_H_
diff --git a/test/scenario/call_client.cc b/test/scenario/call_client.cc
new file mode 100644
index 0000000..47cc3bc
--- /dev/null
+++ b/test/scenario/call_client.cc
@@ -0,0 +1,190 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/call_client.h"
+
+#include <utility>
+
+#include "logging/rtc_event_log/output/rtc_event_log_output_file.h"
+#include "modules/audio_mixer/audio_mixer_impl.h"
+#include "modules/congestion_controller/bbr/test/bbr_printer.h"
+#include "modules/congestion_controller/goog_cc/test/goog_cc_printer.h"
+#include "test/call_test.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+const char* kPriorityStreamId = "priority-track";
+}
+
+LoggingNetworkControllerFactory::LoggingNetworkControllerFactory(
+    std::string filename,
+    TransportControllerConfig config) {
+  if (filename.empty()) {
+    event_log_ = RtcEventLog::CreateNull();
+  } else {
+    event_log_ = RtcEventLog::Create(RtcEventLog::EncodingType::Legacy);
+    bool success = event_log_->StartLogging(
+        absl::make_unique<RtcEventLogOutputFile>(filename + ".rtc.dat",
+                                                 RtcEventLog::kUnlimitedOutput),
+        RtcEventLog::kImmediateOutput);
+    RTC_CHECK(success);
+    cc_out_ = fopen((filename + ".cc_state.txt").c_str(), "w");
+    switch (config.cc) {
+      case TransportControllerConfig::CongestionController::kBbr: {
+        auto bbr_printer = absl::make_unique<BbrStatePrinter>();
+        cc_factory_.reset(new BbrDebugFactory(bbr_printer.get()));
+        cc_printer_.reset(
+            new ControlStatePrinter(cc_out_, std::move(bbr_printer)));
+        break;
+      }
+      case TransportControllerConfig::CongestionController::kGoogCc: {
+        auto goog_printer = absl::make_unique<GoogCcStatePrinter>();
+        cc_factory_.reset(
+            new GoogCcDebugFactory(event_log_.get(), goog_printer.get()));
+        cc_printer_.reset(
+            new ControlStatePrinter(cc_out_, std::move(goog_printer)));
+        break;
+      }
+      case TransportControllerConfig::CongestionController::kGoogCcFeedback: {
+        auto goog_printer = absl::make_unique<GoogCcStatePrinter>();
+        cc_factory_.reset(new GoogCcFeedbackDebugFactory(event_log_.get(),
+                                                         goog_printer.get()));
+        cc_printer_.reset(
+            new ControlStatePrinter(cc_out_, std::move(goog_printer)));
+        break;
+      }
+    }
+    cc_printer_->PrintHeaders();
+  }
+  if (!cc_factory_) {
+    switch (config.cc) {
+      case TransportControllerConfig::CongestionController::kBbr:
+        cc_factory_.reset(new BbrNetworkControllerFactory());
+        break;
+      case TransportControllerConfig::CongestionController::kGoogCcFeedback:
+        cc_factory_.reset(
+            new GoogCcFeedbackNetworkControllerFactory(event_log_.get()));
+        break;
+      case TransportControllerConfig::CongestionController::kGoogCc:
+        cc_factory_.reset(new GoogCcNetworkControllerFactory(event_log_.get()));
+        break;
+    }
+  }
+}
+
+LoggingNetworkControllerFactory::~LoggingNetworkControllerFactory() {
+  if (cc_out_)
+    fclose(cc_out_);
+}
+
+void LoggingNetworkControllerFactory::LogCongestionControllerStats(
+    Timestamp at_time) {
+  if (cc_printer_)
+    cc_printer_->PrintState(at_time);
+}
+
+RtcEventLog* LoggingNetworkControllerFactory::GetEventLog() const {
+  return event_log_.get();
+}
+
+std::unique_ptr<NetworkControllerInterface>
+LoggingNetworkControllerFactory::Create(NetworkControllerConfig config) {
+  return cc_factory_->Create(config);
+}
+
+TimeDelta LoggingNetworkControllerFactory::GetProcessInterval() const {
+  return cc_factory_->GetProcessInterval();
+}
+
+CallClient::CallClient(Clock* clock,
+                       std::string log_filename,
+                       CallClientConfig config)
+    : clock_(clock),
+      network_controller_factory_(log_filename, config.transport) {
+  CallConfig call_config(network_controller_factory_.GetEventLog());
+  call_config.bitrate_config.max_bitrate_bps =
+      config.transport.rates.max_rate.bps_or(-1);
+  call_config.bitrate_config.min_bitrate_bps =
+      config.transport.rates.min_rate.bps();
+  call_config.bitrate_config.start_bitrate_bps =
+      config.transport.rates.start_rate.bps();
+  call_config.network_controller_factory = &network_controller_factory_;
+  call_config.audio_state = InitAudio();
+  call_.reset(Call::Create(call_config));
+  if (!config.priority_target_rate.IsZero() &&
+      config.priority_target_rate.IsFinite()) {
+    call_->SetBitrateAllocationStrategy(
+        absl::make_unique<rtc::AudioPriorityBitrateAllocationStrategy>(
+            kPriorityStreamId, config.priority_target_rate.bps()));
+  }
+}  // namespace test
+
+CallClient::~CallClient() {}
+
+void CallClient::DeliverPacket(MediaType media_type,
+                               rtc::CopyOnWriteBuffer packet,
+                               Timestamp at_time) {
+  call_->Receiver()->DeliverPacket(media_type, packet, at_time.us());
+}
+
+ColumnPrinter CallClient::StatsPrinter() {
+  return ColumnPrinter::Lambda(
+      "pacer_delay call_send_bw",
+      [this](rtc::SimpleStringBuilder& sb) {
+        Call::Stats call_stats = call_->GetStats();
+        sb.AppendFormat("%.3lf %.0lf", call_stats.pacer_delay_ms / 1000.0,
+                        call_stats.send_bandwidth_bps / 8.0);
+      },
+      64);
+}
+
+Call::Stats CallClient::GetStats() {
+  return call_->GetStats();
+}
+
+uint32_t CallClient::GetNextVideoSsrc() {
+  RTC_CHECK_LT(next_video_ssrc_index_, CallTest::kNumSsrcs);
+  return CallTest::kVideoSendSsrcs[next_video_ssrc_index_++];
+}
+
+uint32_t CallClient::GetNextAudioSsrc() {
+  RTC_CHECK_LT(next_audio_ssrc_index_, 1);
+  next_audio_ssrc_index_++;
+  return CallTest::kAudioSendSsrc;
+}
+
+uint32_t CallClient::GetNextRtxSsrc() {
+  RTC_CHECK_LT(next_rtx_ssrc_index_, CallTest::kNumSsrcs);
+  return CallTest::kSendRtxSsrcs[next_rtx_ssrc_index_++];
+}
+
+std::string CallClient::GetNextPriorityId() {
+  RTC_CHECK_LT(next_priority_index_++, 1);
+  return kPriorityStreamId;
+}
+
+rtc::scoped_refptr<AudioState> CallClient::InitAudio() {
+  auto capturer = TestAudioDeviceModule::CreatePulsedNoiseCapturer(256, 48000);
+  auto renderer = TestAudioDeviceModule::CreateDiscardRenderer(48000);
+  fake_audio_device_ = TestAudioDeviceModule::CreateTestAudioDeviceModule(
+      std::move(capturer), std::move(renderer), 1.f);
+  apm_ = AudioProcessingBuilder().Create();
+  fake_audio_device_->Init();
+  AudioState::Config audio_state_config;
+  audio_state_config.audio_mixer = AudioMixerImpl::Create();
+  audio_state_config.audio_processing = apm_;
+  audio_state_config.audio_device_module = fake_audio_device_;
+  auto audio_state = AudioState::Create(audio_state_config);
+  fake_audio_device_->RegisterAudioCallback(audio_state->audio_transport());
+  return audio_state;
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/call_client.h b/test/scenario/call_client.h
new file mode 100644
index 0000000..80e2faf
--- /dev/null
+++ b/test/scenario/call_client.h
@@ -0,0 +1,94 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_CALL_CLIENT_H_
+#define TEST_SCENARIO_CALL_CLIENT_H_
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "call/call.h"
+#include "logging/rtc_event_log/rtc_event_log.h"
+#include "modules/audio_device/include/test_audio_device.h"
+#include "modules/congestion_controller/test/controller_printer.h"
+#include "rtc_base/constructormagic.h"
+#include "test/scenario/column_printer.h"
+#include "test/scenario/scenario_config.h"
+
+namespace webrtc {
+
+namespace test {
+class LoggingNetworkControllerFactory
+    : public NetworkControllerFactoryInterface {
+ public:
+  LoggingNetworkControllerFactory(std::string filename,
+                                  TransportControllerConfig config);
+  RTC_DISALLOW_COPY_AND_ASSIGN(LoggingNetworkControllerFactory);
+  ~LoggingNetworkControllerFactory();
+  std::unique_ptr<NetworkControllerInterface> Create(
+      NetworkControllerConfig config) override;
+  TimeDelta GetProcessInterval() const override;
+  // TODO(srte): Consider using the Columnprinter interface for this.
+  void LogCongestionControllerStats(Timestamp at_time);
+  RtcEventLog* GetEventLog() const;
+
+ private:
+  std::unique_ptr<RtcEventLog> event_log_;
+  std::unique_ptr<NetworkControllerFactoryInterface> cc_factory_;
+  std::unique_ptr<ControlStatePrinter> cc_printer_;
+  FILE* cc_out_ = nullptr;
+};
+
+// CallClient represents a participant in a call scenario. It is created by the
+// Scenario class and is used as sender and receiver when setting up a media
+// stream session.
+class CallClient {
+ public:
+  CallClient(Clock* clock, std::string log_filename, CallClientConfig config);
+  RTC_DISALLOW_COPY_AND_ASSIGN(CallClient);
+
+  ~CallClient();
+  ColumnPrinter StatsPrinter();
+  Call::Stats GetStats();
+
+ private:
+  friend class Scenario;
+  friend class SendVideoStream;
+  friend class ReceiveVideoStream;
+  friend class SendAudioStream;
+  friend class ReceiveAudioStream;
+  friend class NetworkNodeTransport;
+  // TODO(srte): Consider using the Columnprinter interface for this.
+  void DeliverPacket(MediaType media_type,
+                     rtc::CopyOnWriteBuffer packet,
+                     Timestamp at_time);
+  uint32_t GetNextVideoSsrc();
+  uint32_t GetNextAudioSsrc();
+  uint32_t GetNextRtxSsrc();
+  std::string GetNextPriorityId();
+
+  Clock* clock_;
+  LoggingNetworkControllerFactory network_controller_factory_;
+  std::unique_ptr<Call> call_;
+
+  rtc::scoped_refptr<AudioState> InitAudio();
+
+  rtc::scoped_refptr<AudioProcessing> apm_;
+  rtc::scoped_refptr<TestAudioDeviceModule> fake_audio_device_;
+
+  std::unique_ptr<FecControllerFactoryInterface> fec_controller_factory_;
+  int next_video_ssrc_index_ = 0;
+  int next_rtx_ssrc_index_ = 0;
+  int next_audio_ssrc_index_ = 0;
+  int next_priority_index_ = 0;
+};
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_SCENARIO_CALL_CLIENT_H_
diff --git a/test/scenario/column_printer.cc b/test/scenario/column_printer.cc
new file mode 100644
index 0000000..234c919
--- /dev/null
+++ b/test/scenario/column_printer.cc
@@ -0,0 +1,85 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/column_printer.h"
+
+namespace webrtc {
+namespace test {
+
+ColumnPrinter::ColumnPrinter(const ColumnPrinter&) = default;
+ColumnPrinter::~ColumnPrinter() = default;
+
+ColumnPrinter::ColumnPrinter(
+    const char* headers,
+    std::function<void(rtc::SimpleStringBuilder&)> printer,
+    size_t max_length)
+    : headers_(headers), printer_(printer), max_length_(max_length) {}
+
+ColumnPrinter ColumnPrinter::Fixed(const char* headers, std::string fields) {
+  return ColumnPrinter(headers,
+                       [fields](rtc::SimpleStringBuilder& sb) { sb << fields; },
+                       fields.size());
+}
+
+ColumnPrinter ColumnPrinter::Lambda(
+    const char* headers,
+    std::function<void(rtc::SimpleStringBuilder&)> printer,
+    size_t max_length) {
+  return ColumnPrinter(headers, printer, max_length);
+}
+
+StatesPrinter::StatesPrinter(std::string filename,
+                             std::vector<ColumnPrinter> printers)
+    : StatesPrinter(printers) {
+  if (!filename.empty()) {
+    output_file_ = fopen(filename.c_str(), "w");
+    RTC_CHECK(output_file_);
+    output_ = output_file_;
+  }
+}
+
+StatesPrinter::StatesPrinter(std::vector<ColumnPrinter> printers)
+    : printers_(printers) {
+  output_ = stdout;
+  RTC_CHECK(!printers_.empty());
+  for (auto& printer : printers_)
+    buffer_size_ += printer.max_length_ + 1;
+  buffer_.resize(buffer_size_);
+}
+
+StatesPrinter::~StatesPrinter() {
+  if (output_file_)
+    fclose(output_file_);
+}
+
+void StatesPrinter::PrintHeaders() {
+  if (!output_file_)
+    return;
+  fprintf(output_, "%s", printers_[0].headers_);
+  for (size_t i = 1; i < printers_.size(); ++i) {
+    fprintf(output_, " %s", printers_[i].headers_);
+  }
+  fputs("\n", output_);
+}
+
+void StatesPrinter::PrintRow() {
+  // Note that this is run for null output to preserve side effects, this allows
+  // setting break points etc.
+  rtc::SimpleStringBuilder sb(buffer_);
+  printers_[0].printer_(sb);
+  for (size_t i = 1; i < printers_.size(); ++i) {
+    sb << ' ';
+    printers_[i].printer_(sb);
+  }
+  sb << "\n\0";
+  if (output_file_)
+    fputs(&buffer_.front(), output_);
+}
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/column_printer.h b/test/scenario/column_printer.h
new file mode 100644
index 0000000..b1299a0
--- /dev/null
+++ b/test/scenario/column_printer.h
@@ -0,0 +1,63 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_COLUMN_PRINTER_H_
+#define TEST_SCENARIO_COLUMN_PRINTER_H_
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "rtc_base/constructormagic.h"
+#include "rtc_base/strings/string_builder.h"
+
+namespace webrtc {
+namespace test {
+class ColumnPrinter {
+ public:
+  ColumnPrinter(const ColumnPrinter&);
+  ~ColumnPrinter();
+  static ColumnPrinter Fixed(const char* headers, std::string fields);
+  static ColumnPrinter Lambda(
+      const char* headers,
+      std::function<void(rtc::SimpleStringBuilder&)> printer,
+      size_t max_length = 256);
+
+ protected:
+  friend class StatesPrinter;
+  const char* headers_;
+  std::function<void(rtc::SimpleStringBuilder&)> printer_;
+  size_t max_length_;
+
+ private:
+  ColumnPrinter(const char* headers,
+                std::function<void(rtc::SimpleStringBuilder&)> printer,
+                size_t max_length);
+};
+
+class StatesPrinter {
+ public:
+  StatesPrinter(std::string filename, std::vector<ColumnPrinter> printers);
+  explicit StatesPrinter(std::vector<ColumnPrinter> printers);
+  RTC_DISALLOW_COPY_AND_ASSIGN(StatesPrinter);
+  ~StatesPrinter();
+  void PrintHeaders();
+  void PrintRow();
+
+ private:
+  const std::vector<ColumnPrinter> printers_;
+  size_t buffer_size_ = 0;
+  std::vector<char> buffer_;
+  FILE* output_file_ = nullptr;
+  FILE* output_ = nullptr;
+};
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_SCENARIO_COLUMN_PRINTER_H_
diff --git a/test/scenario/hardware_codecs.cc b/test/scenario/hardware_codecs.cc
new file mode 100644
index 0000000..16044de
--- /dev/null
+++ b/test/scenario/hardware_codecs.cc
@@ -0,0 +1,49 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/hardware_codecs.h"
+#include "rtc_base/checks.h"
+
+#ifdef WEBRTC_ANDROID
+#include "modules/video_coding/codecs/test/android_codec_factory_helper.h"
+#endif
+#ifdef WEBRTC_MAC
+#include "modules/video_coding/codecs/test/objc_codec_factory_helper.h"
+#endif
+
+namespace webrtc {
+namespace test {
+std::unique_ptr<VideoEncoderFactory> CreateHardwareEncoderFactory() {
+#ifdef WEBRTC_ANDROID
+  InitializeAndroidObjects();
+  return CreateAndroidEncoderFactory();
+#else
+#ifdef WEBRTC_MAC
+  return CreateObjCEncoderFactory();
+#else
+  RTC_NOTREACHED() << "Hardware encoder not implemented on this platform.";
+  return nullptr;
+#endif
+#endif
+}
+std::unique_ptr<VideoDecoderFactory> CreateHardwareDecoderFactory() {
+#ifdef WEBRTC_ANDROID
+  InitializeAndroidObjects();
+  return CreateAndroidDecoderFactory();
+#else
+#ifdef WEBRTC_MAC
+  return CreateObjCDecoderFactory();
+#else
+  RTC_NOTREACHED() << "Hardware decoder not implemented on this platform.";
+  return nullptr;
+#endif
+#endif
+}
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/hardware_codecs.h b/test/scenario/hardware_codecs.h
new file mode 100644
index 0000000..ae14a27
--- /dev/null
+++ b/test/scenario/hardware_codecs.h
@@ -0,0 +1,24 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_HARDWARE_CODECS_H_
+#define TEST_SCENARIO_HARDWARE_CODECS_H_
+
+#include <memory>
+
+#include "api/video_codecs/video_decoder_factory.h"
+#include "api/video_codecs/video_encoder_factory.h"
+
+namespace webrtc {
+namespace test {
+std::unique_ptr<VideoEncoderFactory> CreateHardwareEncoderFactory();
+std::unique_ptr<VideoDecoderFactory> CreateHardwareDecoderFactory();
+}  // namespace test
+}  // namespace webrtc
+#endif  // TEST_SCENARIO_HARDWARE_CODECS_H_
diff --git a/test/scenario/network_node.cc b/test/scenario/network_node.cc
new file mode 100644
index 0000000..eb215aa
--- /dev/null
+++ b/test/scenario/network_node.cc
@@ -0,0 +1,257 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/network_node.h"
+
+#include <algorithm>
+#include <vector>
+
+namespace webrtc {
+namespace test {
+namespace {
+SimulatedNetwork::Config CreateSimulationConfig(NetworkNodeConfig config) {
+  SimulatedNetwork::Config sim_config;
+  sim_config.link_capacity_kbps = config.simulation.bandwidth.kbps_or(0);
+  sim_config.loss_percent = config.simulation.loss_rate * 100;
+  sim_config.queue_delay_ms = config.simulation.delay.ms();
+  sim_config.delay_standard_deviation_ms = config.simulation.delay_std_dev.ms();
+  return sim_config;
+}
+}  // namespace
+
+bool NullReceiver::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                    uint64_t receiver,
+                                    Timestamp at_time) {
+  return true;
+}
+
+ActionReceiver::ActionReceiver(std::function<void()> action)
+    : action_(action) {}
+
+bool ActionReceiver::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                      uint64_t receiver,
+                                      Timestamp at_time) {
+  action_();
+  return true;
+}
+
+NetworkNode::~NetworkNode() = default;
+
+NetworkNode::NetworkNode(NetworkNodeConfig config,
+                         std::unique_ptr<NetworkSimulationInterface> simulation)
+    : packet_overhead_(config.packet_overhead.bytes()),
+      simulation_(std::move(simulation)) {}
+
+void NetworkNode::SetRoute(uint64_t receiver, NetworkReceiverInterface* node) {
+  rtc::CritScope crit(&crit_sect_);
+  routing_[receiver] = node;
+}
+
+void NetworkNode::ClearRoute(uint64_t receiver_id) {
+  rtc::CritScope crit(&crit_sect_);
+  auto it = routing_.find(receiver_id);
+  routing_.erase(it);
+}
+
+bool NetworkNode::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                   uint64_t receiver,
+                                   Timestamp at_time) {
+  rtc::CritScope crit(&crit_sect_);
+  if (routing_.find(receiver) == routing_.end())
+    return false;
+  uint64_t packet_id = next_packet_id_++;
+  bool sent = simulation_->EnqueuePacket(PacketInFlightInfo(
+      packet.size() + packet_overhead_, at_time.us(), packet_id));
+  if (sent) {
+    packets_.emplace_back(StoredPacket{packet, receiver, packet_id, false});
+  }
+  return sent;
+}
+
+void NetworkNode::Process(Timestamp at_time) {
+  std::vector<PacketDeliveryInfo> delivery_infos;
+  {
+    rtc::CritScope crit(&crit_sect_);
+    absl::optional<int64_t> delivery_us = simulation_->NextDeliveryTimeUs();
+    if (delivery_us && *delivery_us > at_time.us())
+      return;
+
+    delivery_infos = simulation_->DequeueDeliverablePackets(at_time.us());
+  }
+  for (PacketDeliveryInfo& delivery_info : delivery_infos) {
+    StoredPacket* packet = nullptr;
+    NetworkReceiverInterface* receiver = nullptr;
+    {
+      rtc::CritScope crit(&crit_sect_);
+      for (StoredPacket& stored_packet : packets_) {
+        if (stored_packet.id == delivery_info.packet_id) {
+          packet = &stored_packet;
+          break;
+        }
+      }
+      RTC_CHECK(packet);
+      RTC_DCHECK(!packet->removed);
+      receiver = routing_[packet->receiver_id];
+      packet->removed = true;
+    }
+    // We don't want to keep the lock here. Otherwise we would get a deadlock if
+    // the receiver tries to push a new packet.
+    receiver->TryDeliverPacket(packet->packet_data, packet->receiver_id,
+                               at_time);
+    {
+      rtc::CritScope crit(&crit_sect_);
+      while (!packets_.empty() && packets_.front().removed) {
+        packets_.pop_front();
+      }
+    }
+  }
+}
+
+void NetworkNode::Route(int64_t receiver_id,
+                        std::vector<NetworkNode*> nodes,
+                        NetworkReceiverInterface* receiver) {
+  RTC_CHECK(!nodes.empty());
+  for (size_t i = 0; i + 1 < nodes.size(); ++i)
+    nodes[i]->SetRoute(receiver_id, nodes[i + 1]);
+  nodes.back()->SetRoute(receiver_id, receiver);
+}
+
+void NetworkNode::ClearRoute(int64_t receiver_id,
+                             std::vector<NetworkNode*> nodes) {
+  for (NetworkNode* node : nodes)
+    node->ClearRoute(receiver_id);
+}
+
+std::unique_ptr<SimulationNode> SimulationNode::Create(
+    NetworkNodeConfig config) {
+  RTC_DCHECK(config.mode == NetworkNodeConfig::TrafficMode::kSimulation);
+  SimulatedNetwork::Config sim_config = CreateSimulationConfig(config);
+  auto network = absl::make_unique<SimulatedNetwork>(sim_config);
+  SimulatedNetwork* simulation_ptr = network.get();
+  return std::unique_ptr<SimulationNode>(
+      new SimulationNode(config, std::move(network), simulation_ptr));
+}
+
+void SimulationNode::UpdateConfig(
+    std::function<void(NetworkNodeConfig*)> modifier) {
+  modifier(&config_);
+  SimulatedNetwork::Config sim_config = CreateSimulationConfig(config_);
+  simulated_network_->SetConfig(sim_config);
+}
+
+void SimulationNode::PauseTransmissionUntil(Timestamp until) {
+  simulated_network_->PauseTransmissionUntil(until.us());
+}
+
+ColumnPrinter SimulationNode::ConfigPrinter() const {
+  return ColumnPrinter::Lambda("propagation_delay capacity loss_rate",
+                               [this](rtc::SimpleStringBuilder& sb) {
+                                 sb.AppendFormat(
+                                     "%.3lf %.0lf %.2lf",
+                                     config_.simulation.delay.seconds<double>(),
+                                     config_.simulation.bandwidth.bps() / 8.0,
+                                     config_.simulation.loss_rate);
+                               });
+}
+
+SimulationNode::SimulationNode(
+    NetworkNodeConfig config,
+    std::unique_ptr<NetworkSimulationInterface> behavior,
+    SimulatedNetwork* simulation)
+    : NetworkNode(config, std::move(behavior)),
+      simulated_network_(simulation),
+      config_(config) {}
+
+NetworkNodeTransport::NetworkNodeTransport(CallClient* sender,
+                                           NetworkNode* send_net,
+                                           uint64_t receiver,
+                                           DataSize packet_overhead)
+    : sender_(sender),
+      send_net_(send_net),
+      receiver_id_(receiver),
+      packet_overhead_(packet_overhead) {}
+
+NetworkNodeTransport::~NetworkNodeTransport() = default;
+
+bool NetworkNodeTransport::SendRtp(const uint8_t* packet,
+                                   size_t length,
+                                   const PacketOptions& options) {
+  sender_->call_->OnSentPacket(rtc::SentPacket(
+      options.packet_id, sender_->clock_->TimeInMilliseconds()));
+  Timestamp send_time = Timestamp::ms(sender_->clock_->TimeInMilliseconds());
+  rtc::CopyOnWriteBuffer buffer(packet, length,
+                                length + packet_overhead_.bytes());
+  buffer.SetSize(length + packet_overhead_.bytes());
+  return send_net_->TryDeliverPacket(buffer, receiver_id_, send_time);
+}
+
+bool NetworkNodeTransport::SendRtcp(const uint8_t* packet, size_t length) {
+  rtc::CopyOnWriteBuffer buffer(packet, length);
+  Timestamp send_time = Timestamp::ms(sender_->clock_->TimeInMilliseconds());
+  buffer.SetSize(length + packet_overhead_.bytes());
+  return send_net_->TryDeliverPacket(buffer, receiver_id_, send_time);
+}
+
+uint64_t NetworkNodeTransport::ReceiverId() const {
+  return receiver_id_;
+}
+
+CrossTrafficSource::CrossTrafficSource(NetworkReceiverInterface* target,
+                                       uint64_t receiver_id,
+                                       CrossTrafficConfig config)
+    : target_(target),
+      receiver_id_(receiver_id),
+      config_(config),
+      random_(config.random_seed) {}
+
+CrossTrafficSource::~CrossTrafficSource() = default;
+
+DataRate CrossTrafficSource::TrafficRate() const {
+  return config_.peak_rate * intensity_;
+}
+
+void CrossTrafficSource::Process(Timestamp at_time, TimeDelta delta) {
+  time_since_update_ += delta;
+  if (config_.mode == CrossTrafficConfig::Mode::kRandomWalk) {
+    if (time_since_update_ >= config_.random_walk.update_interval) {
+      intensity_ += random_.Gaussian(config_.random_walk.bias,
+                                     config_.random_walk.variance) *
+                    time_since_update_.seconds<double>();
+      intensity_ = rtc::SafeClamp(intensity_, 0.0, 1.0);
+      time_since_update_ = TimeDelta::Zero();
+    }
+  } else if (config_.mode == CrossTrafficConfig::Mode::kPulsedPeaks) {
+    if (intensity_ == 0 && time_since_update_ >= config_.pulsed.hold_duration) {
+      intensity_ = 1;
+      time_since_update_ = TimeDelta::Zero();
+    } else if (intensity_ == 1 &&
+               time_since_update_ >= config_.pulsed.send_duration) {
+      intensity_ = 0;
+      time_since_update_ = TimeDelta::Zero();
+    }
+  }
+  pending_size_ += TrafficRate() * delta;
+  if (pending_size_ > config_.min_packet_size) {
+    target_->TryDeliverPacket(rtc::CopyOnWriteBuffer(pending_size_.bytes()),
+                              receiver_id_, at_time);
+    pending_size_ = DataSize::Zero();
+  }
+}
+
+ColumnPrinter CrossTrafficSource::StatsPrinter() {
+  return ColumnPrinter::Lambda("cross_traffic_rate",
+                               [this](rtc::SimpleStringBuilder& sb) {
+                                 sb.AppendFormat("%.0lf",
+                                                 TrafficRate().bps() / 8.0);
+                               },
+                               32);
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/network_node.h b/test/scenario/network_node.h
new file mode 100644
index 0000000..a94df1b
--- /dev/null
+++ b/test/scenario/network_node.h
@@ -0,0 +1,167 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_NETWORK_NODE_H_
+#define TEST_SCENARIO_NETWORK_NODE_H_
+
+#include <deque>
+#include <map>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "api/call/transport.h"
+#include "api/units/timestamp.h"
+#include "call/simulated_network.h"
+#include "rtc_base/constructormagic.h"
+#include "rtc_base/copyonwritebuffer.h"
+#include "test/scenario/call_client.h"
+#include "test/scenario/column_printer.h"
+#include "test/scenario/scenario_config.h"
+
+namespace webrtc {
+namespace test {
+
+class NetworkReceiverInterface {
+ public:
+  virtual bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                uint64_t receiver,
+                                Timestamp at_time) = 0;
+  virtual ~NetworkReceiverInterface() = default;
+};
+class NullReceiver : public NetworkReceiverInterface {
+ public:
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+};
+class ActionReceiver : public NetworkReceiverInterface {
+ public:
+  explicit ActionReceiver(std::function<void()> action);
+  virtual ~ActionReceiver() = default;
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+
+ private:
+  std::function<void()> action_;
+};
+
+// NetworkNode represents one link in a simulated network. It is created by a
+// scenario and can be used when setting up audio and video stream sessions.
+class NetworkNode : public NetworkReceiverInterface {
+ public:
+  ~NetworkNode() override;
+  RTC_DISALLOW_COPY_AND_ASSIGN(NetworkNode);
+
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+  // Creates a route  for the given receiver_id over all the given nodes to the
+  // given receiver.
+  static void Route(int64_t receiver_id,
+                    std::vector<NetworkNode*> nodes,
+                    NetworkReceiverInterface* receiver);
+
+ protected:
+  friend class Scenario;
+  friend class AudioStreamPair;
+  friend class VideoStreamPair;
+
+  NetworkNode(NetworkNodeConfig config,
+              std::unique_ptr<NetworkSimulationInterface> simulation);
+  static void ClearRoute(int64_t receiver_id, std::vector<NetworkNode*> nodes);
+  void Process(Timestamp at_time);
+
+ private:
+  struct StoredPacket {
+    rtc::CopyOnWriteBuffer packet_data;
+    uint64_t receiver_id;
+    uint64_t id;
+    bool removed;
+  };
+  void SetRoute(uint64_t receiver, NetworkReceiverInterface* node);
+  void ClearRoute(uint64_t receiver_id);
+  rtc::CriticalSection crit_sect_;
+  size_t packet_overhead_ RTC_GUARDED_BY(crit_sect_);
+  const std::unique_ptr<NetworkSimulationInterface> simulation_
+      RTC_GUARDED_BY(crit_sect_);
+  std::map<uint64_t, NetworkReceiverInterface*> routing_
+      RTC_GUARDED_BY(crit_sect_);
+  std::deque<StoredPacket> packets_ RTC_GUARDED_BY(crit_sect_);
+
+  uint64_t next_packet_id_ RTC_GUARDED_BY(crit_sect_) = 1;
+};
+// SimulationNode is a NetworkNode that expose an interface for changing run
+// time behavior of the underlying simulation.
+class SimulationNode : public NetworkNode {
+ public:
+  void UpdateConfig(std::function<void(NetworkNodeConfig*)> modifier);
+  void PauseTransmissionUntil(Timestamp until);
+  ColumnPrinter ConfigPrinter() const;
+
+ private:
+  friend class Scenario;
+
+  SimulationNode(NetworkNodeConfig config,
+                 std::unique_ptr<NetworkSimulationInterface> behavior,
+                 SimulatedNetwork* simulation);
+  static std::unique_ptr<SimulationNode> Create(NetworkNodeConfig config);
+  SimulatedNetwork* const simulated_network_;
+  NetworkNodeConfig config_;
+};
+
+class NetworkNodeTransport : public Transport {
+ public:
+  NetworkNodeTransport(CallClient* sender,
+                       NetworkNode* send_net,
+                       uint64_t receiver,
+                       DataSize packet_overhead);
+  ~NetworkNodeTransport() override;
+
+  bool SendRtp(const uint8_t* packet,
+               size_t length,
+               const PacketOptions& options) override;
+  bool SendRtcp(const uint8_t* packet, size_t length) override;
+  uint64_t ReceiverId() const;
+
+ private:
+  CallClient* const sender_;
+  NetworkNode* const send_net_;
+  const uint64_t receiver_id_;
+  const DataSize packet_overhead_;
+};
+
+// CrossTrafficSource is created by a Scenario and generates cross traffic. It
+// provides methods to access and print internal state.
+class CrossTrafficSource {
+ public:
+  DataRate TrafficRate() const;
+  ColumnPrinter StatsPrinter();
+  ~CrossTrafficSource();
+
+ private:
+  friend class Scenario;
+  CrossTrafficSource(NetworkReceiverInterface* target,
+                     uint64_t receiver_id,
+                     CrossTrafficConfig config);
+  void Process(Timestamp at_time, TimeDelta delta);
+
+  NetworkReceiverInterface* const target_;
+  const uint64_t receiver_id_;
+  CrossTrafficConfig config_;
+  webrtc::Random random_;
+
+  TimeDelta time_since_update_ = TimeDelta::Zero();
+  double intensity_ = 0;
+  DataSize pending_size_ = DataSize::Zero();
+};
+}  // namespace test
+}  // namespace webrtc
+#endif  // TEST_SCENARIO_NETWORK_NODE_H_
diff --git a/test/scenario/scenario.cc b/test/scenario/scenario.cc
new file mode 100644
index 0000000..ab6ee6b
--- /dev/null
+++ b/test/scenario/scenario.cc
@@ -0,0 +1,341 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/scenario.h"
+
+#include <algorithm>
+
+#include "api/audio_codecs/builtin_audio_decoder_factory.h"
+#include "api/audio_codecs/builtin_audio_encoder_factory.h"
+#include "rtc_base/flags.h"
+#include "test/testsupport/fileutils.h"
+
+DEFINE_bool(scenario_logs, false, "Save logs from scenario framework.");
+
+namespace webrtc {
+namespace test {
+namespace {
+int64_t kMicrosPerSec = 1000000;
+}
+
+RepeatedActivity::RepeatedActivity(TimeDelta interval,
+                                   std::function<void(TimeDelta)> function)
+    : interval_(interval), function_(function) {}
+
+void RepeatedActivity::Stop() {
+  interval_ = TimeDelta::PlusInfinity();
+}
+
+void RepeatedActivity::Poll(Timestamp time) {
+  RTC_DCHECK(last_update_.IsFinite());
+  if (time >= last_update_ + interval_) {
+    function_(time - last_update_);
+    last_update_ = time;
+  }
+}
+
+void RepeatedActivity::SetStartTime(Timestamp time) {
+  last_update_ = time;
+}
+
+Timestamp RepeatedActivity::NextTime() {
+  RTC_DCHECK(last_update_.IsFinite());
+  return last_update_ + interval_;
+}
+
+Scenario::Scenario() : Scenario("", true) {}
+
+Scenario::Scenario(std::string file_name) : Scenario(file_name, true) {}
+
+Scenario::Scenario(std::string file_name, bool real_time)
+    : real_time_mode_(real_time),
+      sim_clock_(100000 * kMicrosPerSec),
+      clock_(real_time ? Clock::GetRealTimeClock() : &sim_clock_),
+      audio_decoder_factory_(CreateBuiltinAudioDecoderFactory()),
+      audio_encoder_factory_(CreateBuiltinAudioEncoderFactory()) {
+  if (FLAG_scenario_logs && !file_name.empty()) {
+    CreateDir(OutputPath() + "output_data");
+    for (size_t i = 0; i < file_name.size(); ++i) {
+      if (file_name[i] == '/')
+        CreateDir(OutputPath() + "output_data/" + file_name.substr(0, i));
+    }
+    base_filename_ = OutputPath() + "output_data/" + file_name;
+    RTC_LOG(LS_INFO) << "Saving scenario logs to: " << base_filename_;
+  }
+  if (!real_time_mode_) {
+    rtc::SetClockForTesting(&event_log_fake_clock_);
+    event_log_fake_clock_.SetTimeNanos(sim_clock_.TimeInMicroseconds() * 1000);
+  }
+}
+
+Scenario::~Scenario() {
+  if (!real_time_mode_)
+    rtc::SetClockForTesting(nullptr);
+}
+
+ColumnPrinter Scenario::TimePrinter() {
+  return ColumnPrinter::Lambda("time",
+                               [this](rtc::SimpleStringBuilder& sb) {
+                                 sb.AppendFormat("%.3lf",
+                                                 Now().seconds<double>());
+                               },
+                               32);
+}
+
+StatesPrinter* Scenario::CreatePrinter(std::string name,
+                                       TimeDelta interval,
+                                       std::vector<ColumnPrinter> printers) {
+  std::vector<ColumnPrinter> all_printers{TimePrinter()};
+  for (auto& printer : printers)
+    all_printers.push_back(printer);
+  StatesPrinter* printer =
+      new StatesPrinter(GetFullPathOrEmpty(name), all_printers);
+  printers_.emplace_back(printer);
+  printer->PrintHeaders();
+  if (interval.IsFinite())
+    Every(interval, [printer] { printer->PrintRow(); });
+  return printer;
+}
+
+CallClient* Scenario::CreateClient(std::string name, CallClientConfig config) {
+  CallClient* client = new CallClient(clock_, GetFullPathOrEmpty(name), config);
+  if (config.transport.state_log_interval.IsFinite()) {
+    Every(config.transport.state_log_interval, [this, client]() {
+      client->network_controller_factory_.LogCongestionControllerStats(Now());
+    });
+  }
+  clients_.emplace_back(client);
+  return client;
+}
+
+CallClient* Scenario::CreateClient(
+    std::string name,
+    std::function<void(CallClientConfig*)> config_modifier) {
+  CallClientConfig config;
+  config_modifier(&config);
+  return CreateClient(name, config);
+}
+
+SimulationNode* Scenario::CreateSimulationNode(
+    std::function<void(NetworkNodeConfig*)> config_modifier) {
+  NetworkNodeConfig config;
+  config_modifier(&config);
+  return CreateSimulationNode(config);
+}
+
+SimulationNode* Scenario::CreateSimulationNode(NetworkNodeConfig config) {
+  RTC_DCHECK(config.mode == NetworkNodeConfig::TrafficMode::kSimulation);
+  auto network_node = SimulationNode::Create(config);
+  SimulationNode* sim_node = network_node.get();
+  network_nodes_.emplace_back(std::move(network_node));
+  Every(config.update_frequency,
+        [this, sim_node] { sim_node->Process(Now()); });
+  return sim_node;
+}
+
+NetworkNode* Scenario::CreateNetworkNode(
+    NetworkNodeConfig config,
+    std::unique_ptr<NetworkSimulationInterface> simulation) {
+  RTC_DCHECK(config.mode == NetworkNodeConfig::TrafficMode::kCustom);
+  network_nodes_.emplace_back(new NetworkNode(config, std::move(simulation)));
+  NetworkNode* network_node = network_nodes_.back().get();
+  Every(config.update_frequency,
+        [this, network_node] { network_node->Process(Now()); });
+  return network_node;
+}
+
+void Scenario::TriggerPacketBurst(std::vector<NetworkNode*> over_nodes,
+                                  size_t num_packets,
+                                  size_t packet_size) {
+  int64_t receiver_id = next_receiver_id_++;
+  NetworkNode::Route(receiver_id, over_nodes, &null_receiver_);
+  for (size_t i = 0; i < num_packets; ++i)
+    over_nodes[0]->TryDeliverPacket(rtc::CopyOnWriteBuffer(packet_size),
+                                    receiver_id, Now());
+}
+
+void Scenario::NetworkDelayedAction(std::vector<NetworkNode*> over_nodes,
+                                    size_t packet_size,
+                                    std::function<void()> action) {
+  int64_t receiver_id = next_receiver_id_++;
+  action_receivers_.emplace_back(new ActionReceiver(action));
+  NetworkNode::Route(receiver_id, over_nodes, action_receivers_.back().get());
+  over_nodes[0]->TryDeliverPacket(rtc::CopyOnWriteBuffer(packet_size),
+                                  receiver_id, Now());
+}
+
+CrossTrafficSource* Scenario::CreateCrossTraffic(
+    std::vector<NetworkNode*> over_nodes,
+    std::function<void(CrossTrafficConfig*)> config_modifier) {
+  CrossTrafficConfig cross_config;
+  config_modifier(&cross_config);
+  return CreateCrossTraffic(over_nodes, cross_config);
+}
+
+CrossTrafficSource* Scenario::CreateCrossTraffic(
+    std::vector<NetworkNode*> over_nodes,
+    CrossTrafficConfig config) {
+  int64_t receiver_id = next_receiver_id_++;
+  cross_traffic_sources_.emplace_back(
+      new CrossTrafficSource(over_nodes.front(), receiver_id, config));
+  CrossTrafficSource* node = cross_traffic_sources_.back().get();
+  NetworkNode::Route(receiver_id, over_nodes, &null_receiver_);
+  Every(config.min_packet_interval,
+        [this, node](TimeDelta delta) { node->Process(Now(), delta); });
+  return node;
+}
+
+VideoStreamPair* Scenario::CreateVideoStream(
+    CallClient* sender,
+    std::vector<NetworkNode*> send_link,
+    CallClient* receiver,
+    std::vector<NetworkNode*> return_link,
+    std::function<void(VideoStreamConfig*)> config_modifier) {
+  VideoStreamConfig config;
+  config_modifier(&config);
+  return CreateVideoStream(sender, send_link, receiver, return_link, config);
+}
+
+VideoStreamPair* Scenario::CreateVideoStream(
+    CallClient* sender,
+    std::vector<NetworkNode*> send_link,
+    CallClient* receiver,
+    std::vector<NetworkNode*> return_link,
+    VideoStreamConfig config) {
+  uint64_t send_receiver_id = next_receiver_id_++;
+  uint64_t return_receiver_id = next_receiver_id_++;
+
+  video_streams_.emplace_back(
+      new VideoStreamPair(sender, send_link, send_receiver_id, receiver,
+                          return_link, return_receiver_id, config));
+  return video_streams_.back().get();
+}
+
+AudioStreamPair* Scenario::CreateAudioStream(
+    CallClient* sender,
+    std::vector<NetworkNode*> send_link,
+    CallClient* receiver,
+    std::vector<NetworkNode*> return_link,
+    std::function<void(AudioStreamConfig*)> config_modifier) {
+  AudioStreamConfig config;
+  config_modifier(&config);
+  return CreateAudioStream(sender, send_link, receiver, return_link, config);
+}
+
+AudioStreamPair* Scenario::CreateAudioStream(
+    CallClient* sender,
+    std::vector<NetworkNode*> send_link,
+    CallClient* receiver,
+    std::vector<NetworkNode*> return_link,
+    AudioStreamConfig config) {
+  uint64_t send_receiver_id = next_receiver_id_++;
+  uint64_t return_receiver_id = next_receiver_id_++;
+
+  audio_streams_.emplace_back(new AudioStreamPair(
+      sender, send_link, send_receiver_id, audio_encoder_factory_, receiver,
+      return_link, return_receiver_id, audio_decoder_factory_, config));
+  return audio_streams_.back().get();
+}
+
+RepeatedActivity* Scenario::Every(TimeDelta interval,
+                                  std::function<void(TimeDelta)> function) {
+  repeated_activities_.emplace_back(new RepeatedActivity(interval, function));
+  return repeated_activities_.back().get();
+}
+
+RepeatedActivity* Scenario::Every(TimeDelta interval,
+                                  std::function<void()> function) {
+  auto function_with_argument = [function](TimeDelta) { function(); };
+  repeated_activities_.emplace_back(
+      new RepeatedActivity(interval, function_with_argument));
+  return repeated_activities_.back().get();
+}
+
+void Scenario::At(TimeDelta offset, std::function<void()> function) {
+  pending_activities_.emplace_back(new PendingActivity{offset, function});
+}
+
+void Scenario::RunFor(TimeDelta duration) {
+  RunUntil(duration, TimeDelta::PlusInfinity(), []() { return false; });
+}
+
+void Scenario::RunUntil(TimeDelta max_duration,
+                        TimeDelta poll_interval,
+                        std::function<bool()> exit_function) {
+  start_time_ = Timestamp::us(clock_->TimeInMicroseconds());
+  for (auto& activity : repeated_activities_) {
+    activity->SetStartTime(start_time_);
+  }
+
+  for (auto& stream_pair : video_streams_)
+    stream_pair->receive()->receive_stream_->Start();
+  for (auto& stream_pair : audio_streams_)
+    stream_pair->receive()->receive_stream_->Start();
+  for (auto& stream_pair : video_streams_) {
+    if (stream_pair->config_.autostart) {
+      stream_pair->send()->Start();
+    }
+  }
+  for (auto& stream_pair : audio_streams_) {
+    if (stream_pair->config_.autostart) {
+      stream_pair->send()->Start();
+    }
+  }
+  for (auto& call : clients_) {
+    call->call_->SignalChannelNetworkState(MediaType::AUDIO, kNetworkUp);
+    call->call_->SignalChannelNetworkState(MediaType::VIDEO, kNetworkUp);
+  }
+
+  rtc::Event done_(false, false);
+  while (!exit_function() && Duration() < max_duration) {
+    Timestamp current_time = Now();
+    TimeDelta duration = current_time - start_time_;
+    Timestamp next_time = current_time + poll_interval;
+    for (auto& activity : repeated_activities_) {
+      activity->Poll(current_time);
+      next_time = std::min(next_time, activity->NextTime());
+    }
+    for (auto activity = pending_activities_.begin();
+         activity < pending_activities_.end(); activity++) {
+      if (duration > (*activity)->after_duration) {
+        (*activity)->function();
+        pending_activities_.erase(activity);
+      }
+    }
+    TimeDelta wait_time = next_time - current_time;
+    if (real_time_mode_) {
+      done_.Wait(wait_time.ms<int>());
+    } else {
+      sim_clock_.AdvanceTimeMicroseconds(wait_time.us());
+      event_log_fake_clock_.SetTimeNanos(sim_clock_.TimeInMicroseconds() *
+                                         1000);
+    }
+  }
+  for (auto& stream_pair : video_streams_) {
+    stream_pair->send()->video_capturer_->Stop();
+    stream_pair->send()->send_stream_->Stop();
+  }
+  for (auto& stream_pair : audio_streams_)
+    stream_pair->send()->send_stream_->Stop();
+  for (auto& stream_pair : video_streams_)
+    stream_pair->receive()->receive_stream_->Stop();
+  for (auto& stream_pair : audio_streams_)
+    stream_pair->receive()->receive_stream_->Stop();
+}
+
+Timestamp Scenario::Now() {
+  return Timestamp::us(clock_->TimeInMicroseconds());
+}
+
+TimeDelta Scenario::Duration() {
+  return Now() - start_time_;
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/scenario.h b/test/scenario/scenario.h
new file mode 100644
index 0000000..2cdad35
--- /dev/null
+++ b/test/scenario/scenario.h
@@ -0,0 +1,181 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_SCENARIO_H_
+#define TEST_SCENARIO_SCENARIO_H_
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "rtc_base/constructormagic.h"
+#include "rtc_base/fakeclock.h"
+#include "test/scenario/audio_stream.h"
+#include "test/scenario/call_client.h"
+#include "test/scenario/column_printer.h"
+#include "test/scenario/network_node.h"
+#include "test/scenario/scenario_config.h"
+#include "test/scenario/video_stream.h"
+
+namespace webrtc {
+namespace test {
+// RepeatedActivity is created by the Scenario class and can be used to stop a
+// running activity at runtime.
+class RepeatedActivity {
+ public:
+  void Stop();
+
+ private:
+  friend class Scenario;
+  RepeatedActivity(TimeDelta interval, std::function<void(TimeDelta)> function);
+
+  void Poll(Timestamp time);
+  void SetStartTime(Timestamp time);
+  Timestamp NextTime();
+
+  TimeDelta interval_;
+  std::function<void(TimeDelta)> function_;
+  Timestamp last_update_ = Timestamp::MinusInfinity();
+};
+
+struct PendingActivity {
+  TimeDelta after_duration;
+  std::function<void()> function;
+};
+
+// Scenario is a class owning everything for a test scenario. It creates and
+// holds network nodes, call clients and media streams. It also provides methods
+// for changing behavior at runtime. Since it always keeps ownership of the
+// created components, it generally returns non-owning pointers. It maintains
+// the life of its objects until it is destroyed.
+// For methods accepting configuration structs, a modifier function interface is
+// generally provided. This allows simple partial overriding of the default
+// configuration.
+class Scenario {
+ public:
+  Scenario();
+  explicit Scenario(std::string file_name);
+  Scenario(std::string file_name, bool real_time);
+  RTC_DISALLOW_COPY_AND_ASSIGN(Scenario);
+  ~Scenario();
+
+  SimulationNode* CreateSimulationNode(NetworkNodeConfig config);
+  SimulationNode* CreateSimulationNode(
+      std::function<void(NetworkNodeConfig*)> config_modifier);
+  NetworkNode* CreateNetworkNode(
+      NetworkNodeConfig config,
+      std::unique_ptr<NetworkSimulationInterface> simulation);
+
+  CallClient* CreateClient(std::string name, CallClientConfig config);
+  CallClient* CreateClient(
+      std::string name,
+      std::function<void(CallClientConfig*)> config_modifier);
+
+  VideoStreamPair* CreateVideoStream(
+      CallClient* sender,
+      std::vector<NetworkNode*> send_link,
+      CallClient* receiver,
+      std::vector<NetworkNode*> return_link,
+      std::function<void(VideoStreamConfig*)> config_modifier);
+  VideoStreamPair* CreateVideoStream(CallClient* sender,
+                                     std::vector<NetworkNode*> send_link,
+                                     CallClient* receiver,
+                                     std::vector<NetworkNode*> return_link,
+                                     VideoStreamConfig config);
+
+  AudioStreamPair* CreateAudioStream(
+      CallClient* sender,
+      std::vector<NetworkNode*> send_link,
+      CallClient* receiver,
+      std::vector<NetworkNode*> return_link,
+      std::function<void(AudioStreamConfig*)> config_modifier);
+  AudioStreamPair* CreateAudioStream(CallClient* sender,
+                                     std::vector<NetworkNode*> send_link,
+                                     CallClient* receiver,
+                                     std::vector<NetworkNode*> return_link,
+                                     AudioStreamConfig config);
+
+  CrossTrafficSource* CreateCrossTraffic(
+      std::vector<NetworkNode*> over_nodes,
+      std::function<void(CrossTrafficConfig*)> config_modifier);
+  CrossTrafficSource* CreateCrossTraffic(std::vector<NetworkNode*> over_nodes,
+                                         CrossTrafficConfig config);
+
+  // Runs the provided function with a fixed interval.
+  RepeatedActivity* Every(TimeDelta interval,
+                          std::function<void(TimeDelta)> function);
+  RepeatedActivity* Every(TimeDelta interval, std::function<void()> function);
+
+  // Runs the provided function after given duration has passed in a session.
+  void At(TimeDelta offset, std::function<void()> function);
+
+  // Sends a packet over the nodes and runs |action| when it has been delivered.
+  void NetworkDelayedAction(std::vector<NetworkNode*> over_nodes,
+                            size_t packet_size,
+                            std::function<void()> action);
+
+  // Runs the scenario for the given time or until the exit function returns
+  // true.
+  void RunFor(TimeDelta duration);
+  void RunUntil(TimeDelta max_duration,
+                TimeDelta probe_interval,
+                std::function<bool()> exit_function);
+
+  // Triggers sending of dummy packets over the given nodes.
+  void TriggerPacketBurst(std::vector<NetworkNode*> over_nodes,
+                          size_t num_packets,
+                          size_t packet_size);
+
+  ColumnPrinter TimePrinter();
+  StatesPrinter* CreatePrinter(std::string name,
+                               TimeDelta interval,
+                               std::vector<ColumnPrinter> printers);
+
+  // Returns the current time.
+  Timestamp Now();
+  // Return the duration of the current session so far.
+  TimeDelta Duration();
+
+  std::string GetFullPathOrEmpty(std::string name) const {
+    if (base_filename_.empty() || name.empty())
+      return std::string();
+    return base_filename_ + "." + name;
+  }
+
+ private:
+  NullReceiver null_receiver_;
+  std::string base_filename_;
+  const bool real_time_mode_;
+  SimulatedClock sim_clock_;
+  Clock* clock_;
+  // Event logs use a global clock instance, this is used to override that
+  // instance when not running in real time.
+  rtc::FakeClock event_log_fake_clock_;
+
+  std::vector<std::unique_ptr<CallClient>> clients_;
+  std::vector<std::unique_ptr<NetworkNode>> network_nodes_;
+  std::vector<std::unique_ptr<CrossTrafficSource>> cross_traffic_sources_;
+  std::vector<std::unique_ptr<VideoStreamPair>> video_streams_;
+  std::vector<std::unique_ptr<AudioStreamPair>> audio_streams_;
+
+  std::vector<std::unique_ptr<RepeatedActivity>> repeated_activities_;
+  std::vector<std::unique_ptr<ActionReceiver>> action_receivers_;
+  std::vector<std::unique_ptr<PendingActivity>> pending_activities_;
+  std::vector<std::unique_ptr<StatesPrinter>> printers_;
+
+  int64_t next_receiver_id_ = 40000;
+  rtc::scoped_refptr<AudioDecoderFactory> audio_decoder_factory_;
+  rtc::scoped_refptr<AudioEncoderFactory> audio_encoder_factory_;
+
+  Timestamp start_time_ = Timestamp::PlusInfinity();
+};
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_SCENARIO_SCENARIO_H_
diff --git a/test/scenario/scenario_config.cc b/test/scenario/scenario_config.cc
new file mode 100644
index 0000000..1caac9f
--- /dev/null
+++ b/test/scenario/scenario_config.cc
@@ -0,0 +1,56 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/scenario_config.h"
+
+namespace webrtc {
+namespace test {
+
+TransportControllerConfig::Rates::Rates() = default;
+TransportControllerConfig::Rates::Rates(
+    const TransportControllerConfig::Rates&) = default;
+TransportControllerConfig::Rates::~Rates() = default;
+
+VideoStreamConfig::Encoder::Encoder() = default;
+VideoStreamConfig::Encoder::Encoder(const VideoStreamConfig::Encoder&) =
+    default;
+VideoStreamConfig::Encoder::~Encoder() = default;
+
+VideoStreamConfig::Stream::Stream() = default;
+VideoStreamConfig::Stream::Stream(const VideoStreamConfig::Stream&) = default;
+VideoStreamConfig::Stream::~Stream() = default;
+
+AudioStreamConfig::AudioStreamConfig() = default;
+AudioStreamConfig::AudioStreamConfig(const AudioStreamConfig&) = default;
+AudioStreamConfig::~AudioStreamConfig() = default;
+
+AudioStreamConfig::Encoder::Encoder() = default;
+AudioStreamConfig::Encoder::Encoder(const AudioStreamConfig::Encoder&) =
+    default;
+AudioStreamConfig::Encoder::~Encoder() = default;
+
+AudioStreamConfig::Stream::Stream() = default;
+AudioStreamConfig::Stream::Stream(const AudioStreamConfig::Stream&) = default;
+AudioStreamConfig::Stream::~Stream() = default;
+
+NetworkNodeConfig::NetworkNodeConfig() = default;
+NetworkNodeConfig::NetworkNodeConfig(const NetworkNodeConfig&) = default;
+NetworkNodeConfig::~NetworkNodeConfig() = default;
+
+NetworkNodeConfig::Simulation::Simulation() = default;
+NetworkNodeConfig::Simulation::Simulation(
+    const NetworkNodeConfig::Simulation&) = default;
+NetworkNodeConfig::Simulation::~Simulation() = default;
+
+CrossTrafficConfig::CrossTrafficConfig() = default;
+CrossTrafficConfig::CrossTrafficConfig(const CrossTrafficConfig&) = default;
+CrossTrafficConfig::~CrossTrafficConfig() = default;
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/scenario_config.h b/test/scenario/scenario_config.h
new file mode 100644
index 0000000..c4e8cc0
--- /dev/null
+++ b/test/scenario/scenario_config.h
@@ -0,0 +1,182 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_SCENARIO_CONFIG_H_
+#define TEST_SCENARIO_SCENARIO_CONFIG_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "absl/types/optional.h"
+#include "api/rtpparameters.h"
+#include "api/units/data_rate.h"
+#include "api/units/time_delta.h"
+#include "api/video_codecs/video_codec.h"
+#include "test/frame_generator.h"
+
+namespace webrtc {
+namespace test {
+struct PacketOverhead {
+  static constexpr size_t kIpv4 = 20;
+  static constexpr size_t kIpv6 = 40;
+  static constexpr size_t kUdp = 8;
+  static constexpr size_t kSrtp = 10;
+  static constexpr size_t kTurn = 4;
+  static constexpr size_t kDefault = kIpv4 + kUdp + kSrtp;
+};
+struct TransportControllerConfig {
+  struct Rates {
+    Rates();
+    Rates(const Rates&);
+    ~Rates();
+    DataRate min_rate = DataRate::kbps(30);
+    DataRate max_rate = DataRate::kbps(3000);
+    DataRate start_rate = DataRate::kbps(300);
+  } rates;
+  enum CongestionController { kBbr, kGoogCc, kGoogCcFeedback } cc = kGoogCc;
+  TimeDelta state_log_interval = TimeDelta::ms(100);
+};
+
+struct CallClientConfig {
+  TransportControllerConfig transport;
+  DataRate priority_target_rate = DataRate::Zero();
+};
+
+struct VideoStreamConfig {
+  bool autostart = true;
+  struct Source {
+    enum Capture {
+      kGenerator,
+      kVideoFile,
+      // Support for still images and explicit frame triggers should be added
+      // here if needed.
+    } capture = Capture::kGenerator;
+    struct Generator {
+      using PixelFormat = FrameGenerator::OutputType;
+      PixelFormat pixel_format = PixelFormat::I420;
+    } generator;
+    struct VideoFile {
+      std::string name;
+    } video_file;
+    int width = 320;
+    int height = 180;
+    int framerate = 30;
+  } source;
+  struct Encoder {
+    Encoder();
+    Encoder(const Encoder&);
+    ~Encoder();
+    enum Implementation { kFake, kSoftware, kHardware } implementation = kFake;
+    struct Fake {
+      DataRate max_rate = DataRate::Infinity();
+    } fake;
+
+    using Codec = VideoCodecType;
+    Codec codec = Codec::kVideoCodecGeneric;
+    bool denoising = true;
+    absl::optional<int> key_frame_interval = 3000;
+
+    absl::optional<DataRate> max_data_rate;
+    size_t num_simulcast_streams = 1;
+    using DegradationPreference = DegradationPreference;
+    DegradationPreference degradation_preference =
+        DegradationPreference::MAINTAIN_FRAMERATE;
+  } encoder;
+  struct Stream {
+    Stream();
+    Stream(const Stream&);
+    ~Stream();
+    bool packet_feedback = true;
+    bool use_rtx = true;
+    TimeDelta nack_history_time = TimeDelta::ms(1000);
+    bool use_flexfec = false;
+    bool use_ulpfec = false;
+    DataSize packet_overhead = DataSize::bytes(PacketOverhead::kDefault);
+  } stream;
+  struct Renderer {
+    enum Type { kFake } type = kFake;
+  };
+};
+
+struct AudioStreamConfig {
+  AudioStreamConfig();
+  AudioStreamConfig(const AudioStreamConfig&);
+  ~AudioStreamConfig();
+  bool autostart = true;
+  struct Source {
+    int channels = 1;
+  } source;
+  struct Encoder {
+    Encoder();
+    Encoder(const Encoder&);
+    ~Encoder();
+    bool allocate_bitrate = false;
+    absl::optional<DataRate> fixed_rate;
+    absl::optional<DataRate> min_rate;
+    absl::optional<DataRate> max_rate;
+    TimeDelta initial_frame_length = TimeDelta::ms(20);
+  } encoder;
+  struct Stream {
+    Stream();
+    Stream(const Stream&);
+    ~Stream();
+    bool in_bandwidth_estimation = false;
+    bool rate_allocation_priority = false;
+    DataSize packet_overhead = DataSize::bytes(PacketOverhead::kDefault);
+  } stream;
+  struct Render {
+    std::string sync_group;
+  } render;
+};
+
+struct NetworkNodeConfig {
+  NetworkNodeConfig();
+  NetworkNodeConfig(const NetworkNodeConfig&);
+  ~NetworkNodeConfig();
+  enum class TrafficMode {
+    kSimulation,
+    kCustom
+  } mode = TrafficMode::kSimulation;
+  struct Simulation {
+    Simulation();
+    Simulation(const Simulation&);
+    ~Simulation();
+    DataRate bandwidth = DataRate::Infinity();
+    TimeDelta delay = TimeDelta::Zero();
+    TimeDelta delay_std_dev = TimeDelta::Zero();
+    double loss_rate = 0;
+  } simulation;
+  DataSize packet_overhead = DataSize::Zero();
+  TimeDelta update_frequency = TimeDelta::ms(1);
+};
+
+struct CrossTrafficConfig {
+  CrossTrafficConfig();
+  CrossTrafficConfig(const CrossTrafficConfig&);
+  ~CrossTrafficConfig();
+  enum Mode { kRandomWalk, kPulsedPeaks } mode = kRandomWalk;
+  int random_seed = 1;
+  DataRate peak_rate = DataRate::kbps(100);
+  DataSize min_packet_size = DataSize::bytes(200);
+  TimeDelta min_packet_interval = TimeDelta::ms(1);
+  struct RandomWalk {
+    TimeDelta update_interval = TimeDelta::ms(200);
+    double variance = 0.6;
+    double bias = -0.1;
+  } random_walk;
+  struct PulsedPeaks {
+    TimeDelta send_duration = TimeDelta::ms(100);
+    TimeDelta hold_duration = TimeDelta::ms(2000);
+  } pulsed;
+};
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_SCENARIO_SCENARIO_CONFIG_H_
diff --git a/test/scenario/scenario_tests/BUILD.gn b/test/scenario/scenario_tests/BUILD.gn
new file mode 100644
index 0000000..1810c23
--- /dev/null
+++ b/test/scenario/scenario_tests/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright (c) 2018 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS.  All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import("../../../webrtc.gni")
+
+if (rtc_include_tests) {
+  rtc_test("scenario_tests") {
+    testonly = true
+    sources = [
+      "bbr_performance.cc",
+    ]
+    deps = [
+      "../:scenario",
+      "../..:test_main",
+      "../../:field_trial",
+      "../../:fileutils",
+      "../../:test_common",
+      "../../:test_support",
+      "../../../rtc_base:rtc_base_approved",
+      "../../../rtc_base:stringutils",
+      "../../../rtc_base/experiments:field_trial_parser",
+      "//testing/gtest",
+    ]
+    if (!build_with_chromium && is_clang) {
+      suppressed_configs += [ "//build/config/clang:find_bad_constructs" ]
+    }
+  }
+}
diff --git a/test/scenario/scenario_tests/bbr_performance.cc b/test/scenario/scenario_tests/bbr_performance.cc
new file mode 100644
index 0000000..e87cc68
--- /dev/null
+++ b/test/scenario/scenario_tests/bbr_performance.cc
@@ -0,0 +1,258 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "rtc_base/random.h"
+
+#include "rtc_base/experiments/field_trial_parser.h"
+#include "rtc_base/experiments/field_trial_units.h"
+#include "test/field_trial.h"
+#include "test/gtest.h"
+#include "test/scenario/scenario.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+constexpr int64_t kRunTimeMs = 60000;
+
+using ::testing::Values;
+using ::testing::Combine;
+using ::testing::tuple;
+using ::testing::make_tuple;
+
+using Codec = VideoStreamConfig::Encoder::Codec;
+using CodecImpl = VideoStreamConfig::Encoder::Implementation;
+
+struct CallTestConfig {
+  struct Scenario {
+    FieldTrialParameter<int> random_seed;
+    FieldTrialFlag return_traffic;
+    FieldTrialParameter<DataRate> capacity;
+    FieldTrialParameter<TimeDelta> propagation_delay;
+    FieldTrialParameter<DataRate> cross_traffic;
+    FieldTrialParameter<TimeDelta> delay_noise;
+    FieldTrialParameter<double> loss_rate;
+    Scenario()
+        : random_seed("rs", 1),
+          return_traffic("ret"),
+          capacity("bw", DataRate::kbps(300)),
+          propagation_delay("dl", TimeDelta::ms(100)),
+          cross_traffic("ct", DataRate::Zero()),
+          delay_noise("dn", TimeDelta::Zero()),
+          loss_rate("pl", 0) {}
+    void Parse(std::string config_str) {
+      ParseFieldTrial(
+          {&random_seed, &return_traffic, &capacity, &propagation_delay,
+           &cross_traffic, &delay_noise, &loss_rate},
+          config_str);
+    }
+  } scenario;
+  struct Tuning {
+    FieldTrialFlag use_bbr;
+    FieldTrialFlag bbr_no_target_rate;
+    FieldTrialOptional<DataSize> bbr_initial_window;
+    FieldTrialParameter<double> bbr_encoder_gain;
+    Tuning()
+        : use_bbr("bbr"),
+          bbr_no_target_rate("notr"),
+          bbr_initial_window("iw", DataSize::bytes(8000)),
+          bbr_encoder_gain("eg", 0.8) {}
+    void Parse(std::string config_str) {
+      ParseFieldTrial(
+          {
+              &use_bbr, &bbr_no_target_rate, &bbr_initial_window,
+              &bbr_encoder_gain,
+          },
+          config_str);
+    }
+  } tuning;
+
+  void Parse(std::string scenario_string, std::string tuning_string) {
+    scenario.Parse(scenario_string);
+    tuning.Parse(tuning_string);
+    scenario_str = scenario_string;
+    tuning_str = tuning_string;
+  }
+  std::string scenario_str;
+  std::string tuning_str;
+
+  std::string BbrTrial() const {
+    char trial_buf[1024];
+    rtc::SimpleStringBuilder trial(trial_buf);
+    trial << "WebRTC-BweBbrConfig/";
+    trial << "encoder_rate_gain_in_probe_rtt:0.5";
+    trial.AppendFormat(",encoder_rate_gain:%.1lf",
+                       tuning.bbr_encoder_gain.Get());
+    if (tuning.bbr_no_target_rate)
+      trial << ",pacing_rate_as_target:1";
+    if (tuning.bbr_initial_window)
+      trial << ",initial_cwin:" << tuning.bbr_initial_window->bytes();
+    trial << "/";
+    return trial.str();
+  }
+  std::string FieldTrials() const {
+    std::string trials = "WebRTC-TaskQueueCongestionControl/Enabled/";
+    if (tuning.use_bbr) {
+      trials +=
+          "WebRTC-BweCongestionController/Enabled,BBR/"
+          "WebRTC-PacerPushbackExperiment/Enabled/"
+          "WebRTC-Pacer-DrainQueue/Disabled/"
+          "WebRTC-Pacer-PadInSilence/Enabled/"
+          "WebRTC-Pacer-BlockAudio/Disabled/"
+          "WebRTC-Audio-SendSideBwe/Enabled/"
+          "WebRTC-SendSideBwe-WithOverhead/Enabled/";
+      trials += BbrTrial();
+    }
+    return trials;
+  }
+
+  std::string Name() const {
+    char raw_name[1024];
+    rtc::SimpleStringBuilder name(raw_name);
+    for (char c : scenario_str + "__tun__" + tuning_str) {
+      if (c == ':') {
+        continue;
+      } else if (c == ',') {
+        name << "_";
+      } else if (c == '%') {
+        name << "p";
+      } else {
+        name << c;
+      }
+    }
+    return name.str();
+  }
+};
+}  // namespace
+class BbrScenarioTest
+    : public ::testing::Test,
+      public testing::WithParamInterface<tuple<std::string, std::string>> {
+ public:
+  BbrScenarioTest() {
+    conf_.Parse(::testing::get<0>(GetParam()), ::testing::get<1>(GetParam()));
+    field_trial_.reset(new test::ScopedFieldTrials(conf_.FieldTrials()));
+  }
+  CallTestConfig conf_;
+
+ private:
+  std::unique_ptr<test::ScopedFieldTrials> field_trial_;
+};
+
+TEST_P(BbrScenarioTest, ReceivesVideo) {
+  Scenario s("bbr_test_gen/bbr__" + conf_.Name());
+
+  CallClient* alice = s.CreateClient("send", [&](CallClientConfig* c) {
+    if (conf_.tuning.use_bbr)
+      c->transport.cc = TransportControllerConfig::CongestionController::kBbr;
+    c->transport.state_log_interval = TimeDelta::ms(100);
+    c->transport.rates.min_rate = DataRate::kbps(30);
+    c->transport.rates.max_rate = DataRate::kbps(1800);
+  });
+  CallClient* bob = s.CreateClient("return", [&](CallClientConfig* c) {
+    if (conf_.tuning.use_bbr && conf_.scenario.return_traffic)
+      c->transport.cc = TransportControllerConfig::CongestionController::kBbr;
+    c->transport.state_log_interval = TimeDelta::ms(100);
+    c->transport.rates.min_rate = DataRate::kbps(30);
+    c->transport.rates.max_rate = DataRate::kbps(1800);
+  });
+  NetworkNodeConfig net_conf;
+  net_conf.simulation.bandwidth = conf_.scenario.capacity;
+  net_conf.simulation.delay = conf_.scenario.propagation_delay;
+  net_conf.simulation.loss_rate = conf_.scenario.loss_rate;
+  net_conf.simulation.delay_std_dev = conf_.scenario.delay_noise;
+  SimulationNode* send_net = s.CreateSimulationNode(net_conf);
+  SimulationNode* ret_net = s.CreateSimulationNode(net_conf);
+  VideoStreamPair* alice_video = s.CreateVideoStream(
+      alice, {send_net}, bob, {ret_net}, [&](VideoStreamConfig* c) {
+        c->encoder.fake.max_rate = DataRate::kbps(1800);
+      });
+  s.CreateAudioStream(alice, {send_net}, bob, {ret_net},
+                      [&](AudioStreamConfig* c) {
+                        if (conf_.tuning.use_bbr) {
+                          c->stream.in_bandwidth_estimation = true;
+                          c->encoder.fixed_rate = DataRate::kbps(31);
+                        }
+                      });
+
+  VideoStreamPair* bob_video = nullptr;
+  if (conf_.scenario.return_traffic) {
+    bob_video = s.CreateVideoStream(
+        bob, {ret_net}, alice, {send_net}, [&](VideoStreamConfig* c) {
+          c->encoder.fake.max_rate = DataRate::kbps(1800);
+        });
+    s.CreateAudioStream(bob, {ret_net}, alice, {send_net},
+                        [&](AudioStreamConfig* c) {
+                          if (conf_.tuning.use_bbr) {
+                            c->stream.in_bandwidth_estimation = true;
+                            c->encoder.fixed_rate = DataRate::kbps(31);
+                          }
+                        });
+  }
+  CrossTrafficConfig cross_config;
+  cross_config.peak_rate = conf_.scenario.cross_traffic;
+  cross_config.random_seed = conf_.scenario.random_seed;
+  CrossTrafficSource* cross_traffic =
+      s.CreateCrossTraffic({send_net}, cross_config);
+
+  s.CreatePrinter("send.stats.txt", TimeDelta::ms(100),
+                  {alice->StatsPrinter(), alice_video->send()->StatsPrinter(),
+                   cross_traffic->StatsPrinter(), send_net->ConfigPrinter()});
+
+  std::vector<ColumnPrinter> return_printers{
+      bob->StatsPrinter(), ColumnPrinter::Fixed("cross_traffic_rate", "0"),
+      ret_net->ConfigPrinter()};
+  if (bob_video)
+    return_printers.push_back(bob_video->send()->StatsPrinter());
+  s.CreatePrinter("return.stats.txt", TimeDelta::ms(100), return_printers);
+
+  s.RunFor(TimeDelta::ms(kRunTimeMs));
+}
+
+INSTANTIATE_TEST_CASE_P(Selected,
+                        BbrScenarioTest,
+                        Values(make_tuple("rs:1,bw:150,dl:100,ct:100", "bbr")));
+
+INSTANTIATE_TEST_CASE_P(
+    OneWayTuning,
+    BbrScenarioTest,
+    Values(make_tuple("bw:150,dl:100", "bbr,iw:,eg:100%,notr"),
+           make_tuple("bw:150,dl:100", "bbr,iw:8000,eg:100%,notr"),
+           make_tuple("bw:150,dl:100", "bbr,iw:8000,eg:100%"),
+           make_tuple("bw:150,dl:100", "bbr,iw:8000,eg:80%")));
+
+INSTANTIATE_TEST_CASE_P(OneWayTuned,
+                        BbrScenarioTest,
+                        Values(make_tuple("bw:150,dl:100", "bbr"),
+                               make_tuple("bw:150,dl:100", ""),
+                               make_tuple("bw:800,dl:100", "bbr"),
+                               make_tuple("bw:800,dl:100", "")));
+
+INSTANTIATE_TEST_CASE_P(OneWayDegraded,
+                        BbrScenarioTest,
+                        Values(make_tuple("bw:150,dl:100,dn:30,pl:5%", "bbr"),
+                               make_tuple("bw:150,dl:100,dn:30,pl:5%", ""),
+
+                               make_tuple("bw:150,ct:100,dl:100", "bbr"),
+                               make_tuple("bw:150,ct:100,dl:100", ""),
+
+                               make_tuple("bw:800,dl:100,dn:30,pl:5%", "bbr"),
+                               make_tuple("bw:800,dl:100,dn:30,pl:5%", ""),
+
+                               make_tuple("bw:800,ct:600,dl:100", "bbr"),
+                               make_tuple("bw:800,ct:600,dl:100", "")));
+
+INSTANTIATE_TEST_CASE_P(TwoWay,
+                        BbrScenarioTest,
+                        Values(make_tuple("ret,bw:150,dl:100", "bbr"),
+                               make_tuple("ret,bw:150,dl:100", ""),
+                               make_tuple("ret,bw:800,dl:100", "bbr"),
+                               make_tuple("ret,bw:800,dl:100", ""),
+                               make_tuple("ret,bw:150,dl:50", "bbr"),
+                               make_tuple("ret,bw:150,dl:50", "")));
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/scenario_unittest.cc b/test/scenario/scenario_unittest.cc
new file mode 100644
index 0000000..305f866
--- /dev/null
+++ b/test/scenario/scenario_unittest.cc
@@ -0,0 +1,53 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "test/scenario/scenario.h"
+#include "test/gtest.h"
+namespace webrtc {
+namespace test {
+TEST(ScenarioTest, StartsAndStopsWithoutErrors) {
+  Scenario s;
+  CallClientConfig call_client_config;
+  call_client_config.transport.rates.start_rate = DataRate::kbps(300);
+  auto* alice = s.CreateClient("alice", call_client_config);
+  auto* bob = s.CreateClient("bob", call_client_config);
+  NetworkNodeConfig network_config;
+  auto alice_net = s.CreateSimulationNode(network_config);
+  auto bob_net = s.CreateSimulationNode(network_config);
+
+  VideoStreamConfig video_stream_config;
+  s.CreateVideoStream(alice, {alice_net}, bob, {bob_net}, video_stream_config);
+  s.CreateVideoStream(bob, {bob_net}, alice, {alice_net}, video_stream_config);
+
+  AudioStreamConfig audio_stream_config;
+  s.CreateAudioStream(alice, {alice_net}, bob, {bob_net}, audio_stream_config);
+  s.CreateAudioStream(bob, {bob_net}, alice, {alice_net}, audio_stream_config);
+
+  CrossTrafficConfig cross_traffic_config;
+  s.CreateCrossTraffic({alice_net}, cross_traffic_config);
+
+  bool packet_received = false;
+  s.NetworkDelayedAction({alice_net, bob_net}, 100,
+                         [&packet_received] { packet_received = true; });
+  bool bitrate_changed = false;
+  s.Every(TimeDelta::ms(10), [alice, bob, &bitrate_changed] {
+    if (alice->GetStats().send_bandwidth_bps != 300000 &&
+        bob->GetStats().send_bandwidth_bps != 300000)
+      bitrate_changed = true;
+  });
+  s.RunUntil(TimeDelta::seconds(2), TimeDelta::ms(5),
+             [&bitrate_changed, &packet_received] {
+               return packet_received && bitrate_changed;
+             });
+  EXPECT_TRUE(packet_received);
+  EXPECT_TRUE(bitrate_changed);
+}
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/video_stream.cc b/test/scenario/video_stream.cc
new file mode 100644
index 0000000..fe87a6d
--- /dev/null
+++ b/test/scenario/video_stream.cc
@@ -0,0 +1,383 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#include "test/scenario/video_stream.h"
+
+#include <algorithm>
+#include <utility>
+
+#include "media/base/mediaconstants.h"
+#include "media/engine/internaldecoderfactory.h"
+#include "media/engine/internalencoderfactory.h"
+#include "media/engine/webrtcvideoengine.h"
+#include "test/call_test.h"
+#include "test/fake_encoder.h"
+#include "test/function_video_encoder_factory.h"
+
+#include "test/scenario/hardware_codecs.h"
+#include "test/testsupport/fileutils.h"
+
+namespace webrtc {
+namespace test {
+namespace {
+constexpr int kDefaultMaxQp = cricket::WebRtcVideoChannel::kDefaultQpMax;
+const int kVideoRotationRtpExtensionId = 4;
+uint8_t CodecTypeToPayloadType(VideoCodecType codec_type) {
+  switch (codec_type) {
+    case VideoCodecType::kVideoCodecGeneric:
+      return CallTest::kFakeVideoSendPayloadType;
+    case VideoCodecType::kVideoCodecVP8:
+      return CallTest::kPayloadTypeVP8;
+    case VideoCodecType::kVideoCodecVP9:
+      return CallTest::kPayloadTypeVP9;
+    case VideoCodecType::kVideoCodecH264:
+      return CallTest::kPayloadTypeH264;
+    default:
+      RTC_NOTREACHED();
+  }
+  return {};
+}
+std::string CodecTypeToCodecName(VideoCodecType codec_type) {
+  switch (codec_type) {
+    case VideoCodecType::kVideoCodecGeneric:
+      return "";
+    case VideoCodecType::kVideoCodecVP8:
+      return cricket::kVp8CodecName;
+    case VideoCodecType::kVideoCodecVP9:
+      return cricket::kVp9CodecName;
+    case VideoCodecType::kVideoCodecH264:
+      return cricket::kH264CodecName;
+    default:
+      RTC_NOTREACHED();
+  }
+  return {};
+}
+std::vector<RtpExtension> GetVideoRtpExtensions(
+    const VideoStreamConfig config) {
+  return {RtpExtension(RtpExtension::kTransportSequenceNumberUri,
+                       kTransportSequenceNumberExtensionId),
+          RtpExtension(RtpExtension::kVideoContentTypeUri,
+                       kVideoContentTypeExtensionId),
+          RtpExtension(RtpExtension::kVideoRotationUri,
+                       kVideoRotationRtpExtensionId)};
+}
+
+VideoSendStream::Config CreateVideoSendStreamConfig(VideoStreamConfig config,
+                                                    std::vector<uint32_t> ssrcs,
+                                                    Transport* send_transport) {
+  VideoSendStream::Config send_config(send_transport);
+  send_config.rtp.payload_name = CodecTypeToPayloadString(config.encoder.codec);
+  send_config.rtp.payload_type = CodecTypeToPayloadType(config.encoder.codec);
+
+  send_config.rtp.ssrcs = ssrcs;
+  send_config.rtp.extensions = GetVideoRtpExtensions(config);
+
+  if (config.stream.use_flexfec) {
+    send_config.rtp.flexfec.payload_type = CallTest::kFlexfecPayloadType;
+    send_config.rtp.flexfec.ssrc = CallTest::kFlexfecSendSsrc;
+    send_config.rtp.flexfec.protected_media_ssrcs = ssrcs;
+  }
+  if (config.stream.use_ulpfec) {
+    send_config.rtp.ulpfec.red_payload_type = CallTest::kRedPayloadType;
+    send_config.rtp.ulpfec.ulpfec_payload_type = CallTest::kUlpfecPayloadType;
+    send_config.rtp.ulpfec.red_rtx_payload_type = CallTest::kRtxRedPayloadType;
+  }
+  return send_config;
+}
+rtc::scoped_refptr<VideoEncoderConfig::EncoderSpecificSettings>
+CreateEncoderSpecificSettings(VideoStreamConfig config) {
+  using Codec = VideoStreamConfig::Encoder::Codec;
+  switch (config.encoder.codec) {
+    case Codec::kVideoCodecH264: {
+      VideoCodecH264 h264_settings = VideoEncoder::GetDefaultH264Settings();
+      h264_settings.frameDroppingOn = true;
+      h264_settings.keyFrameInterval =
+          config.encoder.key_frame_interval.value_or(0);
+      return new rtc::RefCountedObject<
+          VideoEncoderConfig::H264EncoderSpecificSettings>(h264_settings);
+    }
+    case Codec::kVideoCodecVP8: {
+      VideoCodecVP8 vp8_settings = VideoEncoder::GetDefaultVp8Settings();
+      vp8_settings.frameDroppingOn = true;
+      vp8_settings.keyFrameInterval =
+          config.encoder.key_frame_interval.value_or(0);
+      vp8_settings.automaticResizeOn = true;
+      vp8_settings.denoisingOn = config.encoder.denoising;
+      return new rtc::RefCountedObject<
+          VideoEncoderConfig::Vp8EncoderSpecificSettings>(vp8_settings);
+    }
+    case Codec::kVideoCodecVP9: {
+      VideoCodecVP9 vp9_settings = VideoEncoder::GetDefaultVp9Settings();
+      vp9_settings.frameDroppingOn = true;
+      vp9_settings.keyFrameInterval =
+          config.encoder.key_frame_interval.value_or(0);
+      vp9_settings.automaticResizeOn = true;
+      vp9_settings.denoisingOn = config.encoder.denoising;
+      vp9_settings.interLayerPred = InterLayerPredMode::kOnKeyPic;
+      return new rtc::RefCountedObject<
+          VideoEncoderConfig::Vp9EncoderSpecificSettings>(vp9_settings);
+    }
+    default:
+      return nullptr;
+  }
+}
+
+VideoEncoderConfig CreateVideoEncoderConfig(VideoStreamConfig config) {
+  size_t num_streams = config.encoder.num_simulcast_streams;
+  VideoEncoderConfig encoder_config;
+  encoder_config.codec_type = config.encoder.codec;
+  encoder_config.content_type = VideoEncoderConfig::ContentType::kRealtimeVideo;
+  encoder_config.video_format =
+      SdpVideoFormat(CodecTypeToPayloadString(config.encoder.codec), {});
+  encoder_config.number_of_streams = num_streams;
+  encoder_config.simulcast_layers = std::vector<VideoStream>(num_streams);
+
+  std::string cricket_codec = CodecTypeToCodecName(config.encoder.codec);
+  if (!cricket_codec.empty()) {
+    encoder_config.video_stream_factory =
+        new rtc::RefCountedObject<cricket::EncoderStreamFactory>(
+            cricket_codec, kDefaultMaxQp, false, false);
+  } else {
+    encoder_config.video_stream_factory =
+        new rtc::RefCountedObject<DefaultVideoStreamFactory>();
+  }
+  if (config.encoder.max_data_rate) {
+    encoder_config.max_bitrate_bps = config.encoder.max_data_rate->bps();
+  } else {
+    encoder_config.max_bitrate_bps = 10000000;  // 10 mbit
+  }
+  encoder_config.encoder_specific_settings =
+      CreateEncoderSpecificSettings(config);
+  return encoder_config;
+}
+}  // namespace
+
+SendVideoStream::SendVideoStream(CallClient* sender,
+                                 VideoStreamConfig config,
+                                 Transport* send_transport)
+    : sender_(sender), config_(config) {
+  for (size_t i = 0; i < config.encoder.num_simulcast_streams; ++i) {
+    ssrcs_.push_back(sender->GetNextVideoSsrc());
+    rtx_ssrcs_.push_back(sender->GetNextRtxSsrc());
+  }
+
+  using Capture = VideoStreamConfig::Source::Capture;
+  switch (config.source.capture) {
+    case Capture::kGenerator:
+      frame_generator_ = test::FrameGeneratorCapturer::Create(
+          config.source.width, config.source.height,
+          config.source.generator.pixel_format, absl::nullopt,
+          config.source.framerate, sender_->clock_);
+      video_capturer_.reset(frame_generator_);
+      break;
+    case Capture::kVideoFile:
+      frame_generator_ = test::FrameGeneratorCapturer::CreateFromYuvFile(
+          test::ResourcePath(config.source.video_file.name, "yuv"),
+          config.source.width, config.source.height, config.source.framerate,
+          sender_->clock_);
+      RTC_CHECK(frame_generator_)
+          << "Could not create capturer for " << config.source.video_file.name
+          << ".yuv. Is this resource file present?";
+      video_capturer_.reset(frame_generator_);
+      break;
+  }
+
+  using Encoder = VideoStreamConfig::Encoder;
+  using Codec = VideoStreamConfig::Encoder::Codec;
+  switch (config.encoder.implementation) {
+    case Encoder::Implementation::kFake:
+      if (config.encoder.codec == Codec::kVideoCodecGeneric) {
+        encoder_factory_ =
+            absl::make_unique<FunctionVideoEncoderFactory>([this, config]() {
+              auto encoder =
+                  absl::make_unique<test::FakeEncoder>(sender_->clock_);
+              if (config.encoder.fake.max_rate.IsFinite())
+                encoder->SetMaxBitrate(config.encoder.fake.max_rate.kbps());
+              return encoder;
+            });
+      } else {
+        RTC_NOTREACHED();
+      }
+      break;
+    case VideoStreamConfig::Encoder::Implementation::kSoftware:
+      encoder_factory_.reset(new InternalEncoderFactory());
+      break;
+    case VideoStreamConfig::Encoder::Implementation::kHardware:
+      encoder_factory_ = CreateHardwareEncoderFactory();
+      break;
+  }
+  RTC_CHECK(encoder_factory_);
+
+  VideoSendStream::Config send_config =
+      CreateVideoSendStreamConfig(config, ssrcs_, send_transport);
+  send_config.encoder_settings.encoder_factory = encoder_factory_.get();
+  VideoEncoderConfig encoder_config = CreateVideoEncoderConfig(config);
+
+  send_stream_ = sender_->call_->CreateVideoSendStream(
+      std::move(send_config), std::move(encoder_config));
+
+  send_stream_->SetSource(video_capturer_.get(),
+                          config.encoder.degradation_preference);
+}
+
+SendVideoStream::~SendVideoStream() {
+  sender_->call_->DestroyVideoSendStream(send_stream_);
+}
+
+void SendVideoStream::Start() {
+  send_stream_->Start();
+  video_capturer_->Start();
+}
+
+bool SendVideoStream::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                       uint64_t receiver,
+                                       Timestamp at_time) {
+  // Removes added overhead before delivering RTCP packet to sender.
+  RTC_DCHECK_GE(packet.size(), config_.stream.packet_overhead.bytes());
+  packet.SetSize(packet.size() - config_.stream.packet_overhead.bytes());
+  sender_->DeliverPacket(MediaType::VIDEO, packet, at_time);
+  return true;
+}
+
+void SendVideoStream::SetCaptureFramerate(int framerate) {
+  RTC_CHECK(frame_generator_)
+      << "Framerate change only implemented for generators";
+  frame_generator_->ChangeFramerate(framerate);
+}
+
+void SendVideoStream::SetMaxFramerate(absl::optional<int> max_framerate) {
+  VideoEncoderConfig encoder_config = CreateVideoEncoderConfig(config_);
+  RTC_DCHECK_EQ(encoder_config.simulcast_layers.size(), 1);
+  encoder_config.simulcast_layers[0].max_framerate = max_framerate.value_or(-1);
+  send_stream_->ReconfigureVideoEncoder(std::move(encoder_config));
+}
+
+VideoSendStream::Stats SendVideoStream::GetStats() const {
+  return send_stream_->GetStats();
+}
+
+ColumnPrinter SendVideoStream::StatsPrinter() {
+  return ColumnPrinter::Lambda(
+      "video_target_rate video_sent_rate width height",
+      [this](rtc::SimpleStringBuilder& sb) {
+        VideoSendStream::Stats video_stats = send_stream_->GetStats();
+        int width = 0;
+        int height = 0;
+        for (auto stream_stat : video_stats.substreams) {
+          width = std::max(width, stream_stat.second.width);
+          height = std::max(height, stream_stat.second.height);
+        }
+        sb.AppendFormat("%.0lf %.0lf %i %i",
+                        video_stats.target_media_bitrate_bps / 8.0,
+                        video_stats.media_bitrate_bps / 8.0, width, height);
+      },
+      64);
+}
+
+ReceiveVideoStream::ReceiveVideoStream(CallClient* receiver,
+                                       VideoStreamConfig config,
+                                       SendVideoStream* send_stream,
+                                       size_t chosen_stream,
+                                       Transport* feedback_transport)
+    : receiver_(receiver),
+      config_(config),
+      decoder_factory_(absl::make_unique<InternalDecoderFactory>()) {
+  renderer_ = absl::make_unique<FakeVideoRenderer>();
+  VideoReceiveStream::Config recv_config(feedback_transport);
+  recv_config.rtp.remb = !config.stream.packet_feedback;
+  recv_config.rtp.transport_cc = config.stream.packet_feedback;
+  recv_config.rtp.local_ssrc = CallTest::kReceiverLocalVideoSsrc;
+  recv_config.rtp.extensions = GetVideoRtpExtensions(config);
+  RTC_DCHECK(!config.stream.use_rtx ||
+             config.stream.nack_history_time > TimeDelta::Zero());
+  recv_config.rtp.nack.rtp_history_ms = config.stream.nack_history_time.ms();
+  recv_config.rtp.protected_by_flexfec = config.stream.use_flexfec;
+  recv_config.renderer = renderer_.get();
+  if (config.stream.use_rtx) {
+    recv_config.rtp.rtx_ssrc = send_stream->rtx_ssrcs_[chosen_stream];
+    recv_config.rtp
+        .rtx_associated_payload_types[CallTest::kSendRtxPayloadType] =
+        CodecTypeToPayloadType(config.encoder.codec);
+  }
+  recv_config.rtp.remote_ssrc = send_stream->ssrcs_[chosen_stream];
+  VideoReceiveStream::Decoder decoder =
+      CreateMatchingDecoder(CodecTypeToPayloadType(config.encoder.codec),
+                            CodecTypeToPayloadString(config.encoder.codec));
+  decoder.decoder_factory = decoder_factory_.get();
+  recv_config.decoders.push_back(decoder);
+
+  if (config.stream.use_flexfec) {
+    RTC_CHECK_EQ(config.encoder.num_simulcast_streams, 1);
+    FlexfecReceiveStream::Config flexfec_config(feedback_transport);
+    flexfec_config.payload_type = CallTest::kFlexfecPayloadType;
+    flexfec_config.remote_ssrc = CallTest::kFlexfecSendSsrc;
+    flexfec_config.protected_media_ssrcs = send_stream->rtx_ssrcs_;
+    flexfec_config.local_ssrc = recv_config.rtp.local_ssrc;
+    flecfec_stream_ =
+        receiver_->call_->CreateFlexfecReceiveStream(flexfec_config);
+  }
+  if (config.stream.use_ulpfec) {
+    recv_config.rtp.red_payload_type = CallTest::kRedPayloadType;
+    recv_config.rtp.ulpfec_payload_type = CallTest::kUlpfecPayloadType;
+    recv_config.rtp.rtx_associated_payload_types[CallTest::kRtxRedPayloadType] =
+        CallTest::kRedPayloadType;
+  }
+  receive_stream_ =
+      receiver_->call_->CreateVideoReceiveStream(std::move(recv_config));
+}
+
+ReceiveVideoStream::~ReceiveVideoStream() {
+  receiver_->call_->DestroyVideoReceiveStream(receive_stream_);
+  if (flecfec_stream_)
+    receiver_->call_->DestroyFlexfecReceiveStream(flecfec_stream_);
+}
+
+bool ReceiveVideoStream::TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                                          uint64_t receiver,
+                                          Timestamp at_time) {
+  RTC_DCHECK_GE(packet.size(), config_.stream.packet_overhead.bytes());
+  packet.SetSize(packet.size() - config_.stream.packet_overhead.bytes());
+  receiver_->DeliverPacket(MediaType::VIDEO, packet, at_time);
+  return true;
+}
+
+VideoStreamPair::~VideoStreamPair() = default;
+
+VideoStreamPair::VideoStreamPair(CallClient* sender,
+                                 std::vector<NetworkNode*> send_link,
+                                 uint64_t send_receiver_id,
+                                 CallClient* receiver,
+                                 std::vector<NetworkNode*> return_link,
+                                 uint64_t return_receiver_id,
+                                 VideoStreamConfig config)
+    : config_(config),
+      send_link_(send_link),
+      return_link_(return_link),
+      send_transport_(sender,
+                      send_link.front(),
+                      send_receiver_id,
+                      config.stream.packet_overhead),
+      return_transport_(receiver,
+                        return_link.front(),
+                        return_receiver_id,
+                        config.stream.packet_overhead),
+      send_stream_(sender, config, &send_transport_),
+      receive_stream_(receiver,
+                      config,
+                      &send_stream_,
+                      /*chosen_stream=*/0,
+                      &return_transport_) {
+  NetworkNode::Route(send_transport_.ReceiverId(), send_link_,
+                     &receive_stream_);
+  NetworkNode::Route(return_transport_.ReceiverId(), return_link_,
+                     &send_stream_);
+}
+
+}  // namespace test
+}  // namespace webrtc
diff --git a/test/scenario/video_stream.h b/test/scenario/video_stream.h
new file mode 100644
index 0000000..e669618
--- /dev/null
+++ b/test/scenario/video_stream.h
@@ -0,0 +1,117 @@
+/*
+ *  Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+#ifndef TEST_SCENARIO_VIDEO_STREAM_H_
+#define TEST_SCENARIO_VIDEO_STREAM_H_
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "rtc_base/constructormagic.h"
+#include "test/frame_generator_capturer.h"
+#include "test/scenario/call_client.h"
+#include "test/scenario/column_printer.h"
+#include "test/scenario/network_node.h"
+#include "test/scenario/scenario_config.h"
+#include "test/test_video_capturer.h"
+
+namespace webrtc {
+namespace test {
+// SendVideoStream provides an interface for changing parameters and retrieving
+// states at run time.
+class SendVideoStream : public NetworkReceiverInterface {
+ public:
+  RTC_DISALLOW_COPY_AND_ASSIGN(SendVideoStream);
+  ~SendVideoStream();
+  void SetCaptureFramerate(int framerate);
+  void SetMaxFramerate(absl::optional<int> max_framerate);
+  VideoSendStream::Stats GetStats() const;
+  ColumnPrinter StatsPrinter();
+  void Start();
+
+ private:
+  friend class Scenario;
+  friend class VideoStreamPair;
+  friend class ReceiveVideoStream;
+  // Handles RTCP feedback for this stream.
+  SendVideoStream(CallClient* sender,
+                  VideoStreamConfig config,
+                  Transport* send_transport);
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+
+  std::vector<uint32_t> ssrcs_;
+  std::vector<uint32_t> rtx_ssrcs_;
+  VideoSendStream* send_stream_ = nullptr;
+  CallClient* const sender_;
+  const VideoStreamConfig config_;
+  std::unique_ptr<VideoEncoderFactory> encoder_factory_;
+  std::unique_ptr<TestVideoCapturer> video_capturer_;
+  FrameGeneratorCapturer* frame_generator_ = nullptr;
+};
+
+// ReceiveVideoStream represents a video receiver. It can't be used directly.
+class ReceiveVideoStream : public NetworkReceiverInterface {
+ public:
+  RTC_DISALLOW_COPY_AND_ASSIGN(ReceiveVideoStream);
+  ~ReceiveVideoStream();
+
+ private:
+  friend class Scenario;
+  friend class VideoStreamPair;
+  ReceiveVideoStream(CallClient* receiver,
+                     VideoStreamConfig config,
+                     SendVideoStream* send_stream,
+                     size_t chosen_stream,
+                     Transport* feedback_transport);
+  bool TryDeliverPacket(rtc::CopyOnWriteBuffer packet,
+                        uint64_t receiver,
+                        Timestamp at_time) override;
+  VideoReceiveStream* receive_stream_ = nullptr;
+  FlexfecReceiveStream* flecfec_stream_ = nullptr;
+  std::unique_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> renderer_;
+  CallClient* const receiver_;
+  const VideoStreamConfig config_;
+  std::unique_ptr<VideoDecoderFactory> decoder_factory_;
+};
+
+// VideoStreamPair represents a video streaming session. It can be used to
+// access underlying send and receive classes. It can also be used in calls to
+// the Scenario class.
+class VideoStreamPair {
+ public:
+  RTC_DISALLOW_COPY_AND_ASSIGN(VideoStreamPair);
+  ~VideoStreamPair();
+  SendVideoStream* send() { return &send_stream_; }
+  ReceiveVideoStream* receive() { return &receive_stream_; }
+
+ private:
+  friend class Scenario;
+  VideoStreamPair(CallClient* sender,
+                  std::vector<NetworkNode*> send_link,
+                  uint64_t send_receiver_id,
+                  CallClient* receiver,
+                  std::vector<NetworkNode*> return_link,
+                  uint64_t return_receiver_id,
+                  VideoStreamConfig config);
+
+  const VideoStreamConfig config_;
+  std::vector<NetworkNode*> send_link_;
+  std::vector<NetworkNode*> return_link_;
+  NetworkNodeTransport send_transport_;
+  NetworkNodeTransport return_transport_;
+
+  SendVideoStream send_stream_;
+  ReceiveVideoStream receive_stream_;
+};
+}  // namespace test
+}  // namespace webrtc
+
+#endif  // TEST_SCENARIO_VIDEO_STREAM_H_