Wire Sframe enable callback through sender and receiver constructors.

This CL implements the C++ plumbing for the SFrame enable signal
described in the WebRTC Encoded Transform spec
(https://w3c.github.io/webrtc-encoded-transform/#attribute, Section
2.2).

Per the spec, when an SFrame transform is set on an RTCRtpSender or
RTCRtpReceiver, the setter must propagate [[useSFrame]] = true to the
owning RTCRtpTransceiver and trigger SDP renegotiation. If the
transceiver's [[useSFrame]] has already been negotiated to false, it
must throw InvalidModificationError.

This is modeled by wiring an on_sframe_enable_requested callback from
each sender/receiver to RtpTransceiver::TryToEnableSframe(). When the
application calls CreateSframeEncrypterOrError() (sender) or
CreateSframeDecrypterOrError() (receiver), the callback fires, which
sets sframe_enabled_ = true on the transceiver and triggers
on_negotiation_needed_(). After negotiation completes,
ApplySframeEnabled() locks the state — matching the spec's behavior
where [[useSFrame]] becomes immutable once associated with a media
description.

Bug: webrtc:479862368
Change-Id: Ie5edd365aa4896bfda44cf34a2ab52dfe32bb441
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/459460
Reviewed-by: Philip Eliasson <philipel@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Philip Eliasson <philipel@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47536}
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index 1b7768f..6514d2f 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -1749,6 +1749,7 @@
     "//third_party/abseil-cpp/absl/algorithm:container",
     "//third_party/abseil-cpp/absl/base:nullability",
     "//third_party/abseil-cpp/absl/functional:any_invocable",
+    "//third_party/abseil-cpp/absl/functional:bind_front",
     "//third_party/abseil-cpp/absl/strings:string_view",
   ]
 }
@@ -1845,6 +1846,8 @@
     "../api:sframe_decrypter_interface",
     "../api:sframe_types",
     "../media:media_channel",
+    "../rtc_base:logging",
+    "../rtc_base:macromagic",
     "../rtc_base:threading",
     "../rtc_base/system:no_unique_address",
     "//third_party/abseil-cpp/absl/functional:any_invocable",
@@ -1867,6 +1870,7 @@
     "../api:frame_transformer_interface",
     "../api:make_ref_counted",
     "../api:media_stream_interface",
+    "../api:rtc_error",
     "../api:rtp_parameters",
     "../api:rtp_receiver_interface",
     "../api:scoped_refptr",
@@ -1899,6 +1903,7 @@
     "../api:frame_transformer_interface",
     "../api:make_ref_counted",
     "../api:media_stream_interface",
+    "../api:rtc_error",
     "../api:rtp_parameters",
     "../api:rtp_receiver_interface",
     "../api:scoped_refptr",
@@ -3182,6 +3187,8 @@
       "../api:rtp_transceiver_interface",
       "../api:scoped_refptr",
       "../api:set_remote_description_observer_interface",
+      "../api:sframe_encrypter_interface",
+      "../api:sframe_types",
       "../api/audio_codecs:builtin_audio_decoder_factory",
       "../api/audio_codecs:builtin_audio_encoder_factory",
       "../api/units:data_rate",
@@ -3433,6 +3440,8 @@
       "../api:rtp_parameters",
       "../api:rtp_receiver_interface",
       "../api:scoped_refptr",
+      "../api:sframe_encrypter_interface",
+      "../api:sframe_types",
       "../api/crypto:frame_decryptor_interface",
       "../api/crypto:frame_encryptor_interface",
       "../api/crypto:options",
diff --git a/pc/audio_rtp_receiver.cc b/pc/audio_rtp_receiver.cc
index ab3fbad..585719c6 100644
--- a/pc/audio_rtp_receiver.cc
+++ b/pc/audio_rtp_receiver.cc
@@ -24,6 +24,7 @@
 #include "api/frame_transformer_interface.h"
 #include "api/make_ref_counted.h"
 #include "api/media_stream_interface.h"
+#include "api/rtc_error.h"
 #include "api/rtp_parameters.h"
 #include "api/rtp_receiver_interface.h"
 #include "api/scoped_refptr.h"
@@ -44,10 +45,12 @@
     Thread* worker_thread,
     absl::string_view receiver_id,
     std::vector<std::string> stream_ids,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     VoiceMediaReceiveChannelInterface* voice_channel)
     : AudioRtpReceiver(worker_thread,
                        receiver_id,
                        CreateStreamsFromIds(std::move(stream_ids)),
+                       std::move(enable_sframe_at_owner),
                        voice_channel,
                        RemoteAudioSource::OnAudioChannelGoneAction::kSurvive) {}
 
@@ -60,6 +63,7 @@
     : AudioRtpReceiver(worker_thread,
                        receiver_id,
                        streams,
+                       nullptr,
                        media_channel,
                        RemoteAudioSource::OnAudioChannelGoneAction::kEnd) {
   RTC_DCHECK(!is_unified_plan);
@@ -73,6 +77,7 @@
     : AudioRtpReceiver(worker_thread,
                        receiver_id,
                        streams,
+                       nullptr,
                        media_channel,
                        RemoteAudioSource::OnAudioChannelGoneAction::kSurvive) {}
 
@@ -80,9 +85,10 @@
     Thread* worker_thread,
     absl::string_view receiver_id,
     const std::vector<scoped_refptr<MediaStreamInterface>>& streams,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     VoiceMediaReceiveChannelInterface* voice_channel,
     RemoteAudioSource::OnAudioChannelGoneAction source_gone_action)
-    : RtpReceiverBase(worker_thread),
+    : RtpReceiverBase(worker_thread, std::move(enable_sframe_at_owner)),
       id_(receiver_id),
       source_(make_ref_counted<RemoteAudioSource>(worker_thread,
                                                   source_gone_action)),
