diff --git a/call/BUILD.gn b/call/BUILD.gn index 388ff0608d..85d9cb87b3 100644 --- a/call/BUILD.gn +++ b/call/BUILD.gn @@ -433,6 +433,7 @@ if (rtc_include_tests) { "../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 8190eea5f3..7935fac389 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 #include #include + #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 @@ TEST(RtpVideoSenderTest, DoesNotRetrasmitAckedPackets) { 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 b13c6a9b3f..f21b0eb1e6 100644 --- a/test/network/network_emulation.cc +++ b/test/network/network_emulation.cc @@ -91,6 +91,10 @@ void NetworkRouterNode::OnPacketReceived(EmulatedIpPacket packet) { 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::SetWatcher( }); } +void NetworkRouterNode::SetFilter( + std::function 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 a37954ee17..b5e8164be1 100644 --- a/test/network/network_emulation.h +++ b/test/network/network_emulation.h @@ -73,6 +73,7 @@ class NetworkRouterNode : public EmulatedNetworkReceiverInterface { EmulatedNetworkReceiverInterface* receiver); void RemoveReceiver(const rtc::IPAddress& dest_ip); void SetWatcher(std::function watcher); + void SetFilter(std::function filter); private: rtc::TaskQueue* const task_queue_; @@ -80,6 +81,8 @@ class NetworkRouterNode : public EmulatedNetworkReceiverInterface { RTC_GUARDED_BY(task_queue_); std::function watcher_ RTC_GUARDED_BY(task_queue_); + std::function 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 29a9cea104..ad382bdb4c 100644 --- a/test/scenario/scenario.cc +++ b/test/scenario/scenario.cc @@ -60,6 +60,10 @@ Scenario::Scenario() : Scenario(std::unique_ptr(), /*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::Every(TimeDelta interval, std::function function) { }); } +void Scenario::Post(std::function function) { + task_queue_.PostTask(function); +} + void Scenario::At(TimeDelta offset, std::function 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 b8b56d8a54..a4dc47108a 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 @@ namespace test { 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 log_writer_manager, @@ -100,6 +102,10 @@ class Scenario { void Every(TimeDelta interval, std::function function); void Every(TimeDelta interval, std::function 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 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 370b225112..def6c2051f 100644 --- a/test/scenario/video_stream.cc +++ b/test/scenario/video_stream.cc @@ -486,6 +486,22 @@ void SendVideoStream::UpdateActiveLayers(std::vector active_layers) { }); } +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 @@ ReceiveVideoStream::ReceiveVideoStream(CallClient* receiver, 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( []() { return std::make_unique(); }); } else { diff --git a/test/scenario/video_stream.h b/test/scenario/video_stream.h index ef98679963..f0b99db57a 100644 --- a/test/scenario/video_stream.h +++ b/test/scenario/video_stream.h @@ -40,6 +40,8 @@ class SendVideoStream { void Stop(); void UpdateConfig(std::function modifier); void UpdateActiveLayers(std::vector active_layers); + bool UsingSsrc(uint32_t ssrc) const; + bool UsingRtxSsrc(uint32_t ssrc) const; private: friend class Scenario;