Add separate test file for PR-Answer tests
This documents the subtleties in testing PR-Answer in one place.
Also adds a test for checking that switching feedback formats works correctly, but this is currently disabled.
Bug: webrtc:448848876
Change-Id: Ia52393b0585186bda1ada98a33ed9b3374ebcdec
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/413640
Reviewed-by: Jonas Oreland <jonaso@webrtc.org>
Commit-Queue: Harald Alvestrand <hta@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#45842}
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index c0f2ff4..d12b50d 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -2470,6 +2470,7 @@
"peer_connection_simulcast_unittest.cc",
"peer_connection_stability_integrationtest.cc",
"peer_connection_svc_integrationtest.cc",
+ "pranswer_switch_integrationtest.cc",
"proxy_unittest.cc",
"rtc_stats_collector_unittest.cc",
"rtc_stats_integrationtest.cc",
diff --git a/pc/pranswer_switch_integrationtest.cc b/pc/pranswer_switch_integrationtest.cc
new file mode 100644
index 0000000..446a175
--- /dev/null
+++ b/pc/pranswer_switch_integrationtest.cc
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2025 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.
+ */
+
+// This file contains tests that verify the correct working of switching
+// to a different callee between PR-Answer and Answer.
+
+#include <atomic>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "api/data_channel_interface.h"
+#include "api/jsep.h"
+#include "api/peer_connection_interface.h"
+#include "api/rtc_error.h"
+#include "api/test/rtc_error_matchers.h"
+#include "p2p/test/test_turn_server.h"
+#include "pc/test/fake_rtc_certificate_generator.h"
+#include "pc/test/integration_test_helpers.h"
+#include "rtc_base/socket_address.h"
+#include "rtc_base/task_queue_for_test.h"
+#include "test/gmock.h"
+#include "test/gtest.h"
+#include "test/wait_until.h"
+
+namespace webrtc {
+
+using ::testing::Eq;
+using ::testing::Field;
+using ::testing::Gt;
+using ::testing::HasSubstr;
+using ::testing::IsTrue;
+using ::testing::Ne;
+using ::testing::Not;
+
+class PeerConnectionPrAnswerSwitchTest
+ : public PeerConnectionIntegrationBaseTest {
+ public:
+ PeerConnectionPrAnswerSwitchTest()
+ : PeerConnectionIntegrationBaseTest(SdpSemantics::kUnifiedPlan) {}
+ std::unique_ptr<PeerConnectionIntegrationWrapper> SetupCallee2(
+ bool addTurn,
+ bool create_media_engine) {
+ RTCConfiguration config;
+ if (addTurn) {
+ static const SocketAddress turn_server_1_internal_address{"192.0.2.1",
+ 3478};
+ static const SocketAddress turn_server_1_external_address{"192.0.3.1", 0};
+ TestTurnServer* turn_server_1 = CreateTurnServer(
+ turn_server_1_internal_address, turn_server_1_external_address);
+
+ // Bypass permission check on received packets so media can be sent before
+ // the candidate is signaled.
+ SendTask(network_thread(), [turn_server_1] {
+ turn_server_1->set_enable_permission_checks(false);
+ });
+
+ PeerConnectionInterface::IceServer ice_server_1;
+ ice_server_1.urls.push_back("turn:192.0.2.1:3478");
+ ice_server_1.username = "test";
+ ice_server_1.password = "test";
+ config.servers.push_back(ice_server_1);
+ config.type = PeerConnectionInterface::kRelay;
+ config.presume_writable_when_fully_relayed = true;
+ }
+ CreatePeerConnectionWrappersWithConfig(config, config, create_media_engine);
+ PeerConnectionDependencies dependencies(nullptr);
+ // Ensure that the key of callee2 is different from the key of
+ // callee1, so that reconnection will trigger an ICE restart.
+ std::unique_ptr<FakeRTCCertificateGenerator> cert_generator(
+ new FakeRTCCertificateGenerator());
+ cert_generator->use_alternate_key();
+ dependencies.cert_generator = std::move(cert_generator);
+ auto callee2 = CreatePeerConnectionWrapper(
+ "Callee2", nullptr, &config, std::move(dependencies), nullptr,
+ /*reset_encoder_factory=*/false,
+ /*reset_decoder_factory=*/false, create_media_engine);
+ ConnectFakeSignaling();
+ callee2->set_signaling_message_receiver(caller());
+ return callee2;
+ }
+
+#ifdef WEBRTC_HAVE_SCTP
+ std::unique_ptr<PeerConnectionIntegrationWrapper> SetupCallee2AndDc(
+ bool addTurn) {
+ std::unique_ptr<PeerConnectionIntegrationWrapper> callee2 =
+ SetupCallee2(addTurn, /* create_media_engine= */ false);
+ DataChannelInit dc_init;
+ dc_init.negotiated = true;
+ dc_init.id = 77;
+ caller()->CreateDataChannel("label", &dc_init);
+ callee()->CreateDataChannel("label", &dc_init);
+ callee2->CreateDataChannel("label", &dc_init);
+
+ return callee2;
+ }
+#endif // WEBRTC_HAVE_SCTP
+
+ void WaitConnected(bool prAnswer,
+ PeerConnectionIntegrationWrapper* caller,
+ PeerConnectionIntegrationWrapper* callee) {
+ if (prAnswer) {
+ EXPECT_EQ(caller->pc()->signaling_state(),
+ PeerConnectionInterface::kHaveRemotePrAnswer);
+ EXPECT_EQ(callee->pc()->signaling_state(),
+ PeerConnectionInterface::kHaveLocalPrAnswer);
+ } else {
+ EXPECT_EQ(caller->pc()->signaling_state(),
+ PeerConnectionInterface::kStable);
+ EXPECT_EQ(callee->pc()->signaling_state(),
+ PeerConnectionInterface::kStable);
+ }
+ ASSERT_THAT(
+ WaitUntil([&] { return caller->pc()->peer_connection_state(); },
+ Eq(PeerConnectionInterface::PeerConnectionState::kConnected)),
+ IsRtcOk());
+ ASSERT_THAT(
+ WaitUntil([&] { return callee->pc()->peer_connection_state(); },
+ Eq(PeerConnectionInterface::PeerConnectionState::kConnected)),
+ IsRtcOk());
+ }
+
+#ifdef WEBRTC_HAVE_SCTP
+ void WaitConnectedAndDcOpen(bool prAnswer,
+ PeerConnectionIntegrationWrapper* caller,
+ PeerConnectionIntegrationWrapper* callee) {
+ WaitConnected(prAnswer, caller, callee);
+ ASSERT_THAT(WaitUntil([&] { return caller->data_channel()->state(); },
+ Eq(DataChannelInterface::kOpen)),
+ IsRtcOk());
+ ASSERT_THAT(WaitUntil([&] { return callee->data_channel()->state(); },
+ Eq(DataChannelInterface::kOpen)),
+ IsRtcOk());
+ }
+
+ static void SendOnDatachannelWhenConnectedCallback(
+ PeerConnectionIntegrationWrapper* peer,
+ const std::string& data,
+ std::atomic<int>& signal) {
+ if (peer->pc()->peer_connection_state() ==
+ PeerConnectionInterface::PeerConnectionState::kConnected &&
+ peer->data_channel()->state() == DataChannelInterface::kOpen) {
+ peer->data_channel()->SendAsync(DataBuffer(data), [&](RTCError err) {
+ signal.store(err.ok() ? 1 : -1);
+ });
+ }
+ }
+ void VerifyReceivedDcMessages(PeerConnectionIntegrationWrapper* peer,
+ const std::string& data,
+ std::atomic<int>& signal) {
+ ASSERT_THAT(WaitUntil([&] { return signal.load(); }, Ne(0)), IsRtcOk());
+ EXPECT_THAT(WaitUntil([&] { return peer->data_observer()->last_message(); },
+ Eq(data)),
+ IsRtcOk());
+ }
+#endif // WEBRTC_HAVE_SCTP
+};
+
+#ifdef WEBRTC_HAVE_SCTP
+TEST_F(PeerConnectionPrAnswerSwitchTest, DtlsRestartOneCalleAtATime) {
+ auto callee2 = SetupCallee2AndDc(/* addTurn= */ false);
+ std::unique_ptr<SessionDescriptionInterface> offer;
+ callee()->SetReceivedSdpMunger(
+ [&](std::unique_ptr<SessionDescriptionInterface>& sdp) {
+ // Capture offer so that it can be sent to Callee2 too.
+ offer = sdp->Clone();
+ });
+ callee()->SetGeneratedSdpMunger(
+ [&](std::unique_ptr<SessionDescriptionInterface>& sdp) {
+ // Modify offer to kPrAnswer
+ SetSdpType(sdp, SdpType::kPrAnswer);
+ });
+ caller()->CreateAndSetAndSignalOffer();
+ ASSERT_FALSE(HasFailure());
+ WaitConnectedAndDcOpen(/* prAnswer= */ true, caller(), callee());
+ ASSERT_FALSE(HasFailure());
+
+ std::atomic<int> caller_sent_on_dc(0);
+ std::atomic<int> callee2_sent_on_dc(0);
+ caller()->set_connection_change_callback([&](auto new_state) {
+ SendOnDatachannelWhenConnectedCallback(caller(), "KESO", caller_sent_on_dc);
+ });
+ // Install same cb on both connection_change_callback and
+ // data_observer->set_state_change_callback since they can fire in any order.
+ callee2->set_connection_change_callback([&](auto new_state) {
+ SendOnDatachannelWhenConnectedCallback(callee2.get(), "KENT",
+ callee2_sent_on_dc);
+ });
+ callee2->data_observer()->set_state_change_callback([&](auto new_state) {
+ SendOnDatachannelWhenConnectedCallback(callee2.get(), "KENT",
+ callee2_sent_on_dc);
+ });
+
+ // Now let callee2 get the offer, apply it and send the answer to caller.
+ std::string offer_sdp;
+ EXPECT_TRUE(offer->ToString(&offer_sdp));
+ callee2->ReceiveSdpMessage(SdpType::kOffer, offer_sdp);
+ WaitConnectedAndDcOpen(/* prAnswer= */ false, caller(), callee2.get());
+ ASSERT_FALSE(HasFailure());
+
+ VerifyReceivedDcMessages(caller(), "KENT", callee2_sent_on_dc);
+ VerifyReceivedDcMessages(callee2.get(), "KESO", caller_sent_on_dc);
+ ASSERT_FALSE(HasFailure());
+}
+#endif // WEBRTC_HAVE_SCTP
+
+TEST_F(PeerConnectionPrAnswerSwitchTest, SendMediaNoDataChannel) {
+ std::unique_ptr<PeerConnectionIntegrationWrapper> second_callee =
+ SetupCallee2(/* addTurn= */ false, /* create_media_engine= */ true);
+ std::string saved_offer;
+ caller()->AddAudioVideoTracks();
+ caller()->SetGeneratedSdpMunger(
+ [&](std::unique_ptr<SessionDescriptionInterface>& sdp) {
+ sdp->ToString(&saved_offer);
+ });
+ callee()->SetGeneratedSdpMunger(
+ [](std::unique_ptr<SessionDescriptionInterface>& sdp) {
+ SetSdpType(sdp, SdpType::kPrAnswer);
+ });
+ caller()->CreateAndSetAndSignalOffer();
+ ASSERT_THAT(WaitUntil(
+ [&] {
+ return caller()->pc()->signaling_state() ==
+ PeerConnectionInterface::kHaveRemotePrAnswer;
+ },
+ IsTrue()),
+ IsRtcOk());
+ WaitConnected(/* prAnswer= */ true, caller(), callee());
+ MediaExpectations media_expectations;
+ media_expectations.CalleeExpectsSomeAudio();
+ media_expectations.CalleeExpectsSomeVideo();
+ ASSERT_TRUE(ExpectNewFrames(media_expectations));
+ // Send original offer to second callee and wait for settlement.
+ second_callee->ReceiveSdpMessage(SdpType::kOffer, saved_offer);
+ EXPECT_THAT(
+ WaitUntil([&] { return caller()->SignalingStateStable(); }, IsTrue()),
+ IsRtcOk());
+ WaitConnected(/* prAnswer= */ false, caller(), second_callee.get());
+ ASSERT_FALSE(HasFailure());
+}
+
+// This test completes, but is disabled because feedback type switching
+// does not work yet.
+// TODO: issues.webrtc.org/448848876 - enable when underlying issue fixed.
+TEST_F(PeerConnectionPrAnswerSwitchTest, DISABLED_MediaWithCcfbFirstThenTwcc) {
+ SetFieldTrials("WebRTC-RFC8888CongestionControlFeedback/Enabled,offer:true/");
+ SetFieldTrials("Callee2",
+ "WebRTC-RFC8888CongestionControlFeedback/Disabled/");
+ std::unique_ptr<PeerConnectionIntegrationWrapper> second_callee =
+ SetupCallee2(/* addTurn= */ false, /* create_media_engine= */ true);
+ std::string saved_offer;
+ caller()->AddAudioVideoTracks();
+ caller()->SetGeneratedSdpMunger(
+ [&](std::unique_ptr<SessionDescriptionInterface>& sdp) {
+ sdp->ToString(&saved_offer);
+ });
+ callee()->SetGeneratedSdpMunger(
+ [](std::unique_ptr<SessionDescriptionInterface>& sdp) {
+ SetSdpType(sdp, SdpType::kPrAnswer);
+ });
+ caller()->CreateAndSetAndSignalOffer();
+ ASSERT_THAT(WaitUntil(
+ [&] {
+ return caller()->pc()->signaling_state() ==
+ PeerConnectionInterface::kHaveRemotePrAnswer;
+ },
+ IsTrue()),
+ IsRtcOk());
+ WaitConnected(/* prAnswer= */ true, caller(), callee());
+ MediaExpectations media_expectations;
+ media_expectations.CalleeExpectsSomeAudio();
+ media_expectations.CalleeExpectsSomeVideo();
+ ASSERT_TRUE(ExpectNewFrames(media_expectations));
+ auto pc_internal = caller()->pc_internal();
+ EXPECT_THAT(
+ WaitUntil(
+ [&] {
+ return pc_internal->FeedbackAccordingToRfc8888CountForTesting();
+ },
+ Gt(0)),
+ IsRtcOk());
+ // There should be no transport-cc generated.
+ EXPECT_THAT(pc_internal->FeedbackAccordingToTransportCcCountForTesting(),
+ Eq(0));
+ // The final answer does TWCC.
+ second_callee->ReceiveSdpMessage(SdpType::kOffer, saved_offer);
+ EXPECT_THAT(
+ WaitUntil([&] { return caller()->SignalingStateStable(); }, IsTrue()),
+ IsRtcOk());
+ WaitConnected(/* prAnswer= */ false, caller(), second_callee.get());
+ ASSERT_FALSE(HasFailure());
+
+ int old_ccfb_count = pc_internal->FeedbackAccordingToRfc8888CountForTesting();
+ int old_twcc_count =
+ pc_internal->FeedbackAccordingToTransportCcCountForTesting();
+ EXPECT_THAT(
+ WaitUntil(
+ [&] {
+ return pc_internal->FeedbackAccordingToTransportCcCountForTesting();
+ },
+ Gt(old_twcc_count)),
+ IsRtcOk());
+ // These expects are easier to interpret than the WaitUntil log result.
+ EXPECT_THAT(pc_internal->FeedbackAccordingToTransportCcCountForTesting(),
+ Gt(old_twcc_count));
+ EXPECT_THAT(pc_internal->FeedbackAccordingToRfc8888CountForTesting(),
+ Eq(old_ccfb_count));
+}
+
+} // namespace webrtc