diff --git a/pc/audio_rtp_receiver.h b/pc/audio_rtp_receiver.h
index f89ab65..984a80c 100644
--- a/pc/audio_rtp_receiver.h
+++ b/pc/audio_rtp_receiver.h
@@ -24,6 +24,7 @@
 #include "api/frame_transformer_interface.h"
 #include "api/media_stream_interface.h"
 #include "api/media_types.h"
+#include "api/rtc_error.h"
 #include "api/rtp_parameters.h"
 #include "api/rtp_receiver_interface.h"
 #include "api/scoped_refptr.h"
@@ -55,6 +56,7 @@
   AudioRtpReceiver(Thread* worker_thread,
                    absl::string_view receiver_id,
                    std::vector<std::string> stream_ids,
+                   absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
                    VoiceMediaReceiveChannelInterface* voice_channel = nullptr);
   // Note: This is a PlanB-only constructor.
   // TODO(https://crbug.com/webrtc/9480): Remove this when streams() is removed.
@@ -134,6 +136,7 @@
       Thread* worker_thread,
       absl::string_view receiver_id,
       const std::vector<scoped_refptr<MediaStreamInterface>>& streams,
+      absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
       VoiceMediaReceiveChannelInterface* media_channel,
       RemoteAudioSource::OnAudioChannelGoneAction source_gone_action);
 
diff --git a/pc/audio_rtp_receiver_unittest.cc b/pc/audio_rtp_receiver_unittest.cc
index 6a69cd6..b296dd5 100644
--- a/pc/audio_rtp_receiver_unittest.cc
+++ b/pc/audio_rtp_receiver_unittest.cc
@@ -44,10 +44,11 @@
  protected:
   AudioRtpReceiverTest()
       : worker_thread_(Thread::Create()),
-        receiver_(
-            make_ref_counted<AudioRtpReceiver>(worker_thread_.get(),
-                                               std::string(),
-                                               std::vector<std::string>())) {
+        receiver_(make_ref_counted<AudioRtpReceiver>(
+            worker_thread_.get(),
+            std::string(),
+            std::vector<std::string>(),
+            /*enable_sframe_at_owner=*/nullptr)) {
     worker_thread_->Start();
     EXPECT_CALL(receive_channel_, SetRawAudioSink(kSsrc, _));
     EXPECT_CALL(receive_channel_, SetBaseMinimumPlayoutDelayMs(kSsrc, _));
@@ -124,7 +125,7 @@
   MockVoiceMediaReceiveChannelInterface receive_channel;
   auto receiver = make_ref_counted<AudioRtpReceiver>(
       worker_thread.get(), std::string(), std::vector<std::string>(),
-      &receive_channel);
+      /*enable_sframe_at_owner=*/nullptr, &receive_channel);
 
   EXPECT_CALL(receive_channel, SetDefaultRawAudioSink(_)).Times(1);
   EXPECT_CALL(receive_channel, SetDefaultOutputVolume(kDefaultVolume)).Times(1);
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index 9a3e98a..1b4e0c2 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -1287,6 +1287,7 @@
     auto audio_sender =
         AudioRtpSender::Create(env_, signaling_thread(), worker_thread(),
                                CreateRandomUuid(), legacy_stats_.get(), nullptr,
+                               /*enable_sframe_at_owner=*/nullptr,
                                rtp_manager()->voice_media_send_channel());
     new_sender = RtpSenderProxyWithInternal<RtpSenderInternal>::Create(
         signaling_thread(), audio_sender);
@@ -1295,6 +1296,7 @@
   } else if (kind == MediaStreamTrackInterface::kVideoKind) {
     auto video_sender = VideoRtpSender::Create(
         env_, signaling_thread(), worker_thread(), CreateRandomUuid(), nullptr,
+        /*enable_sframe_at_owner=*/nullptr,
         rtp_manager()->video_media_send_channel(),
         /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
         /*initial_simulcast_layers=*/{});
diff --git a/pc/rtc_stats_collector_unittest.cc b/pc/rtc_stats_collector_unittest.cc
index ef92482..57f7e84 100644
--- a/pc/rtc_stats_collector_unittest.cc
+++ b/pc/rtc_stats_collector_unittest.cc
@@ -3974,6 +3974,7 @@
   scoped_refptr<AudioRtpSender> sender = AudioRtpSender::Create(
       env_, pc_->signaling_thread(), pc_->worker_thread(), "sender_id",
       /*stats=*/nullptr, /*set_streams_observer=*/nullptr,
+      /*enable_sframe_at_owner=*/nullptr,
       /*media_channel=*/nullptr);
 
   {
diff --git a/pc/rtp_receiver.cc b/pc/rtp_receiver.cc
index 33dddb8..0151622 100644
--- a/pc/rtp_receiver.cc
+++ b/pc/rtp_receiver.cc
@@ -16,6 +16,7 @@
 #include <utility>
 #include <vector>
 
+#include "absl/functional/any_invocable.h"
 #include "api/media_stream_interface.h"
 #include "api/rtc_error.h"
 #include "api/scoped_refptr.h"
@@ -47,12 +48,26 @@
   return streams;
 }
 
-RtpReceiverBase::RtpReceiverBase(Thread* worker_thread)
-    : worker_thread_(worker_thread) {}
+RtpReceiverBase::RtpReceiverBase(
+    Thread* worker_thread,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner)
+    : worker_thread_(worker_thread),
+      enable_sframe_at_owner_(std::move(enable_sframe_at_owner)) {}
 
 RTCErrorOr<scoped_refptr<SframeDecrypterInterface>>
 RtpReceiverBase::CreateSframeDecrypterOrError(SframeCipherSuite cipher_suite) {
   RTC_DCHECK_RUN_ON(&signaling_thread_checker_);
+
+  if (!enable_sframe_at_owner_) {
+    return RTCError(RTCErrorType::INTERNAL_ERROR,
+                    "Receiver is not associated with a transceiver");
+  }
+
+  RTCError error = enable_sframe_at_owner_();
+  if (!error.ok()) {
+    return error;
+  }
+
   // TODO(bugs.webrtc.org/479862368): Create the internal Sframe decryption
   // pipeline and return a key management handle.
   return RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
diff --git a/pc/rtp_receiver.h b/pc/rtp_receiver.h
index 6a2b21d..8f8616c 100644
--- a/pc/rtp_receiver.h
+++ b/pc/rtp_receiver.h
@@ -33,6 +33,7 @@
 #include "media/base/media_channel.h"
 #include "rtc_base/system/no_unique_address.h"
 #include "rtc_base/thread.h"
+#include "rtc_base/thread_annotations.h"
 
 namespace webrtc {
 
@@ -109,10 +110,16 @@
   CreateSframeDecrypterOrError(SframeCipherSuite cipher_suite) override;
 
  protected:
-  explicit RtpReceiverBase(Thread* worker_thread);
+  explicit RtpReceiverBase(
+      Thread* worker_thread,
+      absl::AnyInvocable<RTCError()> enable_sframe_at_owner);
 
   RTC_NO_UNIQUE_ADDRESS SequenceChecker signaling_thread_checker_;
   Thread* const worker_thread_;
+
+ private:
+  absl::AnyInvocable<RTCError()> enable_sframe_at_owner_
+      RTC_GUARDED_BY(signaling_thread_checker_);
 };
 
 }  // namespace webrtc
diff --git a/pc/rtp_sender.cc b/pc/rtp_sender.cc
index 5fbb37f..49cdc86 100644
--- a/pc/rtp_sender.cc
+++ b/pc/rtp_sender.cc
@@ -221,13 +221,15 @@
   return false;
 }
 
