/* * Copyright (c) 2024 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 #include #include "absl/strings/str_cat.h" #include "api/jsep.h" #include "api/make_ref_counted.h" #include "api/scoped_refptr.h" #include "api/stats/rtc_stats_report.h" #include "api/stats/rtcstats_objects.h" #include "api/test/network_emulation/dual_pi2_network_queue.h" #include "api/test/network_emulation/network_emulation_interfaces.h" #include "api/transport/ecn_marking.h" #include "api/units/data_rate.h" #include "api/units/time_delta.h" #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" #include "modules/rtp_rtcp/source/rtcp_packet/congestion_control_feedback.h" #include "modules/rtp_rtcp/source/rtcp_packet/rtpfb.h" #include "modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h" #include "modules/rtp_rtcp/source/rtp_util.h" #include "pc/test/mock_peer_connection_observers.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" #include "rtc_base/network_constants.h" #include "test/create_frame_generator_capturer.h" #include "test/gmock.h" #include "test/gtest.h" #include "test/peer_scenario/peer_scenario.h" #include "test/peer_scenario/peer_scenario_client.h" namespace webrtc { namespace { using test::PeerScenario; using test::PeerScenarioClient; using ::testing::HasSubstr; // Helper class used for counting RTCP feedback messages. class RtcpFeedbackCounter { public: void Count(const EmulatedIpPacket& packet) { if (!IsRtcpPacket(packet.data)) { return; } rtcp::CommonHeader header; ASSERT_TRUE(header.Parse(packet.data.cdata(), packet.data.size())); if (header.type() != rtcp::Rtpfb::kPacketType) { return; } if (header.fmt() == rtcp::CongestionControlFeedback::kFeedbackMessageType) { ++congestion_control_feedback_; rtcp::CongestionControlFeedback fb; ASSERT_TRUE(fb.Parse(header)); for (const rtcp::CongestionControlFeedback::PacketInfo& info : fb.packets()) { switch (info.ecn) { case EcnMarking::kNotEct: ++not_ect_; break; case EcnMarking::kEct0: // Not used. RTC_CHECK_NOTREACHED(); break; case EcnMarking::kEct1: // ECN-Capable Transport ++ect1_; break; case EcnMarking::kCe: ++ce_; } } } if (header.fmt() == rtcp::TransportFeedback::kFeedbackMessageType) { ++transport_sequence_number_feedback_; } } int FeedbackAccordingToRfc8888() const { return congestion_control_feedback_; } int FeedbackAccordingToTransportCc() const { return transport_sequence_number_feedback_; } int not_ect() const { return not_ect_; } int ect1() const { return ect1_; } int ce() const { return ce_; } private: int congestion_control_feedback_ = 0; int transport_sequence_number_feedback_ = 0; int not_ect_ = 0; int ect1_ = 0; int ce_ = 0; }; scoped_refptr GetStatsAndProcess( PeerScenario& s, PeerScenarioClient* client) { auto stats_collector = make_ref_counted(); client->pc()->GetStats(stats_collector.get()); s.ProcessMessages(TimeDelta::Millis(0)); RTC_CHECK(stats_collector->called()); return stats_collector->report(); } DataRate GetAvailableSendBitrate( const scoped_refptr& report) { auto stats = report->GetStatsOfType(); if (stats.empty()) { return DataRate::Zero(); } return DataRate::BitsPerSec(*stats[0]->available_outgoing_bitrate); } TEST(L4STest, NegotiateAndUseCcfbIfEnabled) { PeerScenario s(*test_info_); PeerScenarioClient::Config config; config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback", "Enabled"); config.disable_encryption = true; PeerScenarioClient* caller = s.CreateClient(config); PeerScenarioClient* callee = s.CreateClient(config); // Create network path from caller to callee. auto send_node = s.net()->NodeBuilder().Build().node; auto ret_node = s.net()->NodeBuilder().Build().node; s.net()->CreateRoute(caller->endpoint(), {send_node}, callee->endpoint()); s.net()->CreateRoute(callee->endpoint(), {ret_node}, caller->endpoint()); RtcpFeedbackCounter send_node_feedback_counter; send_node->router()->SetWatcher([&](const EmulatedIpPacket& packet) { send_node_feedback_counter.Count(packet); }); RtcpFeedbackCounter ret_node_feedback_counter; ret_node->router()->SetWatcher([&](const EmulatedIpPacket& packet) { ret_node_feedback_counter.Count(packet); }); auto signaling = s.ConnectSignaling(caller, callee, {send_node}, {ret_node}); PeerScenarioClient::VideoSendTrackConfig video_conf; video_conf.generator.squares_video->framerate = 15; caller->CreateVideo("VIDEO_1", video_conf); callee->CreateVideo("VIDEO_2", video_conf); signaling.StartIceSignaling(); std::atomic offer_exchange_done(false); signaling.NegotiateSdp( [&](SessionDescriptionInterface* offer) { std::string offer_str = absl::StrCat(*offer); // Check that the offer contain both congestion control feedback // accoring to RFC 8888, and transport-cc and the header extension // http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 EXPECT_THAT(offer_str, HasSubstr("a=rtcp-fb:* ack ccfb\r\n")); EXPECT_THAT(offer_str, HasSubstr("transport-cc")); EXPECT_THAT( offer_str, HasSubstr("http://www.ietf.org/id/" "draft-holmer-rmcat-transport-wide-cc-extensions")); }, [&](const SessionDescriptionInterface& answer) { std::string answer_str = absl::StrCat(answer); EXPECT_THAT(answer_str, HasSubstr("a=rtcp-fb:* ack ccfb\r\n")); // Check that the answer does not contain transport-cc nor the // header extension // http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 EXPECT_THAT(answer_str, Not(HasSubstr("transport-cc"))); EXPECT_THAT( answer_str, Not(HasSubstr(" http://www.ietf.org/id/" "draft-holmer-rmcat-transport-wide-cc-extensions-"))); offer_exchange_done = true; }); // Wait for SDP negotiation and the packet filter to be setup. s.WaitAndProcess(&offer_exchange_done); s.ProcessMessages(TimeDelta::Seconds(2)); EXPECT_GT(send_node_feedback_counter.FeedbackAccordingToRfc8888(), 0); EXPECT_EQ(send_node_feedback_counter.FeedbackAccordingToTransportCc(), 0); EXPECT_GT(ret_node_feedback_counter.FeedbackAccordingToRfc8888(), 0); EXPECT_EQ(ret_node_feedback_counter.FeedbackAccordingToTransportCc(), 0); } TEST(L4STest, CallerAdaptToLinkCapacityOnNetworkWithoutEcn) { PeerScenario s(*test_info_); PeerScenarioClient::Config config; config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback", "Enabled"); PeerScenarioClient* caller = s.CreateClient(config); PeerScenarioClient* callee = s.CreateClient(config); auto caller_to_callee = s.net() ->NodeBuilder() .capacity(DataRate::KilobitsPerSec(600)) .Build() .node; auto callee_to_caller = s.net()->NodeBuilder().Build().node; s.net()->CreateRoute(caller->endpoint(), {caller_to_callee}, callee->endpoint()); s.net()->CreateRoute(callee->endpoint(), {callee_to_caller}, caller->endpoint()); auto signaling = s.ConnectSignaling(caller, callee, {caller_to_callee}, {callee_to_caller}); PeerScenarioClient::VideoSendTrackConfig video_conf; video_conf.generator.squares_video->framerate = 30; video_conf.generator.squares_video->width = 640; video_conf.generator.squares_video->height = 360; caller->CreateVideo("VIDEO_1", video_conf); signaling.StartIceSignaling(); std::atomic offer_exchange_done(false); signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { offer_exchange_done = true; }); s.WaitAndProcess(&offer_exchange_done); s.ProcessMessages(TimeDelta::Seconds(3)); DataRate available_bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); EXPECT_GT(available_bwe.kbps(), 450); EXPECT_LT(available_bwe.kbps(), 610); } // Note - this test only test that the // caller adapt to the link capacity. It does not test that the caller uses ECN // to adapt even though the network can mark packets with CE. // TODO: bugs.webrtc.org/42225697 - actually test that the caller adapt to ECN // marking. TEST(L4STest, CallerAdaptToLinkCapacityOnNetworkWithEcn) { PeerScenario s(*test_info_); PeerScenarioClient::Config config; config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback", "Enabled"); PeerScenarioClient* caller = s.CreateClient(config); PeerScenarioClient* callee = s.CreateClient(config); DualPi2NetworkQueueFactory dual_pi_factory({}); auto caller_to_callee = s.net() ->NodeBuilder() .queue_factory(dual_pi_factory) .capacity(DataRate::KilobitsPerSec(600)) .Build() .node; auto callee_to_caller = s.net()->NodeBuilder().Build().node; s.net()->CreateRoute(caller->endpoint(), {caller_to_callee}, callee->endpoint()); s.net()->CreateRoute(callee->endpoint(), {callee_to_caller}, caller->endpoint()); auto signaling = s.ConnectSignaling(caller, callee, {caller_to_callee}, {callee_to_caller}); PeerScenarioClient::VideoSendTrackConfig video_conf; video_conf.generator.squares_video->framerate = 30; video_conf.generator.squares_video->width = 640; video_conf.generator.squares_video->height = 360; caller->CreateVideo("VIDEO_1", video_conf); signaling.StartIceSignaling(); std::atomic offer_exchange_done(false); signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { offer_exchange_done = true; }); s.WaitAndProcess(&offer_exchange_done); s.ProcessMessages(TimeDelta::Seconds(3)); DataRate available_bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); EXPECT_GT(available_bwe.kbps(), 450); EXPECT_LT(available_bwe.kbps(), 610); } TEST(L4STest, SendsEct1UntilFirstFeedback) { PeerScenario s(*test_info_); PeerScenarioClient::Config config; config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback", "Enabled"); config.disable_encryption = true; PeerScenarioClient* caller = s.CreateClient(config); PeerScenarioClient* callee = s.CreateClient(config); // Create network path from caller to callee. auto caller_to_callee = s.net()->NodeBuilder().Build().node; auto callee_to_caller = s.net()->NodeBuilder().Build().node; s.net()->CreateRoute(caller->endpoint(), {caller_to_callee}, callee->endpoint()); s.net()->CreateRoute(callee->endpoint(), {callee_to_caller}, caller->endpoint()); RtcpFeedbackCounter feedback_counter; std::atomic seen_ect1_feedback = false; std::atomic seen_not_ect_feedback = false; callee_to_caller->router()->SetWatcher([&](const EmulatedIpPacket& packet) { feedback_counter.Count(packet); if (feedback_counter.ect1() > 0) { seen_ect1_feedback = true; RTC_LOG(LS_INFO) << " ect 1" << feedback_counter.ect1(); } if (feedback_counter.not_ect() > 0) { seen_not_ect_feedback = true; RTC_LOG(LS_INFO) << " not ect" << feedback_counter.not_ect(); } }); auto signaling = s.ConnectSignaling(caller, callee, {caller_to_callee}, {callee_to_caller}); PeerScenarioClient::VideoSendTrackConfig video_conf; video_conf.generator.squares_video->framerate = 15; caller->CreateVideo("VIDEO_1", video_conf); signaling.StartIceSignaling(); std::atomic offer_exchange_done(false); signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { offer_exchange_done = true; }); s.WaitAndProcess(&offer_exchange_done); // Wait for first feedback where packets have been sent with ECT(1). Then // feedback for packets sent as not ECT since currently webrtc does not // implement adaptation to ECN. EXPECT_TRUE(s.WaitAndProcess(&seen_ect1_feedback, TimeDelta::Seconds(1))); EXPECT_FALSE(seen_not_ect_feedback); EXPECT_TRUE(s.WaitAndProcess(&seen_not_ect_feedback, TimeDelta::Seconds(1))); } TEST(L4STest, SendsEct1AfterRouteChange) { PeerScenario s(*test_info_); PeerScenarioClient::Config config; config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback", "Enabled"); config.disable_encryption = true; config.endpoints = {{0, {.type = AdapterType::ADAPTER_TYPE_WIFI}}}; PeerScenarioClient* caller = s.CreateClient(config); // Callee has booth wifi and cellular adapters. config.endpoints = {{0, {.type = AdapterType::ADAPTER_TYPE_WIFI}}, {1, {.type = AdapterType::ADAPTER_TYPE_CELLULAR}}}; PeerScenarioClient* callee = s.CreateClient(config); // Create network path from caller to callee. auto caller_to_callee = s.net()->NodeBuilder().Build().node; auto callee_to_caller_wifi = s.net()->NodeBuilder().Build().node; auto callee_to_caller_cellular = s.net()->NodeBuilder().Build().node; s.net()->CreateRoute(caller->endpoint(0), {caller_to_callee}, callee->endpoint(0)); s.net()->CreateRoute(caller->endpoint(0), {caller_to_callee}, callee->endpoint(1)); s.net()->CreateRoute(callee->endpoint(0), {callee_to_caller_wifi}, caller->endpoint(0)); s.net()->CreateRoute(callee->endpoint(1), {callee_to_caller_cellular}, caller->endpoint(0)); RtcpFeedbackCounter wifi_feedback_counter; std::atomic seen_ect1_on_wifi_feedback = false; std::atomic seen_not_ect_on_wifi_feedback = false; callee_to_caller_wifi->router()->SetWatcher( [&](const EmulatedIpPacket& packet) { wifi_feedback_counter.Count(packet); if (wifi_feedback_counter.ect1() > 0) { seen_ect1_on_wifi_feedback = true; RTC_LOG(LS_INFO) << " ect 1 feedback on wifi: " << wifi_feedback_counter.ect1(); } if (wifi_feedback_counter.not_ect() > 0) { seen_not_ect_on_wifi_feedback = true; RTC_LOG(LS_INFO) << " not ect feedback on wifi: " << wifi_feedback_counter.not_ect(); } }); auto signaling = s.ConnectSignaling(caller, callee, {caller_to_callee}, {callee_to_caller_wifi}); PeerScenarioClient::VideoSendTrackConfig video_conf; video_conf.generator.squares_video->framerate = 15; caller->CreateVideo("VIDEO_1", video_conf); signaling.StartIceSignaling(); std::atomic offer_exchange_done(false); signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { offer_exchange_done = true; }); s.WaitAndProcess(&offer_exchange_done); // Wait for first feedback where packets have been sent with ECT(1). Then // feedback for packets sent as not ECT since currently webrtc does not // implement adaptation to ECN. EXPECT_TRUE( s.WaitAndProcess(&seen_ect1_on_wifi_feedback, TimeDelta::Seconds(1))); EXPECT_FALSE(seen_not_ect_on_wifi_feedback); EXPECT_TRUE( s.WaitAndProcess(&seen_not_ect_on_wifi_feedback, TimeDelta::Seconds(1))); RtcpFeedbackCounter cellular_feedback_counter; std::atomic seen_ect1_on_cellular_feedback = false; callee_to_caller_cellular->router()->SetWatcher( [&](const EmulatedIpPacket& packet) { cellular_feedback_counter.Count(packet); if (cellular_feedback_counter.ect1() > 0) { seen_ect1_on_cellular_feedback = true; RTC_LOG(LS_INFO) << " ect 1 feedback on cellular: " << cellular_feedback_counter.ect1(); } }); // Disable callees wifi and expect that the connection switch to cellular and // sends packets with ECT(1) again. s.net()->DisableEndpoint(callee->endpoint(0)); EXPECT_TRUE( s.WaitAndProcess(&seen_ect1_on_cellular_feedback, TimeDelta::Seconds(5))); } } // namespace } // namespace webrtc