Refine RFC8888 CCFB enablement logic and add test.

The `SdpOfferAnswerHandler::PushdownMediaDescription` now checks if the remote description supports RFC8888 Congestion Control Feedback when processing an answer. This ensures that RFC8888 feedback is only enabled if both sides signal support.

Bug: webrtc:438613826
Change-Id: Iecf2050cb5f2110e010ce056af6b58077a60891e
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/404482
Commit-Queue: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Auto-Submit: Per Kjellander <perkj@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#45346}
diff --git a/pc/sdp_offer_answer.cc b/pc/sdp_offer_answer.cc
index 174b4e8..1ccc4e8 100644
--- a/pc/sdp_offer_answer.cc
+++ b/pc/sdp_offer_answer.cc
@@ -5040,9 +5040,9 @@
     ContentSource source,
     const std::map<std::string, const ContentGroup*>& bundle_groups_by_mid) {
   TRACE_EVENT0("webrtc", "SdpOfferAnswerHandler::PushdownMediaDescription");
+  RTC_DCHECK_RUN_ON(signaling_thread());
   const SessionDescriptionInterface* sdesc =
       (source == CS_LOCAL ? local_description() : remote_description());
-  RTC_DCHECK_RUN_ON(signaling_thread());
   RTC_DCHECK(sdesc);
 
   if (ConfiguredForMedia()) {
@@ -5114,11 +5114,29 @@
     // If local and remote are both set, we assume that it's safe to trigger
     // CCFB.
     if (pc_->trials().IsEnabled("WebRTC-RFC8888CongestionControlFeedback")) {
-      if (use_ccfb && local_description() && remote_description()) {
-        // The call and the congestion controller live on the worker thread.
-        context_->worker_thread()->PostTask([call = pc_->call_ptr()] {
-          call->EnableSendCongestionControlFeedbackAccordingToRfc8888();
-        });
+      if (type == SdpType::kAnswer && use_ccfb && local_description() &&
+          remote_description()) {
+        bool remote_supports_ccfb = true;
+        // Verify that the remote supports CCFB before enabling.
+        for (const auto& content :
+             remote_description()->description()->contents()) {
+          if (content.type != MediaProtocolType::kRtp || content.rejected ||
+              content.media_description() == nullptr) {
+            continue;
+          }
+          if (!content.media_description()->rtcp_fb_ack_ccfb()) {
+            remote_supports_ccfb = false;
+            break;
+          }
+        }
+        if (remote_supports_ccfb) {
+          // The call and the congestion controller live on the worker thread.
+          context_->worker_thread()->PostTask([call = pc_->call_ptr()] {
+            call->EnableSendCongestionControlFeedbackAccordingToRfc8888();
+          });
+        } else {
+          RTC_LOG(LS_WARNING) << "Local but not Remote supports CCFB.";
+        }
       }
     }
   }
diff --git a/test/peer_scenario/peer_scenario_client.cc b/test/peer_scenario/peer_scenario_client.cc
index e9aa2ae..740e3ba 100644
--- a/test/peer_scenario/peer_scenario_client.cc
+++ b/test/peer_scenario/peer_scenario_client.cc
@@ -465,6 +465,28 @@
           }));
 }
 
+void PeerScenarioClient::SetLocalDescription(
+    std::string sdp,
+    SdpType type,
+    std::function<void(RTCError)> on_complete) {
+  RTC_DCHECK_RUN_ON(signaling_thread_);
+  peer_connection_->SetLocalDescription(
+      CreateSessionDescription(type, sdp),
+      make_ref_counted<LambdaSetLocalDescriptionObserver>(
+          std::move(on_complete)));
+}
+
+void PeerScenarioClient::SetRemoteDescription(
+    std::string sdp,
+    SdpType type,
+    std::function<void(RTCError)> on_complete) {
+  RTC_DCHECK_RUN_ON(signaling_thread_);
+  peer_connection_->SetRemoteDescription(
+      CreateSessionDescription(type, sdp),
+      make_ref_counted<LambdaSetRemoteDescriptionObserver>(
+          std::move(on_complete)));
+}
+
 void PeerScenarioClient::AddIceCandidate(
     std::unique_ptr<IceCandidate> candidate) {
   RTC_DCHECK_RUN_ON(signaling_thread_);
diff --git a/test/peer_scenario/peer_scenario_client.h b/test/peer_scenario/peer_scenario_client.h
index a4c4b48..7ef3df6 100644
--- a/test/peer_scenario/peer_scenario_client.h
+++ b/test/peer_scenario/peer_scenario_client.h
@@ -26,6 +26,7 @@
 #include "api/jsep.h"
 #include "api/media_stream_interface.h"
 #include "api/peer_connection_interface.h"
+#include "api/rtc_error.h"
 #include "api/rtp_receiver_interface.h"
 #include "api/rtp_sender_interface.h"
 #include "api/rtp_transceiver_interface.h"
@@ -174,6 +175,13 @@
       std::function<void(const SessionDescriptionInterface& answer)>
           done_handler);
 
+  void SetLocalDescription(std::string sdp,
+                           SdpType type,
+                           std::function<void(RTCError)> on_complete);
+  void SetRemoteDescription(std::string sdp,
+                            SdpType type,
+                            std::function<void(RTCError)> on_complete);
+
   // Adds the given ice candidate when the peer connection is ready.
   void AddIceCandidate(std::unique_ptr<IceCandidate> candidate);
 
diff --git a/test/peer_scenario/tests/BUILD.gn b/test/peer_scenario/tests/BUILD.gn
index b4c1800..e156fac 100644
--- a/test/peer_scenario/tests/BUILD.gn
+++ b/test/peer_scenario/tests/BUILD.gn
@@ -25,6 +25,7 @@
       "../../../api:audio_options_api",
       "../../../api:libjingle_peerconnection_api",
       "../../../api:make_ref_counted",