-RtpSenderBase::RtpSenderBase(const Environment& env,
-                             Thread* signaling_thread,
-                             Thread* worker_thread,
-                             absl::string_view id,
-                             MediaType media_type,
-                             SetStreamsObserver* set_streams_observer,
-                             MediaSendChannelInterface* media_channel)
+RtpSenderBase::RtpSenderBase(
+    const Environment& env,
+    Thread* signaling_thread,
+    Thread* worker_thread,
+    absl::string_view id,
+    MediaType media_type,
+    SetStreamsObserver* set_streams_observer,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
+    MediaSendChannelInterface* media_channel)
     : env_(env),
       signaling_thread_(signaling_thread),
       worker_thread_(worker_thread),
@@ -240,7 +242,8 @@
           worker_thread_)),
       signaling_safety_(
           PendingTaskSafetyFlag::CreateAttachedToTaskQueue(/*alive=*/true,
-                                                           signaling_thread_)) {
+                                                           signaling_thread_)),
+      enable_sframe_at_owner_(std::move(enable_sframe_at_owner)) {
   RTC_DCHECK(worker_thread_);
   init_parameters_.encodings.emplace_back();
   if (media_channel) {
@@ -1071,6 +1074,17 @@
 RtpSenderBase::CreateSframeEncrypterOrError(
     const SframeEncrypterInit& options) {
   RTC_DCHECK_RUN_ON(signaling_thread_);
+
+  if (!enable_sframe_at_owner_) {
+    return RTCError(RTCErrorType::INTERNAL_ERROR,
+                    "Sender is not associated with a transceiver");
+  }
+
+  RTCError error = enable_sframe_at_owner_();
+  if (!error.ok()) {
+    return error;
+  }
+
   // TODO(bugs.webrtc.org/479862368): Implement Sframe encrypter creation.
   return RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
                   "Sframe encrypter not yet implemented");
@@ -1114,25 +1128,29 @@
     absl::string_view id,
     LegacyStatsCollectorInterface* stats,
     SetStreamsObserver* set_streams_observer,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     MediaSendChannelInterface* media_channel) {
-  return make_ref_counted<AudioRtpSender>(env, signaling_thread, worker_thread,
-                                          id, stats, set_streams_observer,
-                                          media_channel);
+  return make_ref_counted<AudioRtpSender>(
+      env, signaling_thread, worker_thread, id, stats, set_streams_observer,
+      std::move(enable_sframe_at_owner), media_channel);
 }
 
-AudioRtpSender::AudioRtpSender(const Environment& env,
-                               Thread* signaling_thread,
-                               Thread* worker_thread,
-                               absl::string_view id,
-                               LegacyStatsCollectorInterface* stats,
-                               SetStreamsObserver* set_streams_observer,
-                               MediaSendChannelInterface* media_channel)
+AudioRtpSender::AudioRtpSender(
+    const Environment& env,
+    Thread* signaling_thread,
+    Thread* worker_thread,
+    absl::string_view id,
+    LegacyStatsCollectorInterface* stats,
+    SetStreamsObserver* set_streams_observer,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
+    MediaSendChannelInterface* media_channel)
     : RtpSenderBase(env,
                     signaling_thread,
                     worker_thread,
                     id,
                     MediaType::AUDIO,
                     set_streams_observer,
+                    std::move(enable_sframe_at_owner),
                     media_channel),
       legacy_stats_(stats),
       dtmf_sender_(DtmfSender::Create(signaling_thread, this)),
@@ -1286,14 +1304,15 @@
     Thread* worker_thread,
     absl::string_view id,
     SetStreamsObserver* set_streams_observer,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     MediaSendChannelInterface* media_channel,
     const std::vector<RtpEncodingParameters>& init_send_encodings,
     bool simulcast_rejected,
     const std::vector<SimulcastLayer>& initial_simulcast_layers) {
   return make_ref_counted<VideoRtpSender>(
       env, signaling_thread, worker_thread, id, set_streams_observer,
-      media_channel, init_send_encodings, simulcast_rejected,
-      initial_simulcast_layers);
+      std::move(enable_sframe_at_owner), media_channel, init_send_encodings,
+      simulcast_rejected, initial_simulcast_layers);
 }
 
 VideoRtpSender::VideoRtpSender(
@@ -1302,6 +1321,7 @@
     Thread* worker_thread,
     absl::string_view id,
     SetStreamsObserver* set_streams_observer,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     MediaSendChannelInterface* media_channel,
     const std::vector<RtpEncodingParameters>& init_send_encodings,
     bool simulcast_rejected,
@@ -1312,6 +1332,7 @@
                     id,
                     MediaType::VIDEO,
                     set_streams_observer,
+                    std::move(enable_sframe_at_owner),
                     media_channel) {
   set_init_send_encodings(
       CalculateInitialEncodings(init_parameters_.encodings, init_send_encodings,
diff --git a/pc/rtp_sender.h b/pc/rtp_sender.h
index d0b6f80..f39ac69 100644
--- a/pc/rtp_sender.h
+++ b/pc/rtp_sender.h
@@ -273,6 +273,7 @@
                 absl::string_view id,
                 MediaType media_type,
                 SetStreamsObserver* set_streams_observer,
+                absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
                 MediaSendChannelInterface* media_channel);
 
   // TODO(bugs.webrtc.org/8694): Since SSRC == 0 is technically valid, figure
@@ -352,6 +353,9 @@
 
   scoped_refptr<PendingTaskSafetyFlag> worker_safety_;
   ScopedTaskSafety signaling_safety_;
+
+  absl::AnyInvocable<RTCError()> enable_sframe_at_owner_
+      RTC_GUARDED_BY(signaling_thread_);
 };
 
 // LocalAudioSinkAdapter receives data callback as a sink to the local
@@ -410,6 +414,7 @@
       absl::string_view id,
       LegacyStatsCollectorInterface* stats,
       SetStreamsObserver* set_streams_observer,
+      absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
       MediaSendChannelInterface* media_channel);
   ~AudioRtpSender() override;
 
