Adds optional video quality metrics to scenario tests.

Bug: webrtc:9510
Change-Id: I448e7156cc8f56930f58c4d25bd167df83a2ba85
Reviewed-on: https://webrtc-review.googlesource.com/c/114885
Commit-Queue: Sebastian Jansson <srte@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#26065}
This commit is contained in:
Sebastian Jansson
2018-12-19 13:14:41 +01:00
committed by Commit Bot
parent 54fa02486a
commit 9a4f38ec5c
11 changed files with 469 additions and 19 deletions

View File

@ -16,12 +16,16 @@ if (rtc_include_tests) {
"audio_stream.h",
"call_client.cc",
"call_client.h",
"call_client.h",
"column_printer.cc",
"column_printer.h",
"hardware_codecs.cc",
"hardware_codecs.h",
"network_node.cc",
"network_node.h",
"quality_info.h",
"quality_stats.cc",
"quality_stats.h",
"scenario.cc",
"scenario.h",
"scenario_config.cc",
@ -90,6 +94,7 @@ if (rtc_include_tests) {
"../../system_wrappers",
"../../system_wrappers:field_trial",
"../../video",
"//third_party/abseil-cpp/absl/memory",
"//third_party/abseil-cpp/absl/types:optional",
]
if (is_android) {
@ -125,4 +130,25 @@ if (rtc_include_tests) {
"//third_party/abseil-cpp/absl/memory",
]
}
rtc_source_set("scenario_slow_tests") {
testonly = true
sources = [
"quality_stats_unittest.cc",
]
if (!build_with_chromium && is_clang) {
suppressed_configs += [ "//build/config/clang:find_bad_constructs" ]
}
deps = [
":scenario",
"../../logging:mocks",
"../../rtc_base:checks",
"../../rtc_base:rtc_base_approved",
"../../system_wrappers",
"../../system_wrappers:field_trial",
"../../test:field_trial",
"../../test:test_support",
"//testing/gmock",
"//third_party/abseil-cpp/absl/memory",
]
}
}

View File