+      "../../../api:rtc_error",
       "../../../api:rtc_stats_api",
       "../../../api:rtp_parameters",
       "../../../api:rtp_transceiver_direction",
diff --git a/test/peer_scenario/tests/l4s_test.cc b/test/peer_scenario/tests/l4s_test.cc
index 293c6bf..5f562ef 100644
--- a/test/peer_scenario/tests/l4s_test.cc
+++ b/test/peer_scenario/tests/l4s_test.cc
@@ -16,6 +16,7 @@
 #include "absl/strings/str_cat.h"
 #include "api/jsep.h"
 #include "api/make_ref_counted.h"
+#include "api/rtc_error.h"
 #include "api/scoped_refptr.h"
 #include "api/stats/rtc_stats_report.h"
 #include "api/stats/rtcstats_objects.h"
@@ -327,6 +328,114 @@
       return info.param.test_suffix;
     });
 
+TEST(L4STest, NoCcfbSentAfterRenegotiationAndCallerCachLocalDescription) {
+  // The caller supports CCFB, but the callee does not.
+  // This test that the caller does not start sending CCFB after renegotiation
+  // even if the local description is cached. The callers local description
+  // will contain CCFB since it was used in the initial offer.
+  PeerScenario s(*test_info_);
+  PeerScenarioClient::Config caller_config;
+  caller_config.disable_encryption = true;
+  caller_config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback",
+                                 "Enabled");
+  PeerScenarioClient* caller = s.CreateClient(caller_config);
+
+  PeerScenarioClient::Config callee_config;
+  callee_config.disable_encryption = true;
+  callee_config.field_trials.Set("WebRTC-RFC8888CongestionControlFeedback",
+                                 "Disabled");
+  PeerScenarioClient* callee = s.CreateClient(callee_config);
+
+  auto caller_to_callee = s.net()
+                              ->NodeBuilder()
+                              .capacity(DataRate::KilobitsPerSec(600))
+                              .Build()
+                              .node;
+  auto callee_to_caller = s.net()
+                              ->NodeBuilder()
+                              .capacity(DataRate::KilobitsPerSec(600))
+                              .Build()
+                              .node;
+  RtcpFeedbackCounter callee_feedback_counter;
+  caller_to_callee->router()->SetWatcher([&](const EmulatedIpPacket& packet) {
+    callee_feedback_counter.Count(packet);
+  });
+  RtcpFeedbackCounter caller_feedback_counter;
+  callee_to_caller->router()->SetWatcher([&](const EmulatedIpPacket& packet) {
+    caller_feedback_counter.Count(packet);
+  });
+
+  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("FROM_CALLER", video_conf);
+  callee->CreateVideo("FROM_CALLEE", video_conf);
+
+  signaling.StartIceSignaling();
+  std::atomic<bool> offer_exchange_done(false);
+  signaling.NegotiateSdp([&](const SessionDescriptionInterface& answer) {
+    offer_exchange_done = true;
+  });
+  ASSERT_TRUE(s.WaitAndProcess(&offer_exchange_done));
+  s.ProcessMessages(TimeDelta::Seconds(2));
+
+  EXPECT_EQ(caller_feedback_counter.FeedbackAccordingToRfc8888(), 0);
+  EXPECT_EQ(callee_feedback_counter.FeedbackAccordingToRfc8888(), 0);
+  int transport_cc_caller =
+      caller_feedback_counter.FeedbackAccordingToTransportCc();
+  int transport_cc_callee =
+      callee_feedback_counter.FeedbackAccordingToTransportCc();
+  EXPECT_GT(transport_cc_caller, 0);
+  EXPECT_GT(transport_cc_callee, 0);
+
+  offer_exchange_done = false;
+  // Save the caller's local description and use it as answer to the next offer
+  // from callee.
+  std::string answer_str;
+  caller->pc()->local_description()->ToString(&answer_str);
+  ASSERT_FALSE(answer_str.empty());
+  ASSERT_THAT(answer_str, HasSubstr("a=rtcp-fb:* ack ccfb\r\n"));
+
+  callee->CreateAndSetSdp(
+      [&](SessionDescriptionInterface* /*munge_offer*/) {
+        // Do not munge the offer.
+      },
+      [&](std::string offer) {
+        // Callee does not support ccfb and does not have it in the offer.
+        ASSERT_THAT(offer, Not(HasSubstr("a=rtcp-fb:* ack ccfb\r\n")));
+        caller->SetRemoteDescription(
+            offer, SdpType::kOffer, [&](RTCError error) {
+              ASSERT_TRUE(error.ok());
+              caller->SetLocalDescription(
+                  answer_str, SdpType::kAnswer, [&](RTCError error) {
+                    ASSERT_TRUE(error.ok());
+                    callee->SetRemoteDescription(answer_str, SdpType::kAnswer,
+                                                 [&](RTCError error) {
+                                                   ASSERT_TRUE(error.ok());
+                                                   offer_exchange_done = true;
+                                                 });
+                  });
+            });
+      });
+  ASSERT_TRUE(s.WaitAndProcess(&offer_exchange_done));
+  s.ProcessMessages(TimeDelta::Seconds(4));
+
+  EXPECT_EQ(caller_feedback_counter.FeedbackAccordingToRfc8888(), 0);
+  EXPECT_EQ(callee_feedback_counter.FeedbackAccordingToRfc8888(), 0);
+  EXPECT_GT(caller_feedback_counter.FeedbackAccordingToTransportCc(),
+            transport_cc_caller);
+  EXPECT_GT(callee_feedback_counter.FeedbackAccordingToTransportCc(),
+            transport_cc_callee);
+}
+
 // 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.