@@ -434,6 +439,7 @@
                  absl::string_view id,
                  LegacyStatsCollectorInterface* legacy_stats,
                  SetStreamsObserver* set_streams_observer,
+                 absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
                  MediaSendChannelInterface* media_channel);
 
   void SetSend() override;
@@ -483,6 +489,7 @@
       Thread* worker_thread,
       absl::string_view id,
       SetStreamsObserver* set_streams_observer,
+      absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
       MediaSendChannelInterface* media_channel,
       const std::vector<RtpEncodingParameters>& init_send_encodings,
       bool simulcast_rejected,
@@ -505,6 +512,7 @@
                  Thread* worker_thread,
                  absl::string_view id,
                  SetStreamsObserver* set_streams_observer,
+                 absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
                  MediaSendChannelInterface* media_channel,
                  const std::vector<RtpEncodingParameters>& init_send_encodings,
                  bool simulcast_rejected,
diff --git a/pc/rtp_sender_receiver_unittest.cc b/pc/rtp_sender_receiver_unittest.cc
index ee13647..95f419a 100644
--- a/pc/rtp_sender_receiver_unittest.cc
+++ b/pc/rtp_sender_receiver_unittest.cc
@@ -32,6 +32,8 @@
 #include "api/rtp_parameters.h"
 #include "api/rtp_receiver_interface.h"
 #include "api/scoped_refptr.h"
+#include "api/sframe/sframe_encrypter_interface.h"
+#include "api/sframe/sframe_types.h"
 #include "api/task_queue/task_queue_base.h"
 #include "api/test/fake_frame_decryptor.h"
 #include "api/test/fake_frame_encryptor.h"
@@ -212,7 +214,7 @@
       audio_rtp_sender_ = AudioRtpSender::Create(
           CreateEnvironment(), signaling_thread_, worker_thread_.get(),
           audio_track_->id(), nullptr, set_streams_observer.get(),
-          voice_media_send_channel_.get());
+          /*enable_sframe_at_owner=*/nullptr, voice_media_send_channel_.get());
     });
     ASSERT_TRUE(audio_rtp_sender_->SetTrack(audio_track_.get()));
     EXPECT_CALL(*set_streams_observer, OnSetStreams());
@@ -225,7 +227,8 @@
     worker_thread_->BlockingCall([&]() {
       audio_rtp_sender_ = AudioRtpSender::Create(
           CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-          /*id=*/"", nullptr, nullptr, voice_media_send_channel_.get());
+          /*id=*/"", nullptr, nullptr,
+          /*enable_sframe_at_owner=*/nullptr, voice_media_send_channel_.get());
     });
   }
 
@@ -278,7 +281,7 @@
       video_rtp_sender_ = VideoRtpSender::Create(
           CreateEnvironment(), signaling_thread_, worker_thread_.get(),
           video_track_->id(), set_streams_observer.get(),
-          video_media_send_channel(),
+          /*enable_sframe_at_owner=*/nullptr, video_media_send_channel(),
           /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
           /*initial_simulcast_layers=*/{});
     });
@@ -292,7 +295,8 @@
     worker_thread_->BlockingCall([&]() {
       video_rtp_sender_ = VideoRtpSender::Create(
           CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-          /*id=*/"", nullptr, video_media_send_channel(),
+          /*id=*/"", nullptr,
+          /*enable_sframe_at_owner=*/nullptr, video_media_send_channel(),
           /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
           /*initial_simulcast_layers=*/{});
     });
