Adds scenario test for transport wide feedback based retransmission.
This ensures more end to end test coverage of the feature and captures
a wider class of regression then the existing unit test.
Bug: webrtc:9883
Change-Id: I6e74e571500c5c5d74caf8f661cac08bee8934f6
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/164461
Commit-Queue: Sebastian Jansson <srte@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#30252}
diff --git a/call/BUILD.gn b/call/BUILD.gn
index 388ff06..85d9cb8 100644
--- a/call/BUILD.gn
+++ b/call/BUILD.gn
@@ -433,6 +433,7 @@
"../test:video_test_common",
"../test/time_controller:time_controller",
"../video",
+ "//test/scenario:scenario",
"//testing/gmock",
"//testing/gtest",
"//third_party/abseil-cpp/absl/container:inlined_vector",
diff --git a/call/rtp_video_sender_unittest.cc b/call/rtp_video_sender_unittest.cc
index 8190eea..7935fac 100644
--- a/call/rtp_video_sender_unittest.cc
+++ b/call/rtp_video_sender_unittest.cc
@@ -10,8 +10,10 @@
#include "call/rtp_video_sender.h"
+#include <atomic>
#include <memory>
#include <string>
+
#include "call/rtp_transport_controller_send.h"
#include "modules/rtp_rtcp/include/rtp_rtcp_defines.h"
#include "modules/rtp_rtcp/source/byte_io.h"
@@ -25,6 +27,7 @@
#include "test/gmock.h"
#include "test/gtest.h"
#include "test/mock_transport.h"
+#include "test/scenario/scenario.h"
#include "test/time_controller/simulated_time_controller.h"
#include "video/call_stats.h"
#include "video/send_delay_stats.h"
@@ -505,6 +508,62 @@
ASSERT_TRUE(event.Wait(kTimeoutMs));
}
+// This tests that we utilize transport wide feedback to retransmit lost
+// packets. This is tested by dropping all ordirary packets from a "lossy"
+// stream send along with an secondary untouched stream. The transport wide
+// feedback packets from the secondary stream allows the sending side to
+// detect and retreansmit the lost packets from the lossy stream.
+TEST(RtpVideoSenderTest, RetransmitsOnTransportWideLossInfo) {
+ int rtx_packets;
+ test::Scenario s(test_info_);
+ test::CallClientConfig call_conf;
+ // Keeping the bitrate fixed to avoid RTX due to probing.
+ call_conf.transport.rates.max_rate = DataRate::kbps(300);
+ call_conf.transport.rates.start_rate = DataRate::kbps(300);
+ test::NetworkSimulationConfig net_conf;
+ net_conf.bandwidth = DataRate::kbps(300);
+ auto send_node = s.CreateSimulationNode(net_conf);
+ auto* route = s.CreateRoutes(s.CreateClient("send", call_conf), {send_node},
+ s.CreateClient("return", call_conf),
+ {s.CreateSimulationNode(net_conf)});
+
+ test::VideoStreamConfig lossy_config;
+ lossy_config.source.framerate = 5;
+ auto* lossy = s.CreateVideoStream(route->forward(), lossy_config);
+ // The secondary stream acts a driver for transport feedback messages,
+ // ensuring that lost packets on the lossy stream are retransmitted.
+ s.CreateVideoStream(route->forward(), test::VideoStreamConfig());
+
+ send_node->router()->SetFilter([&](const EmulatedIpPacket& packet) {
+ RtpPacket rtp;
+ if (rtp.Parse(packet.data)) {
+ // Drops all regular packets for the lossy stream and counts all RTX
+ // packets. Since no packets are let trough, NACKs can't be triggered
+ // by the receiving side.
+ if (lossy->send()->UsingSsrc(rtp.Ssrc())) {
+ return false;
+ } else if (lossy->send()->UsingRtxSsrc(rtp.Ssrc())) {
+ ++rtx_packets;
+ }
+ }
+ return true;
+ });
+
+ // Run for a short duration and reset counters to avoid counting RTX packets
+ // from initial probing.
+ s.RunFor(TimeDelta::seconds(1));
+ rtx_packets = 0;
+ int decoded_baseline = lossy->receive()->GetStats().frames_decoded;
+ s.RunFor(TimeDelta::seconds(1));
+ // We expect both that RTX packets were sent and that an appropriate number of
+ // frames were received. This is somewhat redundant but reduces the risk of
+ // false positives in future regressions (e.g. RTX is send due to probing).
+ EXPECT_GE(rtx_packets, 1);
+ int frames_decoded =
+ lossy->receive()->GetStats().frames_decoded - decoded_baseline;
+ EXPECT_EQ(frames_decoded, 5);
+}
+
// Integration test verifying that retransmissions are sent for packets which
// can be detected as lost early, using transport wide feedback.
TEST(RtpVideoSenderTest, EarlyRetransmits) {
diff --git a/test/network/network_emulation.cc b/test/network/network_emulation.cc
index b13c6a9..f21b0eb 100644
--- a/test/network/network_emulation.cc
+++ b/test/network/network_emulation.cc
@@ -91,6 +91,10 @@
if (watcher_) {
watcher_(packet);
}
+ if (filter_) {
+ if (!filter_(packet))
+ return;
+ }
auto receiver_it = routing_.find(packet.to.ipaddr());
if (receiver_it == routing_.end()) {
return;
@@ -125,6 +129,14 @@
});
}
+void NetworkRouterNode::SetFilter(
+ std::function<bool(const EmulatedIpPacket&)> filter) {
+ task_queue_->PostTask([=] {
+ RTC_DCHECK_RUN_ON(task_queue_);
+ filter_ = filter;
+ });
+}
+
EmulatedNetworkNode::EmulatedNetworkNode(
Clock* clock,
rtc::TaskQueue* task_queue,
diff --git a/test/network/network_emulation.h b/test/network/network_emulation.h
index a37954e..b5e8164 100644
--- a/test/network/network_emulation.h
+++ b/test/network/network_emulation.h
@@ -73,6 +73,7 @@
EmulatedNetworkReceiverInterface* receiver);
void RemoveReceiver(const rtc::IPAddress& dest_ip);
void SetWatcher(std::function<void(const EmulatedIpPacket&)> watcher);
+ void SetFilter(std::function<bool(const EmulatedIpPacket&)> filter);
private:
rtc::TaskQueue* const task_queue_;
@@ -80,6 +81,8 @@
RTC_GUARDED_BY(task_queue_);
std::function<void(const EmulatedIpPacket&)> watcher_
RTC_GUARDED_BY(task_queue_);
+ std::function<bool(const EmulatedIpPacket&)> filter_
+ RTC_GUARDED_BY(task_queue_);
};
// Represents node in the emulated network. Nodes can be connected with each
diff --git a/test/scenario/scenario.cc b/test/scenario/scenario.cc
index 29a9cea..ad382bd 100644
--- a/test/scenario/scenario.cc
+++ b/test/scenario/scenario.cc
@@ -60,6 +60,10 @@
: Scenario(std::unique_ptr<LogWriterFactoryInterface>(),
/*real_time=*/false) {}
+Scenario::Scenario(const testing::TestInfo* test_info)
+ : Scenario(std::string(test_info->test_suite_name()) + "/" +
+ test_info->name()) {}
+
Scenario::Scenario(std::string file_name)
: Scenario(file_name, /*real_time=*/false) {}
@@ -264,6 +268,10 @@
});
}
+void Scenario::Post(std::function<void()> function) {
+ task_queue_.PostTask(function);
+}
+
void Scenario::At(TimeDelta offset, std::function<void()> function) {
RTC_DCHECK_GT(offset, TimeSinceStart());
task_queue_.PostDelayedTask(function, TimeUntilTarget(offset).ms());
diff --git a/test/scenario/scenario.h b/test/scenario/scenario.h
index b8b56d8..a4dc471 100644
--- a/test/scenario/scenario.h
+++ b/test/scenario/scenario.h
@@ -19,6 +19,7 @@
#include "rtc_base/fake_clock.h"
#include "rtc_base/task_queue.h"
#include "rtc_base/task_utils/repeating_task.h"
+#include "test/gtest.h"
#include "test/logging/log_writer.h"
#include "test/network/network_emulation_manager.h"
#include "test/scenario/audio_stream.h"
@@ -41,6 +42,7 @@
class Scenario {
public:
Scenario();
+ explicit Scenario(const testing::TestInfo* test_info);
explicit Scenario(std::string file_name);
Scenario(std::string file_name, bool real_time);
Scenario(std::unique_ptr<LogWriterFactoryInterface> log_writer_manager,
@@ -100,6 +102,10 @@
void Every(TimeDelta interval, std::function<void(TimeDelta)> function);
void Every(TimeDelta interval, std::function<void()> function);
+ // Runs the provided function on the internal task queue. This ensure that
+ // it's run on the main thread for simulated time tests.
+ void Post(std::function<void()> function);
+
// Runs the provided function after given duration has passed. For real time
// tests, |function| is called after |target_time_since_start| from the call
// to Every().
diff --git a/test/scenario/video_stream.cc b/test/scenario/video_stream.cc
index 370b225..def6c20 100644
--- a/test/scenario/video_stream.cc
+++ b/test/scenario/video_stream.cc
@@ -486,6 +486,22 @@
});
}
+bool SendVideoStream::UsingSsrc(uint32_t ssrc) const {
+ for (uint32_t owned : ssrcs_) {
+ if (owned == ssrc)
+ return true;
+ }
+ return false;
+}
+
+bool SendVideoStream::UsingRtxSsrc(uint32_t ssrc) const {
+ for (uint32_t owned : rtx_ssrcs_) {
+ if (owned == ssrc)
+ return true;
+ }
+ return false;
+}
+
void SendVideoStream::SetCaptureFramerate(int framerate) {
sender_->SendTask([&] { video_capturer_->ChangeFramerate(framerate); });
}
@@ -520,7 +536,8 @@
VideoFrameMatcher* matcher)
: receiver_(receiver), config_(config) {
if (config.encoder.codec ==
- VideoStreamConfig::Encoder::Codec::kVideoCodecGeneric) {
+ VideoStreamConfig::Encoder::Codec::kVideoCodecGeneric ||
+ config.encoder.implementation == VideoStreamConfig::Encoder::kFake) {
decoder_factory_ = std::make_unique<FunctionVideoDecoderFactory>(
[]() { return std::make_unique<FakeDecoder>(); });
} else {
diff --git a/test/scenario/video_stream.h b/test/scenario/video_stream.h
index ef98679..f0b99db 100644
--- a/test/scenario/video_stream.h
+++ b/test/scenario/video_stream.h
@@ -40,6 +40,8 @@
void Stop();
void UpdateConfig(std::function<void(VideoStreamConfig*)> modifier);
void UpdateActiveLayers(std::vector<bool> active_layers);
+ bool UsingSsrc(uint32_t ssrc) const;
+ bool UsingRtxSsrc(uint32_t ssrc) const;
private:
friend class Scenario;