@ -134,17 +134,18 @@ TimeDelta LoggingNetworkControllerFactory::GetProcessInterval() const {
}
CallClient::CallClient(Clock* clock,
std::string name,
std::string log_filename,
CallClientConfig config)
: clock_(clock),
name_(name),
network_controller_factory_(log_filename, config.transport),
fake_audio_setup_(InitAudio()),
call_(CreateCall(config,
&network_controller_factory_,
fake_audio_setup_.audio_state)),
transport_(clock_, call_.get()),
header_parser_(RtpHeaderParser::Create()) {
} // namespace test
header_parser_(RtpHeaderParser::Create()) {}
CallClient::~CallClient() {
delete header_parser_;

View File

@ -58,7 +58,10 @@ struct CallClientFakeAudio {
// stream session.
class CallClient : public NetworkReceiverInterface {
public:
CallClient(Clock* clock, std::string log_filename, CallClientConfig config);
CallClient(Clock* clock,
std::string name,
std::string log_filename,
CallClientConfig config);
RTC_DISALLOW_COPY_AND_ASSIGN(CallClient);
~CallClient();
@ -89,6 +92,7 @@ class CallClient : public NetworkReceiverInterface {
void AddExtensions(std::vector<RtpExtension> extensions);
Clock* clock_;
const std::string name_;
LoggingNetworkControllerFactory network_controller_factory_;
CallClientFakeAudio fake_audio_setup_;
std::unique_ptr<Call> call_;

View File

@ -0,0 +1,27 @@
/*
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#ifndef TEST_SCENARIO_QUALITY_INFO_H_
#define TEST_SCENARIO_QUALITY_INFO_H_
#include "api/units/timestamp.h"
namespace webrtc {
namespace test {
struct VideoFrameQualityInfo {
Timestamp capture_time;
Timestamp received_capture_time;
Timestamp render_time;
int width;
int height;
double psnr;
};
} // namespace test
} // namespace webrtc
#endif // TEST_SCENARIO_QUALITY_INFO_H_

View File

@ -0,0 +1,183 @@
/*
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "test/scenario/quality_stats.h"
#include <utility>
#include "common_video/libyuv/include/webrtc_libyuv.h"
#include "rtc_base/checks.h"
#include "rtc_base/event.h"
namespace webrtc {
namespace test {
VideoQualityAnalyzer::VideoQualityAnalyzer(
std::string filename_or_empty,
std::function<void(const VideoFrameQualityInfo&)> frame_info_handler)
: task_queue_("VideoAnalyzer") {
if (!filename_or_empty.empty()) {
output_file_ = fopen(filename_or_empty.c_str(), "w");
RTC_CHECK(output_file_);
PrintHeaders();
frame_info_handlers_.push_back(
[this](const VideoFrameQualityInfo& info) { PrintFrameInfo(info); });
}
if (frame_info_handler)
frame_info_handlers_.push_back(frame_info_handler);
}
VideoQualityAnalyzer::~VideoQualityAnalyzer() {
rtc::Event event;
task_queue_.PostTask([&event] { event.Set(); });
event.Wait(rtc::Event::kForever);
if (output_file_)
fclose(output_file_);
}
void VideoQualityAnalyzer::OnCapturedFrame(const VideoFrame& frame) {
VideoFrame copy = frame;
task_queue_.PostTask([this, copy] {
if (!first_capture_ntp_time_ms_)
first_capture_ntp_time_ms_ = copy.ntp_time_ms();
captured_frames_.push_back(std::move(copy));
});
}
void VideoQualityAnalyzer::OnDecodedFrame(const VideoFrame& frame) {
VideoFrame decoded = frame;
RTC_CHECK(frame.ntp_time_ms());
RTC_CHECK(frame.timestamp());
task_queue_.PostTask([this, decoded] {
// If first frame never is received, this value will be wrong. However, that
// is something that is very unlikely to happen.
if (!first_decode_rtp_timestamp_)
first_decode_rtp_timestamp_ = decoded.timestamp();
RTC_CHECK(!captured_frames_.empty());
int64_t decoded_capture_time_ms = DecodedFrameCaptureTimeOffsetMs(decoded);
while (CapturedFrameCaptureTimeOffsetMs(captured_frames_.front()) <
decoded_capture_time_ms) {
VideoFrame lost = std::move(captured_frames_.front());
captured_frames_.pop_front();
VideoFrameQualityInfo lost_info =
VideoFrameQualityInfo{Timestamp::us(lost.timestamp_us()),
Timestamp::PlusInfinity(),
Timestamp::PlusInfinity(),
lost.width(),
lost.height(),
NAN};
for (auto& handler : frame_info_handlers_)
handler(lost_info);
RTC_CHECK(!captured_frames_.empty());
}
RTC_CHECK(!captured_frames_.empty());
RTC_CHECK(CapturedFrameCaptureTimeOffsetMs(captured_frames_.front()) ==
DecodedFrameCaptureTimeOffsetMs(decoded));
VideoFrame captured = std::move(captured_frames_.front());
captured_frames_.pop_front();
VideoFrameQualityInfo decoded_info =
VideoFrameQualityInfo{Timestamp::us(captured.timestamp_us()),
Timestamp::ms(decoded.timestamp() / 90.0),
Timestamp::ms(decoded.render_time_ms()),
decoded.width(),
decoded.height(),
I420PSNR(&captured, &decoded)};
for (auto& handler : frame_info_handlers_)
handler(decoded_info);
});
}
bool VideoQualityAnalyzer::Active() const {
return !frame_info_handlers_.empty();
}
int64_t VideoQualityAnalyzer::DecodedFrameCaptureTimeOffsetMs(
const VideoFrame& decoded) const {
// Assumes that the underlying resolution is ms.
// Note that we intentinally allow wraparound. The code is incorrect for
// durations of more than UINT32_MAX/90 ms.
RTC_DCHECK(first_decode_rtp_timestamp_);
return (decoded.timestamp() - *first_decode_rtp_timestamp_) / 90;
}
int64_t VideoQualityAnalyzer::CapturedFrameCaptureTimeOffsetMs(
const VideoFrame& captured) const {
RTC_DCHECK(first_capture_ntp_time_ms_);
return captured.ntp_time_ms() - *first_capture_ntp_time_ms_;
}
void VideoQualityAnalyzer::PrintHeaders() {
fprintf(output_file_, "capt recv_capt render width height psnr\n");
}
void VideoQualityAnalyzer::PrintFrameInfo(const VideoFrameQualityInfo& sample) {
fprintf(output_file_, "%.3f %.3f %.3f %i %i %.3f\n",
sample.capture_time.seconds<double>(),
sample.received_capture_time.seconds<double>(),
sample.render_time.seconds<double>(), sample.width, sample.height,
sample.psnr);
}
void VideoQualityStats::HandleFrameInfo(VideoFrameQualityInfo sample) {
total++;
if (sample.render_time.IsInfinite()) {
++lost;
} else {
++valid;
end_to_end_seconds.AddSample(
(sample.render_time - sample.capture_time).seconds<double>());
psnr.AddSample(sample.psnr);
}
}
ForwardingCapturedFrameTap::ForwardingCapturedFrameTap(
const Clock* clock,
VideoQualityAnalyzer* analyzer,
rtc::VideoSourceInterface<VideoFrame>* source)
: clock_(clock), analyzer_(analyzer), source_(source) {}
ForwardingCapturedFrameTap::~ForwardingCapturedFrameTap() {}
void ForwardingCapturedFrameTap::OnFrame(const VideoFrame& frame) {
RTC_CHECK(sink_);
VideoFrame copy = frame;
if (frame.ntp_time_ms() == 0)
copy.set_ntp_time_ms(clock_->CurrentNtpInMilliseconds());
copy.set_timestamp(copy.ntp_time_ms() * 90);
analyzer_->OnCapturedFrame(copy);
sink_->OnFrame(copy);
}
void ForwardingCapturedFrameTap::OnDiscardedFrame() {
RTC_CHECK(sink_);
discarded_count_++;
sink_->OnDiscardedFrame();
}
void ForwardingCapturedFrameTap::AddOrUpdateSink(
VideoSinkInterface<VideoFrame>* sink,
const rtc::VideoSinkWants& wants) {
sink_ = sink;
source_->AddOrUpdateSink(this, wants);
}
void ForwardingCapturedFrameTap::RemoveSink(
VideoSinkInterface<VideoFrame>* sink) {
source_->RemoveSink(this);
sink_ = nullptr;
}
DecodedFrameTap::DecodedFrameTap(VideoQualityAnalyzer* analyzer)
: analyzer_(analyzer) {}
void DecodedFrameTap::OnFrame(const VideoFrame& frame) {
analyzer_->OnDecodedFrame(frame);
}
} // namespace test
} // namespace webrtc

View File

@ -0,0 +1,110 @@
/*
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#ifndef TEST_SCENARIO_QUALITY_STATS_H_
#define TEST_SCENARIO_QUALITY_STATS_H_
#include <deque>
#include <string>
#include <vector>
#include "absl/types/optional.h"
#include "api/units/timestamp.h"
#include "api/video/video_frame.h"
#include "api/video/video_sink_interface.h"
#include "api/video/video_source_interface.h"
#include "rtc_base/task_queue.h"
#include "rtc_base/timeutils.h"
#include "system_wrappers/include/clock.h"
#include "test/scenario/quality_info.h"
#include "test/scenario/scenario_config.h"
#include "test/statistics.h"
namespace webrtc {
namespace test {
class VideoQualityAnalyzer {
public:
VideoQualityAnalyzer(
std::string filename_or_empty,
std::function<void(const VideoFrameQualityInfo&)> frame_info_handler);
~VideoQualityAnalyzer();
void OnCapturedFrame(const VideoFrame& frame);
void OnDecodedFrame(const VideoFrame& frame);
void Synchronize();
bool Active() const;
const Clock* clock();
private:
int64_t DecodedFrameCaptureTimeOffsetMs(const VideoFrame& decoded) const;
int64_t CapturedFrameCaptureTimeOffsetMs(const VideoFrame& captured) const;
void PrintHeaders();
void PrintFrameInfo(const VideoFrameQualityInfo& sample);
std::vector<std::function<void(const VideoFrameQualityInfo&)>>
frame_info_handlers_;
std::deque<VideoFrame> captured_frames_;
absl::optional<int64_t> first_capture_ntp_time_ms_;
absl::optional<uint32_t> first_decode_rtp_timestamp_;
FILE* output_file_ = nullptr;
rtc::TaskQueue task_queue_;
};
struct VideoQualityStats {
int total = 0;
int valid = 0;
int lost = 0;
Statistics end_to_end_seconds;
Statistics frame_size;
Statistics psnr;
Statistics ssim;
void HandleFrameInfo(VideoFrameQualityInfo sample);
};
class ForwardingCapturedFrameTap
: public rtc::VideoSinkInterface<VideoFrame>,
public rtc::VideoSourceInterface<VideoFrame> {
public:
ForwardingCapturedFrameTap(const Clock* clock,
VideoQualityAnalyzer* analyzer,
rtc::VideoSourceInterface<VideoFrame>* source);
ForwardingCapturedFrameTap(ForwardingCapturedFrameTap&) = delete;
ForwardingCapturedFrameTap& operator=(ForwardingCapturedFrameTap&) = delete;
~ForwardingCapturedFrameTap();
// VideoSinkInterface interface
void OnFrame(const VideoFrame& frame) override;
void OnDiscardedFrame() override;
// VideoSourceInterface interface
void AddOrUpdateSink(VideoSinkInterface<VideoFrame>* sink,
const rtc::VideoSinkWants& wants) override;
void RemoveSink(VideoSinkInterface<VideoFrame>* sink) override;
VideoFrame PopFrame();
private:
const Clock* clock_;
VideoQualityAnalyzer* const analyzer_;
rtc::VideoSourceInterface<VideoFrame>* const source_;
VideoSinkInterface<VideoFrame>* sink_;
int discarded_count_ = 0;
};
class DecodedFrameTap : public rtc::VideoSinkInterface<VideoFrame> {
public:
explicit DecodedFrameTap(VideoQualityAnalyzer* analyzer);
// VideoSinkInterface interface
void OnFrame(const VideoFrame& frame) override;
private:
VideoQualityAnalyzer* const analyzer_;
};
} // namespace test
} // namespace webrtc
#endif // TEST_SCENARIO_QUALITY_STATS_H_

View File

@ -0,0 +1,62 @@
/*
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "test/gtest.h"
#include "test/scenario/scenario.h"
namespace webrtc {
namespace test {
namespace {
VideoStreamConfig AnalyzerVideoConfig(VideoQualityStats* stats) {
VideoStreamConfig config;
config.encoder.codec = VideoStreamConfig::Encoder::Codec::kVideoCodecVP8;
config.encoder.implementation =
VideoStreamConfig::Encoder::Implementation::kSoftware;
config.analyzer.frame_quality_handler = [stats](VideoFrameQualityInfo info) {
stats->HandleFrameInfo(info);
};
return config;
}
} // namespace
TEST(ScenarioAnalyzerTest, PsnrIsHighWhenNetworkIsGood) {
VideoQualityStats stats;
{
Scenario s;
NetworkNodeConfig good_network;
good_network.simulation.bandwidth = DataRate::kbps(1000);
auto route = s.CreateRoutes(s.CreateClient("caller", CallClientConfig()),
{s.CreateSimulationNode(good_network)},
s.CreateClient("callee", CallClientConfig()),
{s.CreateSimulationNode(NetworkNodeConfig())});
s.CreateVideoStream(route->forward(), AnalyzerVideoConfig(&stats));
s.RunFor(TimeDelta::seconds(1));
}
EXPECT_GT(stats.psnr.Mean(), 46);
}
TEST(ScenarioAnalyzerTest, PsnrIsLowWhenNetworkIsBad) {
VideoQualityStats stats;
{
Scenario s;
NetworkNodeConfig bad_network;
bad_network.simulation.bandwidth = DataRate::kbps(100);
bad_network.simulation.loss_rate = 0.02;
auto route = s.CreateRoutes(s.CreateClient("caller", CallClientConfig()),
{s.CreateSimulationNode(bad_network)},
s.CreateClient("callee", CallClientConfig()),
{s.CreateSimulationNode(NetworkNodeConfig())});
s.CreateVideoStream(route->forward(), AnalyzerVideoConfig(&stats));
s.RunFor(TimeDelta::seconds(2));
}
EXPECT_LT(stats.psnr.Mean(), 40);
}
} // namespace test
} // namespace webrtc

View File

@ -107,7 +107,8 @@ StatesPrinter* Scenario::CreatePrinter(std::string name,
CallClient* Scenario::CreateClient(std::string name, CallClientConfig config) {
RTC_DCHECK(real_time_mode_);
CallClient* client = new CallClient(clock_, GetFullPathOrEmpty(name), config);
CallClient* client =
new CallClient(clock_, name, GetFullPathOrEmpty(name), config);
if (config.transport.state_log_interval.IsFinite()) {
Every(config.transport.state_log_interval, [this, client]() {
client->network_controller_factory_.LogCongestionControllerStats(Now());
@ -266,8 +267,12 @@ VideoStreamPair* Scenario::CreateVideoStream(
VideoStreamPair* Scenario::CreateVideoStream(
std::pair<CallClient*, CallClient*> clients,
VideoStreamConfig config) {
video_streams_.emplace_back(
new VideoStreamPair(clients.first, clients.second, config));
std::string quality_log_file_name;
if (config.analyzer.log_to_file)
quality_log_file_name =
GetFullPathOrEmpty(clients.first->name_ + ".video_quality.txt");
video_streams_.emplace_back(new VideoStreamPair(
clients.first, clients.second, config, quality_log_file_name));
return video_streams_.back().get();
}

View File

@ -21,6 +21,7 @@
#include "api/units/time_delta.h"
#include "common_types.h" // NOLINT(build/include)
#include "test/frame_generator.h"
#include "test/scenario/quality_info.h"
namespace webrtc {
namespace test {
@ -138,6 +139,10 @@ struct VideoStreamConfig {
struct Renderer {
enum Type { kFake } type = kFake;
};
struct analyzer {
bool log_to_file = false;
std::function<void(const VideoFrameQualityInfo&)> frame_quality_handler;
} analyzer;
};
struct AudioStreamConfig {

View File

@ -12,6 +12,7 @@
#include <algorithm>
#include <utility>
#include "absl/memory/memory.h"
#include "api/test/video/function_video_encoder_factory.h"
#include "api/video/builtin_video_bitrate_allocator_factory.h"
#include "media/base/mediaconstants.h"
@ -175,7 +176,8 @@ VideoEncoderConfig CreateVideoEncoderConfig(VideoStreamConfig config) {
SendVideoStream::SendVideoStream(CallClient* sender,
VideoStreamConfig config,
Transport* send_transport)
Transport* send_transport,
VideoQualityAnalyzer* analyzer)
: sender_(sender), config_(config) {
for (size_t i = 0; i < config.encoder.num_simulcast_streams; ++i) {
ssrcs_.push_back(sender->GetNextVideoSsrc());
@ -244,10 +246,21 @@ SendVideoStream::SendVideoStream(CallClient* sender,
send_stream_ = sender_->call_->CreateVideoSendStream(
std::move(send_config), std::move(encoder_config));
std::vector<std::function<void(const VideoFrameQualityInfo&)> >
frame_info_handlers;
if (config.analyzer.frame_quality_handler)
frame_info_handlers.push_back(config.analyzer.frame_quality_handler);
if (analyzer->Active()) {
frame_tap_.reset(new ForwardingCapturedFrameTap(sender_->clock_, analyzer,
video_capturer_.get()));
send_stream_->SetSource(frame_tap_.get(),
config.encoder.degradation_preference);
} else {
send_stream_->SetSource(video_capturer_.get(),
config.encoder.degradation_preference);
}
}
SendVideoStream::~SendVideoStream() {
sender_->call_->DestroyVideoSendStream(send_stream_);
@ -282,7 +295,6 @@ void SendVideoStream::SetCaptureFramerate(int framerate) {
RTC_CHECK(frame_generator_)
<< "Framerate change only implemented for generators";
frame_generator_->ChangeFramerate(framerate);
}
VideoSendStream::Stats SendVideoStream::GetStats() const {
@ -311,9 +323,14 @@ ReceiveVideoStream::ReceiveVideoStream(CallClient* receiver,
VideoStreamConfig config,
SendVideoStream* send_stream,
size_t chosen_stream,
Transport* feedback_transport)
Transport* feedback_transport,
VideoQualityAnalyzer* analyzer)
: receiver_(receiver), config_(config) {
if (analyzer->Active()) {
renderer_ = absl::make_unique<DecodedFrameTap>(analyzer);
} else {
renderer_ = absl::make_unique<FakeVideoRenderer>();
}
VideoReceiveStream::Config recv_config(feedback_transport);
recv_config.rtp.remb = !config.stream.packet_feedback;
recv_config.rtp.transport_cc = config.stream.packet_feedback;
@ -384,14 +401,17 @@ VideoStreamPair::~VideoStreamPair() = default;
VideoStreamPair::VideoStreamPair(CallClient* sender,
CallClient* receiver,
VideoStreamConfig config)
VideoStreamConfig config,
std::string quality_log_file_name)
: config_(config),
send_stream_(sender, config, &sender->transport_),
analyzer_(quality_log_file_name, config.analyzer.frame_quality_handler),
send_stream_(sender, config, &sender->transport_, &analyzer_),
receive_stream_(receiver,
config,
&send_stream_,
/*chosen_stream=*/0,
&receiver->transport_) {}
&receiver->transport_,
&analyzer_) {}
} // namespace test
} // namespace webrtc