@@ -331,7 +335,8 @@
   void CreateVideoRtpReceiver(
       std::vector<scoped_refptr<MediaStreamInterface>> streams = {}) {
     video_rtp_receiver_ = make_ref_counted<VideoRtpReceiver>(
-        worker_thread_.get(), kVideoTrackId, streams);
+        worker_thread_.get(), kVideoTrackId, streams,
+        /*enable_sframe_at_owner=*/nullptr);
     worker_thread_->BlockingCall([this] {
       video_rtp_receiver_->SetMediaChannel(video_media_receive_channel());
     });
@@ -354,7 +359,8 @@
     uint32_t primary_ssrc = stream_params.first_ssrc();
 
     video_rtp_receiver_ = make_ref_counted<VideoRtpReceiver>(
-        worker_thread_.get(), kVideoTrackId, streams);
+        worker_thread_.get(), kVideoTrackId, streams,
+        /*enable_sframe_at_owner=*/nullptr);
     worker_thread_->BlockingCall([this] {
       video_rtp_receiver_->SetMediaChannel(video_media_receive_channel());
     });
@@ -504,7 +510,7 @@
       const std::vector<std::string>& disabled_layers) {
     auto sender = VideoRtpSender::Create(
         CreateEnvironment(), signaling_thread_, worker_thread_.get(), "1",
-        nullptr, nullptr,
+        nullptr, /*enable_sframe_at_owner=*/nullptr, nullptr,
         /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
         /*initial_simulcast_layers=*/{});
     RtpParameters parameters;
@@ -966,7 +972,8 @@
 TEST_F(RtpSenderReceiverTest, AudioSenderCanSetParametersBeforeNegotiation) {
   audio_rtp_sender_ = AudioRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      /*id=*/"", nullptr, nullptr, nullptr);
+      /*id=*/"", nullptr, nullptr,
+      /*enable_sframe_at_owner=*/nullptr, nullptr);
 
   RtpParameters params = audio_rtp_sender_->GetParameters();
   ASSERT_EQ(1u, params.encodings.size());
@@ -984,7 +991,8 @@
        AudioSenderCanSetParametersAsyncBeforeNegotiation) {
   audio_rtp_sender_ = AudioRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      /*id=*/"", nullptr, nullptr, nullptr);
+      /*id=*/"", nullptr, nullptr,
+      /*enable_sframe_at_owner=*/nullptr, nullptr);
 
   std::optional<RTCError> result;
   RtpParameters params = audio_rtp_sender_->GetParameters();
@@ -1019,7 +1027,8 @@
       std::make_unique<MockSetStreamsObserver>();
   audio_rtp_sender_ = AudioRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      audio_track_->id(), nullptr, set_streams_observer.get(), nullptr);
+      audio_track_->id(), nullptr, set_streams_observer.get(),
+      /*enable_sframe_at_owner=*/nullptr, nullptr);
   ASSERT_TRUE(audio_rtp_sender_->SetTrack(audio_track_.get()));
   EXPECT_CALL(*set_streams_observer, OnSetStreams());
   audio_rtp_sender_->SetStreams({local_stream_->id()});
@@ -1053,7 +1062,8 @@
        AudioSenderMustCallGetParametersBeforeSetParametersBeforeNegotiation) {
   audio_rtp_sender_ = AudioRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      /*id=*/"", nullptr, nullptr, nullptr);
+      /*id=*/"", nullptr, nullptr,
+      /*enable_sframe_at_owner=*/nullptr, nullptr);
 
   RtpParameters params;
   RTCError result = audio_rtp_sender_->SetParameters(params);
