| /* |
| * Copyright (c) 2023 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 <atomic> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include "api/jsep.h" |
| #include "api/media_types.h" |
| #include "api/rtp_parameters.h" |
| #include "api/rtp_sender_interface.h" |
| #include "api/rtp_transceiver_direction.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_config_schedule.pb.h" |
| #include "api/test/network_emulation/network_emulation_interfaces.h" |
| #include "api/test/network_emulation/network_queue.h" |
| #include "api/test/network_emulation/schedulable_network_node_builder.h" |
| #include "api/transport/bitrate_settings.h" |
| #include "api/units/data_rate.h" |
| #include "api/units/time_delta.h" |
| #include "modules/rtp_rtcp/include/rtp_header_extension_map.h" |
| #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" |
| #include "modules/rtp_rtcp/source/rtp_header_extensions.h" |
| #include "modules/rtp_rtcp/source/rtp_util.h" |
| #include "pc/media_session.h" |
| #include "test/create_frame_generator_capturer.h" |
| #include "test/gmock.h" |
| #include "test/gtest.h" |
| #include "test/peer_scenario/bwe_integration_tests/stats_utilities.h" |
| #include "test/peer_scenario/peer_scenario.h" |
| #include "test/peer_scenario/peer_scenario_client.h" |
| |
| namespace webrtc { |
| namespace test { |
| |
| using ::testing::SizeIs; |
| using ::testing::Test; |
| using ::testing::ValuesIn; |
| using ::testing::WithParamInterface; |
| |
| TEST(BweRampupTest, BweRampUpWhenCapacityIncrease) { |
| PeerScenario s(*test_info_); |
| |
| PeerScenarioClient* caller = s.CreateClient({}); |
| PeerScenarioClient* callee = s.CreateClient({}); |
| |
| network_behaviour::NetworkConfigSchedule schedule; |
| auto initial_config = schedule.add_item(); |
| initial_config->set_link_capacity_kbps(500); |
| auto updated_capacity = schedule.add_item(); |
| updated_capacity->set_time_since_first_sent_packet_ms(3000); |
| updated_capacity->set_link_capacity_kbps(3000); |
| SchedulableNetworkNodeBuilder schedulable_builder(*s.net(), |
| std::move(schedule)); |
| |
| auto caller_node = schedulable_builder.Build(/*random_seed=*/1); |
| auto callee_node = s.net()->NodeBuilder().capacity_kbps(5000).Build().node; |
| s.net()->CreateRoute(caller->endpoint(), {caller_node}, callee->endpoint()); |
| s.net()->CreateRoute(callee->endpoint(), {callee_node}, caller->endpoint()); |
| |
| FrameGeneratorCapturerConfig::SquaresVideo video_resolution = { |
| .framerate = 30, .width = 1280, .height = 720}; |
| PeerScenarioClient::VideoSendTrack track = caller->CreateVideo( |
| "VIDEO", {.generator = {.squares_video = video_resolution}}); |
| |
| auto signaling = |
| s.ConnectSignaling(caller, callee, {caller_node}, {callee_node}); |
| |
| signaling.StartIceSignaling(); |
| |
| std::atomic<bool> offer_exchange_done(false); |
| signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { |
| offer_exchange_done = true; |
| }); |
| // Wait for SDP negotiation. |
| s.WaitAndProcess(&offer_exchange_done); |
| |
| // Capacity increases 3s after the first sent packet. Sample sooner so this |
| // check still reflects the initial 500 kbps link. |
| s.ProcessMessages(TimeDelta::Seconds(2)); |
| DataRate bwe_before_capacity_increase = |
| GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| EXPECT_GT(bwe_before_capacity_increase.kbps(), 300); |
| EXPECT_LT(bwe_before_capacity_increase.kbps(), 650); |
| s.ProcessMessages(TimeDelta::Seconds(15)); |
| EXPECT_GT(GetAvailableSendBitrate(GetStatsAndProcess(s, caller)).kbps(), |
| 1000); |
| } |
| |
| // Test that caller BWE can rampup even if callee can not demux incoming RTP |
| // packets. |
| TEST(BweRampupTest, RampUpWithUndemuxableRtpPackets) { |
| PeerScenario s(*test_info_); |
| |
| PeerScenarioClient::Config config = PeerScenarioClient::Config(); |
| config.disable_encryption = true; |
| PeerScenarioClient* caller = s.CreateClient(config); |
| PeerScenarioClient* callee = s.CreateClient(config); |
| |
| 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()); |
| |
| auto signaling = s.ConnectSignaling(caller, callee, {send_node}, {ret_node}); |
| PeerScenarioClient::VideoSendTrackConfig video_conf; |
| video_conf.generator.squares_video->framerate = 15; |
| |
| PeerScenarioClient::VideoSendTrack track = |
| caller->CreateVideo("VIDEO", video_conf); |
| |
| signaling.StartIceSignaling(); |
| |
| std::atomic<bool> offer_exchange_done(false); |
| signaling.NegotiateSdp( |
| [&](SessionDescriptionInterface* offer) { |
| RtpHeaderExtensionMap extension_map( |
| GetFirstVideoContentDescription(offer->description()) |
| ->rtp_header_extensions()); |
| ASSERT_TRUE(extension_map.IsRegistered(kRtpExtensionMid)); |
| const std::string video_mid = |
| GetFirstVideoContent(offer->description())->mid(); |
| send_node->router()->SetFilter([extension_map, video_mid, &send_node]( |
| const EmulatedIpPacket& packet) { |
| if (IsRtpPacket(packet.data)) { |
| // Replace Mid with another. This should lead to that packets |
| // can not be demuxed by the callee, but BWE should still |
| // function. |
| RtpPacket parsed_packet; |
| parsed_packet.IdentifyExtensions(extension_map); |
| EXPECT_TRUE(parsed_packet.Parse(packet.data)); |
| std::string mid; |
| if (parsed_packet.GetExtension<RtpMid>(&mid)) { |
| if (mid == video_mid) { |
| parsed_packet.SetExtension<RtpMid>("x"); |
| EmulatedIpPacket updated_packet(packet.from, packet.to, |
| parsed_packet.Buffer(), |
| packet.arrival_time); |
| send_node->OnPacketReceived(std::move(updated_packet)); |
| return false; |
| } |
| } |
| } |
| return true; |
| }); |
| }, |
| [&](const SessionDescriptionInterface& answer) { |
| offer_exchange_done = true; |
| }); |
| // Wait for SDP negotiation and the packet filter to be setup. |
| s.WaitAndProcess(&offer_exchange_done); |
| |
| DataRate initial_bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| s.ProcessMessages(TimeDelta::Seconds(2)); |
| |
| // Since the packets are undemuxable, no packets will be received preventing |
| // the inbound-rtp stats entry from being created. |
| auto callee_inbound_stats = |
| GetStatsAndProcess(s, callee)->GetStatsOfType<RTCInboundRtpStreamStats>(); |
| ASSERT_THAT(callee_inbound_stats, SizeIs(0)); |
| |
| DataRate final_bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| // Ensure BWE has increased from the initial BWE. BWE will not increase unless |
| // RTCP feedback is received. The increase is just an arbitrary value to |
| // ensure BWE has increased beyond noise levels. |
| EXPECT_GT(final_bwe, initial_bwe + DataRate::KilobitsPerSec(345)); |
| } |
| |
| struct InitialProbeTestParams { |
| std::string test_name; |
| bool l4s_network = false; |
| DataRate network_capacity; |
| DataRate expected_bwe_min; |
| std::optional<DataRate> max_bwe; |
| }; |
| class BweRampupWithInitialProbeTest |
| : public Test, |
| public WithParamInterface<InitialProbeTestParams> {}; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| BweRampupWithInitialProbeTest, |
| BweRampupWithInitialProbeTest, |
| ValuesIn<InitialProbeTestParams>( |
| {{ |
| .test_name = "3Mbit", |
| .network_capacity = DataRate::KilobitsPerSec(3000), |
| .expected_bwe_min = DataRate::KilobitsPerSec(2500), |
| }, |
| { |
| .test_name = "500Kbit", |
| .network_capacity = DataRate::KilobitsPerSec(500), |
| .expected_bwe_min = DataRate::KilobitsPerSec(400), |
| }, |
| { |
| .test_name = "L4s3Mbit", |
| .l4s_network = true, |
| .network_capacity = DataRate::KilobitsPerSec(3000), |
| .expected_bwe_min = DataRate::KilobitsPerSec(1500), |
| .max_bwe = DataRate::KilobitsPerSec(3600), |
| }, |
| { |
| .test_name = "L4s500Kbit", |
| .l4s_network = true, |
| .network_capacity = DataRate::KilobitsPerSec(500), |
| .expected_bwe_min = DataRate::KilobitsPerSec(200), |
| .max_bwe = DataRate::KilobitsPerSec(600), |
| }}), |
| [](const ::testing::TestParamInfo<InitialProbeTestParams>& info) { |
| return info.param.test_name; |
| }); |
| |
| class MockRtpSenderObserver : public RtpSenderObserverInterface { |
| public: |
| MOCK_METHOD(void, OnFirstPacketSent, (MediaType)); |
| }; |
| |
| // Test that caller and callee BWE rampup even if no media packets are sent. |
| // - BandWidthEstimationSettings.allow_probe_without_media must be set. |
| // - A Video RtpTransceiver with RTX support needs to be negotiated. |
| TEST_P(BweRampupWithInitialProbeTest, BweRampUpBothDirectionsWithoutMedia) { |
| PeerScenario s(*::testing::UnitTest::GetInstance()->current_test_info()); |
| InitialProbeTestParams test_params = GetParam(); |
| PeerScenarioClient::Config config; |
| if (test_params.l4s_network) { |
| config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback", |
| "Enabled,offer:true"); |
| config.field_trials.Set("WebRTC-Bwe-ScreamV2", "Enabled"); |
| } |
| PeerScenarioClient* caller = s.CreateClient(config); |
| PeerScenarioClient* callee = s.CreateClient(config); |
| |
| auto transceiver_or = caller->pc()->AddTransceiver(MediaType::VIDEO); |
| ASSERT_TRUE(transceiver_or.error().ok()); |
| auto transceiver = transceiver_or.value(); |
| |
| auto parameters = transceiver->sender()->GetParameters(); |
| ASSERT_THAT(parameters.encodings, testing::SizeIs(1)); |
| parameters.encodings[0].max_bitrate_bps = 3'000'000; |
| ASSERT_TRUE(transceiver->sender()->SetParameters(parameters).ok()); |
| |
| MockRtpSenderObserver observer; |
| EXPECT_CALL(observer, OnFirstPacketSent).Times(0); |
| transceiver->sender()->SetObserver(&observer); |
| |
| caller->pc()->ReconfigureBandwidthEstimation( |
| {.allow_probe_without_media = true}); |
| callee->pc()->ReconfigureBandwidthEstimation( |
| {.allow_probe_without_media = true}); |
| |
| auto node_builder = s.net() |
| ->NodeBuilder() |
| .capacity_kbps(test_params.network_capacity.kbps()) |
| .delay_ms(20); |
| std::unique_ptr<NetworkQueueFactory> queue_factory; |
| if (test_params.l4s_network) { |
| queue_factory = std::make_unique<DualPi2NetworkQueueFactory>( |
| DualPi2NetworkQueue::Config({.target_delay = TimeDelta::Millis(25)})); |
| node_builder.queue_factory(*queue_factory); |
| } |
| |
| auto caller_node = node_builder.Build().node; |
| auto callee_node = node_builder.Build().node; |
| s.net()->CreateRoute(caller->endpoint(), {caller_node}, callee->endpoint()); |
| s.net()->CreateRoute(callee->endpoint(), {callee_node}, caller->endpoint()); |
| |
| auto signaling = |
| s.ConnectSignaling(caller, callee, {caller_node}, {callee_node}); |
| signaling.StartIceSignaling(); |
| |
| std::atomic<bool> offer_exchange_done(false); |
| signaling.NegotiateSdp( |
| [&]() { |
| // When remote description has been set, a transceiver is created. |
| // Set the diretion to sendrecv so that it can be used for BWE probing |
| // from callee -> caller. |
| ASSERT_THAT(callee->pc()->GetTransceivers(), SizeIs(1)); |
| ASSERT_TRUE( |
| callee->pc() |
| ->GetTransceivers()[0] |
| ->SetDirectionWithError(RtpTransceiverDirection::kSendRecv) |
| .ok()); |
| }, |
| [&](const SessionDescriptionInterface& answer) { |
| offer_exchange_done = true; |
| }); |
| // Wait for SDP negotiation. |
| s.WaitAndProcess(&offer_exchange_done); |
| |
| // Test that 1s after offer/answer exchange finish, we have a BWE estimate, |
| // even though no video frames have been sent. |
| s.ProcessMessages(TimeDelta::Seconds(2)); |
| |
| auto callee_inbound_stats = |
| GetStatsAndProcess(s, callee)->GetStatsOfType<RTCInboundRtpStreamStats>(); |
| ASSERT_THAT(callee_inbound_stats, SizeIs(1)); |
| ASSERT_EQ(*callee_inbound_stats[0]->frames_received, 0u); |
| auto caller_inbound_stats = |
| GetStatsAndProcess(s, caller)->GetStatsOfType<RTCInboundRtpStreamStats>(); |
| ASSERT_THAT(caller_inbound_stats, SizeIs(1)); |
| ASSERT_EQ(*caller_inbound_stats[0]->frames_received, 0u); |
| |
| DataRate caller_bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| DataRate callee_bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, callee)); |
| EXPECT_GT(caller_bwe.kbps(), test_params.expected_bwe_min.kbps()); |
| EXPECT_GT(callee_bwe.kbps(), test_params.expected_bwe_min.kbps()); |
| if (test_params.max_bwe.has_value()) { |
| EXPECT_LE(caller_bwe.kbps(), test_params.max_bwe->kbps()); |
| EXPECT_LE(callee_bwe.kbps(), test_params.max_bwe->kbps()); |
| } else { |
| EXPECT_LE(caller_bwe.kbps(), test_params.network_capacity.kbps()); |
| EXPECT_LE(callee_bwe.kbps(), test_params.network_capacity.kbps()); |
| } |
| } |
| |
| // Test that we can reconfigure bandwidth estimation and send new BWE probes. |
| // In this test, camera is stopped, and some times later, the app want to get a |
| // new BWE estimate. |
| TEST(BweRampupTest, CanReconfigureBweAfterStopingVideo) { |
| PeerScenario s(*::testing::UnitTest::GetInstance()->current_test_info()); |
| PeerScenarioClient* caller = s.CreateClient({}); |
| PeerScenarioClient* callee = s.CreateClient({}); |
| |
| auto node_builder = s.net()->NodeBuilder().capacity_kbps(1000); |
| auto caller_node = node_builder.Build().node; |
| auto callee_node = node_builder.Build().node; |
| s.net()->CreateRoute(caller->endpoint(), {caller_node}, callee->endpoint()); |
| s.net()->CreateRoute(callee->endpoint(), {callee_node}, caller->endpoint()); |
| |
| PeerScenarioClient::VideoSendTrack track = caller->CreateVideo("VIDEO", {}); |
| |
| auto signaling = |
| s.ConnectSignaling(caller, callee, {caller_node}, {callee_node}); |
| |
| signaling.StartIceSignaling(); |
| |
| std::atomic<bool> offer_exchange_done(false); |
| signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { |
| offer_exchange_done = true; |
| }); |
| // Wait for SDP negotiation. |
| s.WaitAndProcess(&offer_exchange_done); |
| |
| // Send a TCP messages to the receiver using the same downlink node. |
| // This is done just to force a lower BWE than the link capacity. |
| TcpMessageRoute* tcp_route = s.net()->CreateTcpRoute( |
| s.net()->CreateRoute({caller_node}), s.net()->CreateRoute({callee_node})); |
| DataRate bwe_before_restart = DataRate::Zero(); |
| |
| std::atomic<bool> message_delivered(false); |
| tcp_route->SendMessage( |
| /*size=*/5'00'000, |
| /*on_received=*/[&]() { message_delivered = true; }); |
| s.WaitAndProcess(&message_delivered); |
| bwe_before_restart = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| |
| // Camera is stopped. |
| track.capturer->Stop(); |
| s.ProcessMessages(TimeDelta::Seconds(2)); |
| |
| // Some time later, the app is interested in restarting BWE since we may want |
| // to resume video eventually. |
| caller->pc()->ReconfigureBandwidthEstimation( |
| {.allow_probe_without_media = true}); |
| s.ProcessMessages(TimeDelta::Seconds(1)); |
| DataRate bwe_after_restart = |
| GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| EXPECT_GT(bwe_after_restart.kbps(), bwe_before_restart.kbps() + 300); |
| EXPECT_LT(bwe_after_restart.kbps(), 1000); |
| } |
| |
| TEST(BweRampupTest, RespectsStartRateFromSetBitrate) { |
| PeerScenario s(*test_info_); |
| |
| PeerScenarioClient::Config config; |
| config.field_trials.Set("WebRTC-PcFactoryDefaultBitrates", "max:100000"); |
| |
| PeerScenarioClient* caller = s.CreateClient(config); |
| webrtc::BitrateSettings bitrate_settings; |
| bitrate_settings.min_bitrate_bps = 5'000'000; |
| bitrate_settings.start_bitrate_bps = 70'000'000; |
| bitrate_settings.max_bitrate_bps = 100'000'000; |
| |
| caller->pc()->SetBitrate(bitrate_settings); |
| PeerScenarioClient* callee = s.CreateClient({}); |
| |
| // No network constraints. |
| auto caller_to_calee_node = |
| s.net()->NodeBuilder().capacity_Mbps(100).Build().node; |
| auto callee_to_caler_node = |
| s.net()->NodeBuilder().capacity_Mbps(100).Build().node; |
| |
| FrameGeneratorCapturerConfig::SquaresVideo video_resolution = { |
| .framerate = 30, .width = 1280 * 4, .height = 720 * 4}; |
| PeerScenarioClient::VideoSendTrack track = caller->CreateVideo( |
| "VIDEO", {.generator = {.squares_video = video_resolution}}); |
| |
| s.SimpleConnection(caller, callee, {caller_to_calee_node}, |
| {callee_to_caler_node}); |
| |
| s.ProcessMessages(TimeDelta::Seconds(1)); |
| DataRate bwe = GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| EXPECT_GE(bwe.kbps(), 70000); |
| } |
| |
| // Reproduces slow BWE ramp-up when max allocated bitrate increases while the |
| // sender is outside ALR (e.g. join with media off, then start screen share). |
| TEST(BweRampupTest, RampUpWhenAllocatedBitrateIncreasesOutsideAlr) { |
| PeerScenario s(*::testing::UnitTest::GetInstance()->current_test_info()); |
| PeerScenarioClient* caller = s.CreateClient({}); |
| PeerScenarioClient* callee = s.CreateClient({}); |
| |
| // High capacity so the link is not the bottleneck. |
| auto node_builder = s.net()->NodeBuilder().capacity_kbps(5000).delay_ms(25); |
| auto caller_node = node_builder.Build().node; |
| auto callee_node = node_builder.Build().node; |
| s.net()->CreateRoute(caller->endpoint(), {caller_node}, callee->endpoint()); |
| s.net()->CreateRoute(callee->endpoint(), {callee_node}, caller->endpoint()); |
| |
| FrameGeneratorCapturerConfig::SquaresVideo video_resolution = { |
| .framerate = 30, .width = 1920, .height = 1080}; |
| PeerScenarioClient::VideoSendTrack track = caller->CreateVideo( |
| "VIDEO", |
| {.generator = {.squares_video = video_resolution}, .screencast = true}); |
| // Mimic joining a call with outgoing media disabled. |
| track.source->Stop(); |
| |
| { |
| RtpParameters parameters = track.sender->GetParameters(); |
| ASSERT_THAT(parameters.encodings, SizeIs(1)); |
| parameters.encodings[0].max_bitrate_bps = 500'000; |
| ASSERT_TRUE(track.sender->SetParameters(parameters).ok()); |
| } |
| |
| auto signaling = |
| s.ConnectSignaling(caller, callee, {caller_node}, {callee_node}); |
| signaling.StartIceSignaling(); |
| |
| std::atomic<bool> offer_exchange_done(false); |
| signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) { |
| offer_exchange_done = true; |
| }); |
| s.WaitAndProcess(&offer_exchange_done); |
| |
| // Allow initial probing to finish while no media is sent, so the sender |
| // remains outside ALR when the allocation is later increased. |
| s.ProcessMessages(TimeDelta::Seconds(5)); |
| DataRate bwe_before_allocation_increase = |
| GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| EXPECT_LT(bwe_before_allocation_increase.kbps(), 700); |
| |
| auto outbound_stats = GetStatsAndProcess(s, caller) |
| ->GetStatsOfType<RTCOutboundRtpStreamStats>(); |
| ASSERT_FALSE(outbound_stats.empty()); |
| EXPECT_EQ(outbound_stats[0]->frames_encoded.value_or(0), 0u); |
| |
| // Raise max bitrate while outside ALR, then start media. The allocation |
| // increase should trigger probing and let BWE ramp toward the new allocation. |
| { |
| RtpParameters parameters = track.sender->GetParameters(); |
| ASSERT_THAT(parameters.encodings, SizeIs(1)); |
| parameters.encodings[0].max_bitrate_bps = 3'500'000; |
| ASSERT_TRUE(track.sender->SetParameters(parameters).ok()); |
| } |
| |
| track.source->Start(); |
| s.ProcessMessages(TimeDelta::Seconds(3)); |
| |
| auto outbound_after = GetStatsAndProcess(s, caller) |
| ->GetStatsOfType<RTCOutboundRtpStreamStats>(); |
| ASSERT_FALSE(outbound_after.empty()); |
| EXPECT_GT(outbound_after[0]->frames_encoded.value_or(0), 0u); |
| |
| DataRate bwe_after_media_start = |
| GetAvailableSendBitrate(GetStatsAndProcess(s, caller)); |
| |
| EXPECT_GT(bwe_after_media_start.kbps(), 3000); |
| } |
| |
| } // namespace test |
| } // namespace webrtc |