View File

@ -19,6 +19,7 @@
#include "test/scenario/call_client.h"
#include "test/scenario/column_printer.h"
#include "test/scenario/network_node.h"
#include "test/scenario/quality_stats.h"
#include "test/scenario/scenario_config.h"
#include "test/test_video_capturer.h"
@ -43,7 +44,8 @@ class SendVideoStream {
// Handles RTCP feedback for this stream.
SendVideoStream(CallClient* sender,
VideoStreamConfig config,
Transport* send_transport);
Transport* send_transport,
VideoQualityAnalyzer* analyzer);
rtc::CriticalSection crit_;
std::vector<uint32_t> ssrcs_;
@ -55,6 +57,7 @@ class SendVideoStream {
std::vector<test::FakeEncoder*> fake_encoders_ RTC_GUARDED_BY(crit_);
std::unique_ptr<VideoBitrateAllocatorFactory> bitrate_allocator_factory_;
std::unique_ptr<TestVideoCapturer> video_capturer_;
std::unique_ptr<ForwardingCapturedFrameTap> frame_tap_;
FrameGeneratorCapturer* frame_generator_ = nullptr;
int next_local_network_id_ = 0;
int next_remote_network_id_ = 0;
@ -74,11 +77,12 @@ class ReceiveVideoStream {
VideoStreamConfig config,
SendVideoStream* send_stream,
size_t chosen_stream,
Transport* feedback_transport);
Transport* feedback_transport,
VideoQualityAnalyzer* analyzer);
VideoReceiveStream* receive_stream_ = nullptr;
FlexfecReceiveStream* flecfec_stream_ = nullptr;
std::unique_ptr<rtc::VideoSinkInterface<webrtc::VideoFrame>> renderer_;
std::unique_ptr<rtc::VideoSinkInterface<VideoFrame>> renderer_;
CallClient* const receiver_;
const VideoStreamConfig config_;
std::unique_ptr<VideoDecoderFactory> decoder_factory_;
@ -93,15 +97,18 @@ class VideoStreamPair {
~VideoStreamPair();
SendVideoStream* send() { return &send_stream_; }
ReceiveVideoStream* receive() { return &receive_stream_; }
VideoQualityAnalyzer* analyzer() { return &analyzer_; }
private:
friend class Scenario;
VideoStreamPair(CallClient* sender,
CallClient* receiver,
VideoStreamConfig config);
VideoStreamConfig config,
std::string quality_log_file_name);
const VideoStreamConfig config_;
VideoQualityAnalyzer analyzer_;
SendVideoStream send_stream_;
ReceiveVideoStream receive_stream_;
};