@@ -1232,27 +1242,7 @@
 TEST_F(RtpSenderReceiverTest, VideoSenderCanSetParametersBeforeNegotiation) {
   video_rtp_sender_ = VideoRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(), /*id=*/"",
-      nullptr, nullptr,
-      /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
-      /*initial_simulcast_layers=*/{});
-
-  RtpParameters params = video_rtp_sender_->GetParameters();
-  ASSERT_EQ(1u, params.encodings.size());
-  params.encodings[0].max_bitrate_bps = 90000;
-  EXPECT_TRUE(video_rtp_sender_->SetParameters(params).ok());
-
-  params = video_rtp_sender_->GetParameters();
-  EXPECT_TRUE(video_rtp_sender_->SetParameters(params).ok());
-  EXPECT_EQ(params.encodings[0].max_bitrate_bps, 90000);
-
-  DestroyVideoRtpSender();
-}
-
-TEST_F(RtpSenderReceiverTest,
-       VideoSenderCanSetParametersAsyncBeforeNegotiation) {
-  video_rtp_sender_ = VideoRtpSender::Create(
-      CreateEnvironment(), signaling_thread_, worker_thread_.get(), /*id=*/"",
-      nullptr, nullptr,
+      nullptr, /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
 
@@ -1286,7 +1276,8 @@
       std::make_unique<MockSetStreamsObserver>();
   video_rtp_sender_ = VideoRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      video_track_->id(), set_streams_observer.get(), nullptr,
+      video_track_->id(), set_streams_observer.get(),
+      /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
   ASSERT_TRUE(video_rtp_sender_->SetTrack(video_track_.get()));
@@ -1332,7 +1323,8 @@
       std::make_unique<MockSetStreamsObserver>();
   video_rtp_sender_ = VideoRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      video_track_->id(), set_streams_observer.get(), nullptr,
+      video_track_->id(), set_streams_observer.get(),
+      /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
   ASSERT_TRUE(video_rtp_sender_->SetTrack(video_track_.get()));
@@ -1397,7 +1389,7 @@
       std::make_unique<MockSetStreamsObserver>();
   auto video_rtp_sender = VideoRtpSender::Create(
       env, thread, thread, video_track->id(), set_streams_observer.get(),
-      nullptr,
+      /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
 
@@ -1436,7 +1428,7 @@
        VideoSenderMustCallGetParametersBeforeSetParametersBeforeNegotiation) {
   video_rtp_sender_ = VideoRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(), /*id=*/"",
-      nullptr, nullptr,
+      nullptr, /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
 
@@ -1851,7 +1843,8 @@
   video_track_->set_content_hint(VideoTrackInterface::ContentHint::kDetailed);
   video_rtp_sender_ = VideoRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      video_track_->id(), set_streams_observer.get(), nullptr,
+      video_track_->id(), set_streams_observer.get(),
+      /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
   ASSERT_TRUE(video_rtp_sender_->SetTrack(video_track_.get()));
@@ -2061,7 +2054,8 @@
   AddVideoTrack();
   video_rtp_sender_ = VideoRtpSender::Create(
       CreateEnvironment(), signaling_thread_, worker_thread_.get(),
-      video_track_->id(), nullptr, nullptr,
+      video_track_->id(), nullptr,
+      /*enable_sframe_at_owner=*/nullptr, nullptr,
       /*init_send_encodings=*/{}, /*simulcast_rejected=*/false,
       /*initial_simulcast_layers=*/{});
   video_rtp_sender_->SetStreams({"1", "2", "1"});
@@ -2130,4 +2124,138 @@
     RtpSenderReceiverTest,
     ::testing::ValuesIn(kDisableSimulcastLayersParameters));
 
+TEST_F(RtpSenderReceiverTest, AudioSenderCreateSframeEncrypterInvokesCallback) {
+  bool callback_called = false;
+  worker_thread_->BlockingCall([&]() {
+    audio_rtp_sender_ = AudioRtpSender::Create(
+        CreateEnvironment(), signaling_thread_, worker_thread_.get(),
+        /*id=*/"", nullptr, nullptr,
+        [&callback_called]() -> RTCError {
+          callback_called = true;
+          return RTCError::OK();
+        },
+        nullptr);
+  });
+
+  SframeEncrypterInit options{SframeMode::kPerFrame,
+                              SframeCipherSuite::kAes128GcmSha256_128};
+  audio_rtp_sender_->CreateSframeEncrypterOrError(options);
+  EXPECT_TRUE(callback_called);
+}
+
+TEST_F(RtpSenderReceiverTest, VideoSenderCreateSframeEncrypterInvokesCallback) {
+  bool callback_called = false;
+  worker_thread_->BlockingCall([&]() {
+    video_rtp_sender_ = VideoRtpSender::Create(
+        CreateEnvironment(), signaling_thread_, worker_thread_.get(),
+        /*id=*/"", nullptr,
+        [&callback_called]() -> RTCError {
+          callback_called = true;
+          return RTCError::OK();
+        },
+        nullptr, /*init_send_encodings=*/{},
+        /*simulcast_rejected=*/false, /*initial_simulcast_layers=*/{});
+  });
+
+  SframeEncrypterInit options{SframeMode::kPerFrame,
+                              SframeCipherSuite::kAes128GcmSha256_128};
+  video_rtp_sender_->CreateSframeEncrypterOrError(options);
+  EXPECT_TRUE(callback_called);
+}
+
+TEST_F(RtpSenderReceiverTest,
+       SenderCreateSframeEncrypterPropagatesCallbackError) {
+  worker_thread_->BlockingCall([&]() {
+    audio_rtp_sender_ = AudioRtpSender::Create(
+        CreateEnvironment(), signaling_thread_, worker_thread_.get(),
+        /*id=*/"", nullptr, nullptr,
+        []() -> RTCError {
+          return RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
+                          "Rejected for testing");
+        },
+        nullptr);
+  });
+
+  SframeEncrypterInit options{SframeMode::kPerFrame,
+                              SframeCipherSuite::kAes128GcmSha256_128};
+  auto result = audio_rtp_sender_->CreateSframeEncrypterOrError(options);
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(result.error().type(), RTCErrorType::UNSUPPORTED_OPERATION);
+  EXPECT_STREQ(result.error().message(), "Rejected for testing");
+}
+
+TEST_F(RtpSenderReceiverTest,
+       SenderCreateSframeEncrypterFailsWithNullCallback) {
+  worker_thread_->BlockingCall([&]() {
+    audio_rtp_sender_ = AudioRtpSender::Create(
+        CreateEnvironment(), signaling_thread_, worker_thread_.get(),
+        /*id=*/"", nullptr, nullptr,
+        /*enable_sframe_at_owner=*/nullptr, nullptr);
+  });
+
+  SframeEncrypterInit options{SframeMode::kPerFrame,
+                              SframeCipherSuite::kAes128GcmSha256_128};
+  auto result = audio_rtp_sender_->CreateSframeEncrypterOrError(options);
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(result.error().type(), RTCErrorType::INTERNAL_ERROR);
+}
+
+TEST_F(RtpSenderReceiverTest,
+       AudioReceiverCreateSframeDecrypterInvokesCallback) {
+  bool callback_called = false;
+  auto receiver = make_ref_counted<AudioRtpReceiver>(
+      worker_thread_.get(), kAudioTrackId, std::vector<std::string>(),
+      [&callback_called]() -> RTCError {
+        callback_called = true;
+        return RTCError::OK();
+      });
+
+  receiver->CreateSframeDecrypterOrError(
+      SframeCipherSuite::kAes128GcmSha256_128);
+  EXPECT_TRUE(callback_called);
+}
+
+TEST_F(RtpSenderReceiverTest,
+       VideoReceiverCreateSframeDecrypterInvokesCallback) {
+  bool callback_called = false;
+  auto receiver = make_ref_counted<VideoRtpReceiver>(
+      worker_thread_.get(), kVideoTrackId, std::vector<std::string>(),
+      [&callback_called]() -> RTCError {
+        callback_called = true;
+        return RTCError::OK();
+      });
+
+  receiver->CreateSframeDecrypterOrError(
+      SframeCipherSuite::kAes128GcmSha256_128);
+  EXPECT_TRUE(callback_called);
+}
+
+TEST_F(RtpSenderReceiverTest,
+       ReceiverCreateSframeDecrypterPropagatesCallbackError) {
+  auto receiver = make_ref_counted<AudioRtpReceiver>(
+      worker_thread_.get(), kAudioTrackId, std::vector<std::string>(),
+      []() -> RTCError {
+        return RTCError(RTCErrorType::UNSUPPORTED_OPERATION,
+                        "Rejected for testing");
+      });
+
+  auto result = receiver->CreateSframeDecrypterOrError(
+      SframeCipherSuite::kAes128GcmSha256_128);
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(result.error().type(), RTCErrorType::UNSUPPORTED_OPERATION);
+  EXPECT_STREQ(result.error().message(), "Rejected for testing");
+}
+
+TEST_F(RtpSenderReceiverTest,
+       ReceiverCreateSframeDecrypterFailsWithNullCallback) {
+  auto receiver = make_ref_counted<AudioRtpReceiver>(
+      worker_thread_.get(), kAudioTrackId, std::vector<std::string>(),
+      /*enable_sframe_at_owner=*/nullptr);
+
+  auto result = receiver->CreateSframeDecrypterOrError(
+      SframeCipherSuite::kAes128GcmSha256_128);
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(result.error().type(), RTCErrorType::INTERNAL_ERROR);
+}
+
 }  // namespace webrtc
diff --git a/pc/rtp_transceiver.cc b/pc/rtp_transceiver.cc
index 12db4ac..bc2a937 100644
--- a/pc/rtp_transceiver.cc
+++ b/pc/rtp_transceiver.cc
@@ -23,6 +23,7 @@
 #include "absl/algorithm/container.h"
 #include "absl/base/nullability.h"
 #include "absl/functional/any_invocable.h"
+#include "absl/functional/bind_front.h"
 #include "absl/strings/string_view.h"
 #include "api/audio_options.h"
 #include "api/crypto/crypto_options.h"
@@ -160,6 +161,7 @@
     LegacyStatsCollectorInterface* legacy_stats,
     RtpSenderBase::SetStreamsObserver* set_streams_observer,
     absl::string_view sender_id,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     MediaSendChannelInterface* media_send_channel,
     const std::vector<RtpEncodingParameters>& init_send_encodings,
     bool simulcast_rejected,
@@ -170,6 +172,7 @@
         AudioRtpSender::Create(
             env, context->signaling_thread(), context->worker_thread(),
             sender_id, legacy_stats, set_streams_observer,
+            std::move(enable_sframe_at_owner),
             static_cast<VoiceMediaSendChannelInterface*>(media_send_channel)));
   }
   RTC_DCHECK_EQ(media_type, MediaType::VIDEO);
@@ -177,7 +180,7 @@
       context->signaling_thread(),
       VideoRtpSender::Create(
           env, context->signaling_thread(), context->worker_thread(), sender_id,
-          set_streams_observer,
+          set_streams_observer, std::move(enable_sframe_at_owner),
           static_cast<VideoMediaSendChannelInterface*>(media_send_channel),
           init_send_encodings, simulcast_rejected, initial_simulcast_layers));
 }
@@ -201,11 +204,13 @@
 CreateReceiverOfType(Thread* signaling_thread,
                      Thread* worker_thread,
                      absl::string_view receiver_id,
-                     MediaReceiveChannelInterface* receive_channel) {
+                     MediaReceiveChannelInterface* receive_channel,
+                     absl::AnyInvocable<RTCError()> enable_sframe_at_owner) {
   return RtpReceiverProxyWithInternal<RtpReceiverInternal>::Create(
       signaling_thread, worker_thread,
       make_ref_counted<RtpReceiverT>(
           worker_thread, receiver_id, std::vector<std::string>(),
+          std::move(enable_sframe_at_owner),
           static_cast<ReceiveInterface*>(receive_channel)));
 }
 
@@ -214,16 +219,19 @@
     Thread* signaling_thread,
     Thread* worker_thread,
     absl::string_view receiver_id,
-    MediaReceiveChannelInterface* receive_channel) {
+    MediaReceiveChannelInterface* receive_channel,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner) {
   if (media_type == MediaType::AUDIO) {
     return CreateReceiverOfType<AudioRtpReceiver,
                                 VoiceMediaReceiveChannelInterface>(
-        signaling_thread, worker_thread, receiver_id, receive_channel);
+        signaling_thread, worker_thread, receiver_id, receive_channel,
+        std::move(enable_sframe_at_owner));
   }
   RTC_DCHECK_EQ(media_type, MediaType::VIDEO);
   return CreateReceiverOfType<VideoRtpReceiver,
                               VideoMediaReceiveChannelInterface>(
-      signaling_thread, worker_thread, receiver_id, receive_channel);
+      signaling_thread, worker_thread, receiver_id, receive_channel,
+      std::move(enable_sframe_at_owner));
 }
 
 std::pair<std::unique_ptr<MediaSendChannelInterface>,
@@ -427,8 +435,10 @@
             std::move(encoder_switch_callback));
         auto sender = CreateSender(
             media_type_, env_, context_, legacy_stats_, set_streams_observer_,
-            sender_id, channels.first.get(), init_send_encodings,
-            simulcast_rejected, initial_simulcast_layers);
+            sender_id,
+            absl::bind_front(&RtpTransceiver::TryToEnableSframe, this),
+            channels.first.get(), init_send_encodings, simulcast_rejected,
+            initial_simulcast_layers);
         return ScopedOperationsBatcher::FinalizerTask(
             [this, channels = std::move(channels), sender = std::move(sender),
              track, stream_ids, init_send_encodings, receiver_id]() mutable {
@@ -444,7 +454,8 @@
                   media_type_, context_->signaling_thread(),
                   context_->worker_thread(),
                   receiver_id.empty() ? CreateRandomUuid() : receiver_id,
-                  owned_receive_channel_.get()));
+                  owned_receive_channel_.get(),
+                  absl::bind_front(&RtpTransceiver::TryToEnableSframe, this)));
             });
       });
 }
@@ -795,8 +806,9 @@
     RTC_DCHECK_RUN_ON(context()->worker_thread());
     senders_.push_back(CreateSender(
         media_type_, env_, context_, legacy_stats_, set_streams_observer_,
-        sender_id, channel_ ? channel_->media_send_channel() : nullptr,
-        send_encodings, false, {}));
+        sender_id, /*enable_sframe_at_owner=*/nullptr,
+        channel_ ? channel_->media_send_channel() : nullptr, send_encodings,
+        false, {}));
   });
   ConfigureSender(senders_.back(), track.get(), stream_ids, send_encodings,
                   codec_vendor());
diff --git a/pc/rtp_transmission_manager.cc b/pc/rtp_transmission_manager.cc
index e0ecad9..a40d287 100644
--- a/pc/rtp_transmission_manager.cc
+++ b/pc/rtp_transmission_manager.cc
@@ -511,7 +511,8 @@
   // TODO(https://crbug.com/webrtc/9480): When we remove remote_streams(), use
   // the constructor taking stream IDs instead.
   auto video_receiver = make_ref_counted<VideoRtpReceiver>(
-      worker_thread(), remote_sender_info.sender_id, streams);
+      worker_thread(), remote_sender_info.sender_id, streams,
+      /*enable_sframe_at_owner=*/nullptr);
 
   auto task = video_receiver->GetSetupForMediaChannel(
       remote_sender_info.sender_id == kDefaultVideoSenderId
diff --git a/pc/video_rtp_receiver.cc b/pc/video_rtp_receiver.cc
index 250caa9..5d3bf2b 100644
--- a/pc/video_rtp_receiver.cc
+++ b/pc/video_rtp_receiver.cc
@@ -24,6 +24,7 @@
 #include "api/frame_transformer_interface.h"
 #include "api/make_ref_counted.h"
 #include "api/media_stream_interface.h"
+#include "api/rtc_error.h"
 #include "api/rtp_parameters.h"
 #include "api/rtp_receiver_interface.h"
 #include "api/scoped_refptr.h"
@@ -47,18 +48,21 @@
     Thread* worker_thread,
     absl::string_view receiver_id,
     std::vector<std::string> stream_ids,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     VideoMediaReceiveChannelInterface* media_channel)
     : VideoRtpReceiver(worker_thread,
                        receiver_id,
                        CreateStreamsFromIds(std::move(stream_ids)),
+                       std::move(enable_sframe_at_owner),
                        media_channel) {}
 
 VideoRtpReceiver::VideoRtpReceiver(
     Thread* worker_thread,
     absl::string_view receiver_id,
     const std::vector<scoped_refptr<MediaStreamInterface>>& streams,
+    absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
     VideoMediaReceiveChannelInterface* media_channel)
-    : RtpReceiverBase(worker_thread),
+    : RtpReceiverBase(worker_thread, std::move(enable_sframe_at_owner)),
       id_(receiver_id),
       media_channel_(media_channel),
       source_(make_ref_counted<VideoRtpTrackSource>(&source_callback_)),
diff --git a/pc/video_rtp_receiver.h b/pc/video_rtp_receiver.h
index 552539d..67cafd1 100644
--- a/pc/video_rtp_receiver.h
+++ b/pc/video_rtp_receiver.h
@@ -24,6 +24,7 @@
 #include "api/frame_transformer_interface.h"
 #include "api/media_stream_interface.h"
 #include "api/media_types.h"
+#include "api/rtc_error.h"
 #include "api/rtp_parameters.h"
 #include "api/rtp_receiver_interface.h"
 #include "api/scoped_refptr.h"
@@ -49,6 +50,7 @@
   VideoRtpReceiver(Thread* worker_thread,
                    absl::string_view receiver_id,
                    std::vector<std::string> streams_ids,
+                   absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
                    VideoMediaReceiveChannelInterface* media_channel = nullptr);
   // TODO(hbos): Remove this when streams() is removed.
   // https://crbug.com/webrtc/9480
@@ -58,6 +60,7 @@
       Thread* worker_thread,
       absl::string_view receiver_id,
       const std::vector<scoped_refptr<MediaStreamInterface>>& streams,
+      absl::AnyInvocable<RTCError()> enable_sframe_at_owner,
       VideoMediaReceiveChannelInterface* media_channel = nullptr);
 
   ~VideoRtpReceiver() override;
diff --git a/pc/video_rtp_receiver_unittest.cc b/pc/video_rtp_receiver_unittest.cc
index ed73fac..cda5869 100644
--- a/pc/video_rtp_receiver_unittest.cc
+++ b/pc/video_rtp_receiver_unittest.cc
@@ -83,7 +83,8 @@
         receiver_(make_ref_counted<VideoRtpReceiver>(
             worker_thread_.get(),
             "receiver",
-            std::vector<std::string>({"stream"}))) {
+            std::vector<std::string>({"stream"}),
+            /*enable_sframe_at_owner=*/nullptr)) {
     worker_thread_->Start();
     SetMediaChannel(&channel_);
   }