Implement RTCRtpTransceiver.setCodecPreferences

SetCodecPreferences allows clients to filter and reorder codecs in their
SDP offer and answer.

Bug: webrtc:9777
Change-Id: I716bed9b06496629b45210883b286f599c875239
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/129727
Reviewed-by: Steve Anton <steveanton@webrtc.org>
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Commit-Queue: Florent Castelli <orphis@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#27817}
diff --git a/api/rtp_transceiver_interface.cc b/api/rtp_transceiver_interface.cc
index 0504331..dc82fad 100644
--- a/api/rtp_transceiver_interface.cc
+++ b/api/rtp_transceiver_interface.cc
@@ -25,9 +25,15 @@
   return absl::nullopt;
 }
 
-void RtpTransceiverInterface::SetCodecPreferences(
+RTCError RtpTransceiverInterface::SetCodecPreferences(
     rtc::ArrayView<RtpCodecCapability>) {
   RTC_NOTREACHED() << "Not implemented";
+  return {};
+}
+
+std::vector<RtpCodecCapability> RtpTransceiverInterface::codec_preferences()
+    const {
+  return {};
 }
 
 }  // namespace webrtc
diff --git a/api/rtp_transceiver_interface.h b/api/rtp_transceiver_interface.h
index 9b052d1..4606632 100644
--- a/api/rtp_transceiver_interface.h
+++ b/api/rtp_transceiver_interface.h
@@ -129,8 +129,9 @@
   // The SetCodecPreferences method overrides the default codec preferences used
   // by WebRTC for this transceiver.
   // https://w3c.github.io/webrtc-pc/#dom-rtcrtptransceiver-setcodecpreferences
-  // TODO(steveanton): Not implemented.
-  virtual void SetCodecPreferences(rtc::ArrayView<RtpCodecCapability> codecs);
+  virtual RTCError SetCodecPreferences(
+      rtc::ArrayView<RtpCodecCapability> codecs);
+  virtual std::vector<RtpCodecCapability> codec_preferences() const;
 
  protected:
   ~RtpTransceiverInterface() override = default;
diff --git a/media/base/codec.cc b/media/base/codec.cc
index 16ef65d..d0ca29b 100644
--- a/media/base/codec.cc
+++ b/media/base/codec.cc
@@ -99,6 +99,18 @@
              : (absl::EqualsIgnoreCase(name, codec.name));
 }
 
+bool Codec::MatchesCapability(
+    const webrtc::RtpCodecCapability& codec_capability) const {
+  webrtc::RtpCodecParameters codec_parameters = ToCodecParameters();
+
+  return codec_parameters.name == codec_capability.name &&
+         codec_parameters.kind == codec_capability.kind &&
+         (codec_parameters.name == cricket::kRtxCodecName ||
+          (codec_parameters.num_channels == codec_capability.num_channels &&
+           codec_parameters.clock_rate == codec_capability.clock_rate &&
+           codec_parameters.parameters == codec_capability.parameters));
+}
+
 bool Codec::GetParam(const std::string& name, std::string* out) const {
   CodecParameterMap::const_iterator iter = params.find(name);
   if (iter == params.end())
diff --git a/media/base/codec.h b/media/base/codec.h
index b60d001..091adb6 100644
--- a/media/base/codec.h
+++ b/media/base/codec.h
@@ -73,6 +73,7 @@
 
   // Indicates if this codec is compatible with the specified codec.
   bool Matches(const Codec& codec) const;
+  bool MatchesCapability(const webrtc::RtpCodecCapability& capability) const;
 
   // Find the parameter for |name| and write the value to |out|.
   bool GetParam(const std::string& name, std::string* out) const;
diff --git a/media/base/fake_media_engine.cc b/media/base/fake_media_engine.cc
index 9724423..358b51c 100644
--- a/media/base/fake_media_engine.cc
+++ b/media/base/fake_media_engine.cc
@@ -495,7 +495,7 @@
 FakeVoiceEngine::FakeVoiceEngine() : fail_create_channel_(false) {
   // Add a fake audio codec. Note that the name must not be "" as there are
   // sanity checks against that.
-  codecs_.push_back(AudioCodec(101, "fake_audio_codec", 0, 0, 1));
+  SetCodecs({AudioCodec(101, "fake_audio_codec", 0, 0, 1)});
 }
 RtpCapabilities FakeVoiceEngine::GetCapabilities() const {
   return RtpCapabilities();
@@ -524,13 +524,20 @@
   channels_.erase(absl::c_find(channels_, channel));
 }
 const std::vector<AudioCodec>& FakeVoiceEngine::send_codecs() const {
-  return codecs_;
+  return send_codecs_;
 }
 const std::vector<AudioCodec>& FakeVoiceEngine::recv_codecs() const {
-  return codecs_;
+  return recv_codecs_;
 }
 void FakeVoiceEngine::SetCodecs(const std::vector<AudioCodec>& codecs) {
-  codecs_ = codecs;
+  send_codecs_ = codecs;
+  recv_codecs_ = codecs;
+}
+void FakeVoiceEngine::SetRecvCodecs(const std::vector<AudioCodec>& codecs) {
+  recv_codecs_ = codecs;
+}
+void FakeVoiceEngine::SetSendCodecs(const std::vector<AudioCodec>& codecs) {
+  send_codecs_ = codecs;
 }
 int FakeVoiceEngine::GetInputLevel() {
   return 0;
@@ -601,6 +608,14 @@
 void FakeMediaEngine::SetAudioCodecs(const std::vector<AudioCodec>& codecs) {
   voice_->SetCodecs(codecs);
 }
+void FakeMediaEngine::SetAudioRecvCodecs(
+    const std::vector<AudioCodec>& codecs) {
+  voice_->SetRecvCodecs(codecs);
+}
+void FakeMediaEngine::SetAudioSendCodecs(
+    const std::vector<AudioCodec>& codecs) {
+  voice_->SetSendCodecs(codecs);
+}
 void FakeMediaEngine::SetVideoCodecs(const std::vector<VideoCodec>& codecs) {
   video_->SetCodecs(codecs);
 }
diff --git a/media/base/fake_media_engine.h b/media/base/fake_media_engine.h
index f1192c5..b42f896 100644
--- a/media/base/fake_media_engine.h
+++ b/media/base/fake_media_engine.h
@@ -523,6 +523,8 @@
   const std::vector<AudioCodec>& send_codecs() const override;
   const std::vector<AudioCodec>& recv_codecs() const override;
   void SetCodecs(const std::vector<AudioCodec>& codecs);
+  void SetRecvCodecs(const std::vector<AudioCodec>& codecs);
+  void SetSendCodecs(const std::vector<AudioCodec>& codecs);
   int GetInputLevel();
   bool StartAecDump(rtc::PlatformFile file, int64_t max_size_bytes) override;
   void StopAecDump() override;
@@ -531,7 +533,8 @@
 
  private:
   std::vector<FakeVoiceMediaChannel*> channels_;
-  std::vector<AudioCodec> codecs_;
+  std::vector<AudioCodec> recv_codecs_;
+  std::vector<AudioCodec> send_codecs_;
   bool fail_create_channel_;
 
   friend class FakeMediaEngine;
@@ -572,6 +575,8 @@
   ~FakeMediaEngine() override;
 
   void SetAudioCodecs(const std::vector<AudioCodec>& codecs);
+  void SetAudioRecvCodecs(const std::vector<AudioCodec>& codecs);
+  void SetAudioSendCodecs(const std::vector<AudioCodec>& codecs);
   void SetVideoCodecs(const std::vector<VideoCodec>& codecs);
 
   FakeVoiceMediaChannel* GetVoiceChannel(size_t index);
diff --git a/pc/media_session.cc b/pc/media_session.cc
index 8377f10..0eace22 100644
--- a/pc/media_session.cc
+++ b/pc/media_session.cc
@@ -799,7 +799,8 @@
 template <class C>
 static void NegotiateCodecs(const std::vector<C>& local_codecs,
                             const std::vector<C>& offered_codecs,
-                            std::vector<C>* negotiated_codecs) {
+                            std::vector<C>* negotiated_codecs,
+                            bool keep_offer_order) {
   for (const C& ours : local_codecs) {
     C theirs;
     // Note that we intentionally only find one matching codec for each of our
@@ -823,19 +824,22 @@
       negotiated_codecs->push_back(std::move(negotiated));
     }
   }
-  // RFC3264: Although the answerer MAY list the formats in their desired
-  // order of preference, it is RECOMMENDED that unless there is a
-  // specific reason, the answerer list formats in the same relative order
-  // they were present in the offer.
-  std::unordered_map<int, int> payload_type_preferences;
-  int preference = static_cast<int>(offered_codecs.size() + 1);
-  for (const C& codec : offered_codecs) {
-    payload_type_preferences[codec.id] = preference--;
+  if (keep_offer_order) {
+    // RFC3264: Although the answerer MAY list the formats in their desired
+    // order of preference, it is RECOMMENDED that unless there is a
+    // specific reason, the answerer list formats in the same relative order
+    // they were present in the offer.
+    // This can be skipped when the transceiver has any codec preferences.
+    std::unordered_map<int, int> payload_type_preferences;
+    int preference = static_cast<int>(offered_codecs.size() + 1);
+    for (const C& codec : offered_codecs) {
+      payload_type_preferences[codec.id] = preference--;
+    }
+    absl::c_sort(*negotiated_codecs, [&payload_type_preferences](const C& a,
+                                                                 const C& b) {
+      return payload_type_preferences[a.id] > payload_type_preferences[b.id];
+    });
   }
-  absl::c_sort(
-      *negotiated_codecs, [&payload_type_preferences](const C& a, const C& b) {
-        return payload_type_preferences[a.id] > payload_type_preferences[b.id];
-      });
 }
 
 // Finds a codec in |codecs2| that matches |codec_to_match|, which is
@@ -954,6 +958,51 @@
   }
 }
 
+template <typename Codecs>
+static Codecs MatchCodecPreference(
+    const std::vector<webrtc::RtpCodecCapability>& codec_preferences,
+    const Codecs& codecs) {
+  Codecs filtered_codecs;
+  std::set<std::string> kept_codecs_ids;
+  bool want_rtx = false;
+
+  for (const auto& codec_preference : codec_preferences) {
+    auto found_codec = absl::c_find_if(
+        codecs, [&codec_preference](const typename Codecs::value_type& codec) {
+          webrtc::RtpCodecParameters codec_parameters =
+              codec.ToCodecParameters();
+          return codec_parameters.name == codec_preference.name &&
+                 codec_parameters.kind == codec_preference.kind &&
+                 codec_parameters.num_channels ==
+                     codec_preference.num_channels &&
+                 codec_parameters.clock_rate == codec_preference.clock_rate &&
+                 codec_parameters.parameters == codec_preference.parameters;
+        });
+
+    if (found_codec != codecs.end()) {
+      filtered_codecs.push_back(*found_codec);
+      kept_codecs_ids.insert(std::to_string(found_codec->id));
+    } else if (IsRtxCodec(codec_preference)) {
+      want_rtx = true;
+    }
+  }
+
+  if (want_rtx) {
+    for (const auto& codec : codecs) {
+      if (IsRtxCodec(codec)) {
+        const auto apt =
+            codec.params.find(cricket::kCodecParamAssociatedPayloadType);
+        if (apt != codec.params.end() &&
+            kept_codecs_ids.count(apt->second) > 0) {
+          filtered_codecs.push_back(codec);
+        }
+      }
+    }
+  }
+
+  return filtered_codecs;
+}
+
 static bool FindByUriAndEncryption(const RtpHeaderExtensions& extensions,
                                    const webrtc::RtpExtension& ext_to_match,
                                    webrtc::RtpExtension* found_extension) {
@@ -1159,7 +1208,8 @@
     bool bundle_enabled,
     MediaContentDescriptionImpl<C>* answer) {
   std::vector<C> negotiated_codecs;
-  NegotiateCodecs(local_codecs, offer->codecs(), &negotiated_codecs);
+  NegotiateCodecs(local_codecs, offer->codecs(), &negotiated_codecs,
+                  media_description_options.codec_preferences.empty());
   answer->AddCodecs(negotiated_codecs);
   answer->set_protocol(offer->protocol());
 
@@ -2009,30 +2059,38 @@
       GetAudioCodecsForOffer(media_description_options.direction);
 
   AudioCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not rejected nor
-  // recycled.
-  if (current_content && !current_content->rejected &&
-      current_content->name == media_description_options.mid) {
-    RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
-    const AudioContentDescription* acd =
-        current_content->media_description()->as_audio();
-    for (const AudioCodec& codec : acd->codecs()) {
-      if (FindMatchingCodec<AudioCodec>(acd->codecs(), audio_codecs, codec,
-                                        nullptr)) {
-        filtered_codecs.push_back(codec);
+
+  if (!media_description_options.codec_preferences.empty()) {
+    // Add the codecs from the current transceiver's codec preferences.
+    // They override any existing codecs from previous negotiations.
+    filtered_codecs = MatchCodecPreference(
+        media_description_options.codec_preferences, supported_audio_codecs);
+  } else {
+    // Add the codecs from current content if it exists and is not rejected nor
+    // recycled.
+    if (current_content && !current_content->rejected &&
+        current_content->name == media_description_options.mid) {
+      RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
+      const AudioContentDescription* acd =
+          current_content->media_description()->as_audio();
+      for (const AudioCodec& codec : acd->codecs()) {
+        if (FindMatchingCodec<AudioCodec>(acd->codecs(), audio_codecs, codec,
+                                          nullptr)) {
+          filtered_codecs.push_back(codec);
+        }
       }
     }
-  }
-  // Add other supported audio codecs.
-  AudioCodec found_codec;
-  for (const AudioCodec& codec : supported_audio_codecs) {
-    if (FindMatchingCodec<AudioCodec>(supported_audio_codecs, audio_codecs,
-                                      codec, &found_codec) &&
-        !FindMatchingCodec<AudioCodec>(supported_audio_codecs, filtered_codecs,
-                                       codec, nullptr)) {
-      // Use the |found_codec| from |audio_codecs| because it has the correctly
-      // mapped payload type.
-      filtered_codecs.push_back(found_codec);
+    // Add other supported audio codecs.
+    AudioCodec found_codec;
+    for (const AudioCodec& codec : supported_audio_codecs) {
+      if (FindMatchingCodec<AudioCodec>(supported_audio_codecs, audio_codecs,
+                                        codec, &found_codec) &&
+          !FindMatchingCodec<AudioCodec>(supported_audio_codecs,
+                                         filtered_codecs, codec, nullptr)) {
+        // Use the |found_codec| from |audio_codecs| because it has the
+        // correctly mapped payload type.
+        filtered_codecs.push_back(found_codec);
+      }
     }
   }
 
@@ -2088,30 +2146,38 @@
                                         &crypto_suites);
 
   VideoCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not rejected nor
-  // recycled.
-  if (current_content && !current_content->rejected &&
-      current_content->name == media_description_options.mid) {
-    RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
-    const VideoContentDescription* vcd =
-        current_content->media_description()->as_video();
-    for (const VideoCodec& codec : vcd->codecs()) {
-      if (FindMatchingCodec<VideoCodec>(vcd->codecs(), video_codecs, codec,
-                                        nullptr)) {
-        filtered_codecs.push_back(codec);
+
+  if (!media_description_options.codec_preferences.empty()) {
+    // Add the codecs from the current transceiver's codec preferences.
+    // They override any existing codecs from previous negotiations.
+    filtered_codecs = MatchCodecPreference(
+        media_description_options.codec_preferences, video_codecs_);
+  } else {
+    // Add the codecs from current content if it exists and is not rejected nor
+    // recycled.
+    if (current_content && !current_content->rejected &&
+        current_content->name == media_description_options.mid) {
+      RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
+      const VideoContentDescription* vcd =
+          current_content->media_description()->as_video();
+      for (const VideoCodec& codec : vcd->codecs()) {
+        if (FindMatchingCodec<VideoCodec>(vcd->codecs(), video_codecs, codec,
+                                          nullptr)) {
+          filtered_codecs.push_back(codec);
+        }
       }
     }
-  }
-  // Add other supported video codecs.
-  VideoCodec found_codec;
-  for (const VideoCodec& codec : video_codecs_) {
-    if (FindMatchingCodec<VideoCodec>(video_codecs_, video_codecs, codec,
-                                      &found_codec) &&
-        !FindMatchingCodec<VideoCodec>(video_codecs_, filtered_codecs, codec,
-                                       nullptr)) {
-      // Use the |found_codec| from |video_codecs| because it has the correctly
-      // mapped payload type.
-      filtered_codecs.push_back(found_codec);
+    // Add other supported video codecs.
+    VideoCodec found_codec;
+    for (const VideoCodec& codec : video_codecs_) {
+      if (FindMatchingCodec<VideoCodec>(video_codecs_, video_codecs, codec,
+                                        &found_codec) &&
+          !FindMatchingCodec<VideoCodec>(video_codecs_, filtered_codecs, codec,
+                                         nullptr)) {
+        // Use the |found_codec| from |video_codecs| because it has the
+        // correctly mapped payload type.
+        filtered_codecs.push_back(found_codec);
+      }
     }
   }
 
@@ -2254,29 +2320,35 @@
       GetAudioCodecsForAnswer(offer_rtd, answer_rtd);
 
   AudioCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not rejected nor
-  // recycled.
-  if (current_content && !current_content->rejected &&
-      current_content->name == media_description_options.mid) {
-    RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
-    const AudioContentDescription* acd =
-        current_content->media_description()->as_audio();
-    for (const AudioCodec& codec : acd->codecs()) {
-      if (FindMatchingCodec<AudioCodec>(acd->codecs(), audio_codecs, codec,
-                                        nullptr)) {
-        filtered_codecs.push_back(codec);
+
+  if (!media_description_options.codec_preferences.empty()) {
+    filtered_codecs = MatchCodecPreference(
+        media_description_options.codec_preferences, supported_audio_codecs);
+  } else {
+    // Add the codecs from current content if it exists and is not rejected nor
+    // recycled.
+    if (current_content && !current_content->rejected &&
+        current_content->name == media_description_options.mid) {
+      RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_AUDIO));
+      const AudioContentDescription* acd =
+          current_content->media_description()->as_audio();
+      for (const AudioCodec& codec : acd->codecs()) {
+        if (FindMatchingCodec<AudioCodec>(acd->codecs(), audio_codecs, codec,
+                                          nullptr)) {
+          filtered_codecs.push_back(codec);
+        }
       }
     }
-  }
-  // Add other supported audio codecs.
-  for (const AudioCodec& codec : supported_audio_codecs) {
-    if (FindMatchingCodec<AudioCodec>(supported_audio_codecs, audio_codecs,
-                                      codec, nullptr) &&
-        !FindMatchingCodec<AudioCodec>(supported_audio_codecs, filtered_codecs,
-                                       codec, nullptr)) {
-      // We should use the local codec with local parameters and the codec id
-      // would be correctly mapped in |NegotiateCodecs|.
-      filtered_codecs.push_back(codec);
+    // Add other supported audio codecs.
+    for (const AudioCodec& codec : supported_audio_codecs) {
+      if (FindMatchingCodec<AudioCodec>(supported_audio_codecs, audio_codecs,
+                                        codec, nullptr) &&
+          !FindMatchingCodec<AudioCodec>(supported_audio_codecs,
+                                         filtered_codecs, codec, nullptr)) {
+        // We should use the local codec with local parameters and the codec id
+        // would be correctly mapped in |NegotiateCodecs|.
+        filtered_codecs.push_back(codec);
+      }
     }
   }
 
@@ -2342,29 +2414,35 @@
   }
 
   VideoCodecs filtered_codecs;
-  // Add the codecs from current content if it exists and is not rejected nor
-  // recycled.
-  if (current_content && !current_content->rejected &&
-      current_content->name == media_description_options.mid) {
-    RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
-    const VideoContentDescription* vcd =
-        current_content->media_description()->as_video();
-    for (const VideoCodec& codec : vcd->codecs()) {
-      if (FindMatchingCodec<VideoCodec>(vcd->codecs(), video_codecs, codec,
-                                        nullptr)) {
-        filtered_codecs.push_back(codec);
+
+  if (!media_description_options.codec_preferences.empty()) {
+    filtered_codecs = MatchCodecPreference(
+        media_description_options.codec_preferences, video_codecs_);
+  } else {
+    // Add the codecs from current content if it exists and is not rejected nor
+    // recycled.
+    if (current_content && !current_content->rejected &&
+        current_content->name == media_description_options.mid) {
+      RTC_CHECK(IsMediaContentOfType(current_content, MEDIA_TYPE_VIDEO));
+      const VideoContentDescription* vcd =
+          current_content->media_description()->as_video();
+      for (const VideoCodec& codec : vcd->codecs()) {
+        if (FindMatchingCodec<VideoCodec>(vcd->codecs(), video_codecs, codec,
+                                          nullptr)) {
+          filtered_codecs.push_back(codec);
+        }
       }
     }
-  }
-  // Add other supported video codecs.
-  for (const VideoCodec& codec : video_codecs_) {
-    if (FindMatchingCodec<VideoCodec>(video_codecs_, video_codecs, codec,
-                                      nullptr) &&
-        !FindMatchingCodec<VideoCodec>(video_codecs_, filtered_codecs, codec,
-                                       nullptr)) {
-      // We should use the local codec with local parameters and the codec id
-      // would be correctly mapped in |NegotiateCodecs|.
-      filtered_codecs.push_back(codec);
+    // Add other supported video codecs.
+    for (const VideoCodec& codec : video_codecs_) {
+      if (FindMatchingCodec<VideoCodec>(video_codecs_, video_codecs, codec,
+                                        nullptr) &&
+          !FindMatchingCodec<VideoCodec>(video_codecs_, filtered_codecs, codec,
+                                         nullptr)) {
+        // We should use the local codec with local parameters and the codec id
+        // would be correctly mapped in |NegotiateCodecs|.
+        filtered_codecs.push_back(codec);
+      }
     }
   }
 
@@ -2499,7 +2577,7 @@
   // expensive than decoding, and prioritizing a codec in the send list probably
   // means it's a codec we can handle efficiently.
   NegotiateCodecs(audio_recv_codecs_, audio_send_codecs_,
-                  &audio_sendrecv_codecs_);
+                  &audio_sendrecv_codecs_, true);
 }
 
 bool IsMediaContent(const ContentInfo* content) {
diff --git a/pc/media_session.h b/pc/media_session.h
index 33c8c17..a369756 100644
--- a/pc/media_session.h
+++ b/pc/media_session.h
@@ -76,6 +76,7 @@
   // Note: There's no equivalent "RtpReceiverOptions" because only send
   // stream information goes in the local descriptions.
   std::vector<SenderOptions> sender_options;
+  std::vector<webrtc::RtpCodecCapability> codec_preferences;
 
  private:
   // Doesn't DCHECK on |type|.
diff --git a/pc/peer_connection.cc b/pc/peer_connection.cc
index f7cfc9a..8a6d0e5 100644
--- a/pc/peer_connection.cc
+++ b/pc/peer_connection.cc
@@ -1703,7 +1703,8 @@
   // could be invalid, but should not cause a crash).
   RTC_DCHECK(!FindSenderById(sender->id()));
   auto transceiver = RtpTransceiverProxyWithInternal<RtpTransceiver>::Create(
-      signaling_thread(), new RtpTransceiver(sender, receiver));
+      signaling_thread(),
+      new RtpTransceiver(sender, receiver, channel_manager()));
   transceivers_.push_back(transceiver);
   transceiver->internal()->SignalNegotiationNeeded.connect(
       this, &PeerConnection::OnNegotiationNeeded);
@@ -4397,6 +4398,8 @@
   cricket::MediaDescriptionOptions media_description_options(
       transceiver->media_type(), mid, transceiver->direction(),
       transceiver->stopped());
+  media_description_options.codec_preferences =
+      transceiver->codec_preferences();
   // This behavior is specified in JSEP. The gist is that:
   // 1. The MSID is included if the RtpTransceiver's direction is sendonly or
   //    sendrecv.
diff --git a/pc/peer_connection_media_unittest.cc b/pc/peer_connection_media_unittest.cc
index c81d511..e9e3a00 100644
--- a/pc/peer_connection_media_unittest.cc
+++ b/pc/peer_connection_media_unittest.cc
@@ -73,11 +73,21 @@
     return CreatePeerConnection(RTCConfiguration());
   }
 
+  WrapperPtr CreatePeerConnection(const RTCConfiguration& config) {
+    return CreatePeerConnection(config, absl::make_unique<FakeMediaEngine>());
+  }
+
+  WrapperPtr CreatePeerConnection(
+      std::unique_ptr<FakeMediaEngine> media_engine) {
+    return CreatePeerConnection(RTCConfiguration(), std::move(media_engine));
+  }
+
   // Creates PeerConnectionFactory and PeerConnection for given configuration.
   // Note that PeerConnectionFactory is created with MediaTransportFactory,
   // because some tests pass config.use_media_transport = true.
-  WrapperPtr CreatePeerConnection(const RTCConfiguration& config) {
-    auto media_engine = absl::make_unique<FakeMediaEngine>();
+  WrapperPtr CreatePeerConnection(
+      const RTCConfiguration& config,
+      std::unique_ptr<FakeMediaEngine> media_engine) {
     auto* media_engine_ptr = media_engine.get();
 
     PeerConnectionFactoryDependencies factory_dependencies;
@@ -125,6 +135,18 @@
     return wrapper;
   }
 
+  // Accepts the same arguments as CreatePeerConnection and adds default video
+  // track (but no audio).
+  template <typename... Args>
+  WrapperPtr CreatePeerConnectionWithVideo(Args&&... args) {
+    auto wrapper = CreatePeerConnection(std::forward<Args>(args)...);
+    if (!wrapper) {
+      return nullptr;
+    }
+    wrapper->AddVideoTrack("v");
+    return wrapper;
+  }
+
   // Accepts the same arguments as CreatePeerConnection and adds default audio
   // and video tracks.
   template <typename... Args>
@@ -1239,6 +1261,498 @@
   ASSERT_EQ(nullptr, callee_video->media_transport());
 }
 
+template <typename C>
+bool CompareCodecs(const std::vector<webrtc::RtpCodecCapability>& capabilities,
+                   const std::vector<C>& codecs) {
+  bool capability_has_rtx =
+      absl::c_any_of(capabilities, [](const webrtc::RtpCodecCapability& codec) {
+        return codec.name == cricket::kRtxCodecName;
+      });
+  bool codecs_has_rtx = absl::c_any_of(codecs, [](const C& codec) {
+    return codec.name == cricket::kRtxCodecName;
+  });
+
+  std::vector<C> codecs_no_rtx;
+  absl::c_copy_if(
+      codecs, std::back_inserter(codecs_no_rtx),
+      [](const C& codec) { return codec.name != cricket::kRtxCodecName; });
+
+  std::vector<webrtc::RtpCodecCapability> capabilities_no_rtx;
+  absl::c_copy_if(capabilities, std::back_inserter(capabilities_no_rtx),
+                  [](const webrtc::RtpCodecCapability& codec) {
+                    return codec.name != cricket::kRtxCodecName;
+                  });
+
+  return capability_has_rtx == codecs_has_rtx &&
+         absl::c_equal(
+             capabilities_no_rtx, codecs_no_rtx,
+             [](const webrtc::RtpCodecCapability& capability, const C& codec) {
+               return codec.MatchesCapability(capability);
+             });
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesAudioMissingRecvCodec) {
+  auto fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto send_codecs = fake_engine->voice().send_codecs();
+  send_codecs.push_back(cricket::AudioCodec(send_codecs.back().id + 1,
+                                            "send_only_codec", 0, 0, 1));
+  fake_engine->SetAudioSendCodecs(send_codecs);
+
+  auto caller = CreatePeerConnectionWithAudio(std::move(fake_engine));
+
+  auto transceiver = caller->pc()->GetTransceivers().front();
+  auto capabilities = caller->pc_factory()->GetRtpSenderCapabilities(
+      cricket::MediaType::MEDIA_TYPE_AUDIO);
+
+  std::vector<webrtc::RtpCodecCapability> codecs;
+  absl::c_copy_if(capabilities.codecs, std::back_inserter(codecs),
+                  [](const webrtc::RtpCodecCapability& codec) {
+                    return codec.name.find("_only_") != std::string::npos;
+                  });
+
+  auto result = transceiver->SetCodecPreferences(codecs);
+  EXPECT_EQ(RTCErrorType::INVALID_MODIFICATION, result.type());
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesAudioMissingSendCodec) {
+  auto fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto recv_codecs = fake_engine->voice().recv_codecs();
+  recv_codecs.push_back(cricket::AudioCodec(recv_codecs.back().id + 1,
+                                            "recv_only_codec", 0, 0, 1));
+  fake_engine->SetAudioRecvCodecs(recv_codecs);
+  auto caller = CreatePeerConnectionWithAudio(std::move(fake_engine));
+
+  auto transceiver = caller->pc()->GetTransceivers().front();
+  auto capabilities = caller->pc_factory()->GetRtpReceiverCapabilities(
+      cricket::MediaType::MEDIA_TYPE_AUDIO);
+
+  std::vector<webrtc::RtpCodecCapability> codecs;
+  absl::c_copy_if(capabilities.codecs, std::back_inserter(codecs),
+                  [](const webrtc::RtpCodecCapability& codec) {
+                    return codec.name.find("_only_") != std::string::npos;
+                  });
+
+  auto result = transceiver->SetCodecPreferences(codecs);
+  EXPECT_EQ(RTCErrorType::INVALID_MODIFICATION, result.type());
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesAudioRejectsVideoCodec) {
+  auto caller = CreatePeerConnectionWithAudio();
+
+  auto transceiver = caller->pc()->GetTransceivers().front();
+  auto video_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_VIDEO)
+          .codecs;
+  auto codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_AUDIO)
+          .codecs;
+  codecs.insert(codecs.end(), video_codecs.begin(), video_codecs.end());
+  auto result = transceiver->SetCodecPreferences(codecs);
+  EXPECT_EQ(RTCErrorType::INVALID_MODIFICATION, result.type());
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesAudioRejectsOnlyRtxRedFec) {
+  auto fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto audio_codecs = fake_engine->voice().send_codecs();
+  audio_codecs.push_back(cricket::AudioCodec(audio_codecs.back().id + 1,
+                                             cricket::kRtxCodecName, 0, 0, 1));
+  audio_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(audio_codecs.back().id - 1);
+  audio_codecs.push_back(cricket::AudioCodec(audio_codecs.back().id + 1,
+                                             cricket::kRedCodecName, 0, 0, 1));
+  audio_codecs.push_back(cricket::AudioCodec(
+      audio_codecs.back().id + 1, cricket::kUlpfecCodecName, 0, 0, 1));
+  fake_engine->SetAudioCodecs(audio_codecs);
+
+  auto caller = CreatePeerConnectionWithAudio(std::move(fake_engine));
+
+  auto transceiver = caller->pc()->GetTransceivers().front();
+  auto codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_AUDIO)
+          .codecs;
+  auto codecs_only_rtx_red_fec = codecs;
+  auto it = std::remove_if(codecs_only_rtx_red_fec.begin(),
+                           codecs_only_rtx_red_fec.end(),
+                           [](const webrtc::RtpCodecCapability& codec) {
+                             return !(codec.name == cricket::kRtxCodecName ||
+                                      codec.name == cricket::kRedCodecName ||
+                                      codec.name == cricket::kUlpfecCodecName);
+                           });
+  codecs_only_rtx_red_fec.erase(it, codecs_only_rtx_red_fec.end());
+
+  auto result = transceiver->SetCodecPreferences(codecs_only_rtx_red_fec);
+  EXPECT_EQ(RTCErrorType::INVALID_MODIFICATION, result.type());
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan, SetCodecPreferencesAllAudioCodecs) {
+  auto caller = CreatePeerConnectionWithAudio();
+
+  auto sender_audio_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_AUDIO)
+          .codecs;
+
+  auto audio_transceiver = caller->pc()->GetTransceivers().front();
+
+  // Normal case, set all capabilities as preferences
+  EXPECT_TRUE(audio_transceiver->SetCodecPreferences(sender_audio_codecs).ok());
+  auto offer = caller->CreateOffer();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_audio()
+                    ->codecs();
+  EXPECT_TRUE(CompareCodecs(sender_audio_codecs, codecs));
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesResetAudioCodecs) {
+  auto caller = CreatePeerConnectionWithAudio();
+
+  auto sender_audio_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_AUDIO)
+          .codecs;
+  std::vector<webrtc::RtpCodecCapability> empty_codecs = {};
+
+  auto audio_transceiver = caller->pc()->GetTransceivers().front();
+
+  // Normal case, reset codec preferences
+  EXPECT_TRUE(audio_transceiver->SetCodecPreferences(empty_codecs).ok());
+  auto offer = caller->CreateOffer();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_audio()
+                    ->codecs();
+  EXPECT_TRUE(CompareCodecs(sender_audio_codecs, codecs));
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesVideoRejectsAudioCodec) {
+  auto caller = CreatePeerConnectionWithVideo();
+
+  auto transceiver = caller->pc()->GetTransceivers().front();
+  auto audio_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_AUDIO)
+          .codecs;
+  auto codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_VIDEO)
+          .codecs;
+  codecs.insert(codecs.end(), audio_codecs.begin(), audio_codecs.end());
+  auto result = transceiver->SetCodecPreferences(codecs);
+  EXPECT_EQ(RTCErrorType::INVALID_MODIFICATION, result.type());
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesVideoRejectsOnlyRtxRedFec) {
+  auto fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto video_codecs = fake_engine->video().codecs();
+  video_codecs.push_back(
+      cricket::VideoCodec(video_codecs.back().id + 1, cricket::kRtxCodecName));
+  video_codecs.push_back(
+      cricket::VideoCodec(video_codecs.back().id + 1, cricket::kRedCodecName));
+  video_codecs.push_back(cricket::VideoCodec(video_codecs.back().id + 1,
+                                             cricket::kUlpfecCodecName));
+  fake_engine->SetVideoCodecs(video_codecs);
+
+  auto caller = CreatePeerConnectionWithVideo(std::move(fake_engine));
+
+  auto transceiver = caller->pc()->GetTransceivers().front();
+  auto codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MediaType::MEDIA_TYPE_VIDEO)
+          .codecs;
+  auto codecs_only_rtx_red_fec = codecs;
+  auto it = std::remove_if(codecs_only_rtx_red_fec.begin(),
+                           codecs_only_rtx_red_fec.end(),
+                           [](const webrtc::RtpCodecCapability& codec) {
+                             return !(codec.name == cricket::kRtxCodecName ||
+                                      codec.name == cricket::kRedCodecName ||
+                                      codec.name == cricket::kUlpfecCodecName);
+                           });
+  codecs_only_rtx_red_fec.erase(it, codecs_only_rtx_red_fec.end());
+
+  auto result = transceiver->SetCodecPreferences(codecs_only_rtx_red_fec);
+  EXPECT_EQ(RTCErrorType::INVALID_MODIFICATION, result.type());
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan, SetCodecPreferencesAllVideoCodecs) {
+  auto caller = CreatePeerConnectionWithVideo();
+
+  auto sender_video_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_VIDEO)
+          .codecs;
+
+  auto video_transceiver = caller->pc()->GetTransceivers().front();
+
+  // Normal case, setting preferences to normal capabilities
+  EXPECT_TRUE(video_transceiver->SetCodecPreferences(sender_video_codecs).ok());
+  auto offer = caller->CreateOffer();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_video()
+                    ->codecs();
+  EXPECT_TRUE(CompareCodecs(sender_video_codecs, codecs));
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesResetVideoCodecs) {
+  auto caller = CreatePeerConnectionWithVideo();
+
+  auto sender_video_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_VIDEO)
+          .codecs;
+
+  std::vector<webrtc::RtpCodecCapability> empty_codecs = {};
+
+  auto video_transceiver = caller->pc()->GetTransceivers().front();
+
+  // Normal case, resetting preferences with empty list of codecs
+  EXPECT_TRUE(video_transceiver->SetCodecPreferences(empty_codecs).ok());
+  auto offer = caller->CreateOffer();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_video()
+                    ->codecs();
+  EXPECT_TRUE(CompareCodecs(sender_video_codecs, codecs));
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesVideoCodecDuplicatesRemoved) {
+  auto caller = CreatePeerConnectionWithVideo();
+
+  auto sender_video_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_VIDEO)
+          .codecs;
+
+  auto video_transceiver = caller->pc()->GetTransceivers().front();
+
+  // Check duplicates are removed
+  auto single_codec = sender_video_codecs;
+  single_codec.resize(1);
+  auto duplicate_codec = single_codec;
+  duplicate_codec.push_back(duplicate_codec.front());
+  duplicate_codec.push_back(duplicate_codec.front());
+  duplicate_codec.push_back(duplicate_codec.front());
+
+  EXPECT_TRUE(video_transceiver->SetCodecPreferences(duplicate_codec).ok());
+  auto offer = caller->CreateOffer();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_video()
+                    ->codecs();
+  EXPECT_TRUE(CompareCodecs(single_codec, codecs));
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan, SetCodecPreferencesVideoWithRtx) {
+  auto caller_fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto caller_video_codecs = caller_fake_engine->video().codecs();
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kVp8CodecName));
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kRtxCodecName));
+  caller_video_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(caller_video_codecs.back().id - 1);
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kVp9CodecName));
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kRtxCodecName));
+  caller_video_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(caller_video_codecs.back().id - 1);
+  caller_fake_engine->SetVideoCodecs(caller_video_codecs);
+
+  auto caller = CreatePeerConnectionWithVideo(std::move(caller_fake_engine));
+
+  auto sender_video_codecs =
+      caller->pc_factory()
+          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_VIDEO)
+          .codecs;
+
+  auto video_transceiver = caller->pc()->GetTransceivers().front();
+
+  // Check that RTX codec is properly added
+  auto video_codecs_vpx_rtx = sender_video_codecs;
+  auto it =
+      std::remove_if(video_codecs_vpx_rtx.begin(), video_codecs_vpx_rtx.end(),
+                     [](const webrtc::RtpCodecCapability& codec) {
+                       return codec.name != cricket::kRtxCodecName &&
+                              codec.name != cricket::kVp8CodecName &&
+                              codec.name != cricket::kVp9CodecName;
+                     });
+  video_codecs_vpx_rtx.erase(it, video_codecs_vpx_rtx.end());
+  absl::c_reverse(video_codecs_vpx_rtx);
+  EXPECT_EQ(video_codecs_vpx_rtx.size(), 3u);  // VP8, VP9, RTX
+  EXPECT_TRUE(
+      video_transceiver->SetCodecPreferences(video_codecs_vpx_rtx).ok());
+  auto offer = caller->CreateOffer();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_video()
+                    ->codecs();
+
+  EXPECT_TRUE(CompareCodecs(video_codecs_vpx_rtx, codecs));
+  EXPECT_EQ(codecs.size(), 4u);
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesVideoCodecsNegotiation) {
+  auto caller_fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto caller_video_codecs = caller_fake_engine->video().codecs();
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kVp8CodecName));
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kRtxCodecName));
+  caller_video_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(caller_video_codecs.back().id - 1);
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kVp9CodecName));
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kRtxCodecName));
+  caller_video_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(caller_video_codecs.back().id - 1);
+  caller_fake_engine->SetVideoCodecs(caller_video_codecs);
+
+  auto callee_fake_engine = absl::make_unique<FakeMediaEngine>();
+  callee_fake_engine->SetVideoCodecs(caller_video_codecs);
+
+  auto caller = CreatePeerConnectionWithVideo(std::move(caller_fake_engine));
+  auto callee = CreatePeerConnection(std::move(callee_fake_engine));
+
+  auto video_codecs = caller->pc_factory()
+                          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_VIDEO)
+                          .codecs;
+
+  auto send_transceiver = caller->pc()->GetTransceivers().front();
+
+  auto video_codecs_vpx = video_codecs;
+  auto it = std::remove_if(video_codecs_vpx.begin(), video_codecs_vpx.end(),
+                           [](const webrtc::RtpCodecCapability& codec) {
+                             return codec.name != cricket::kVp8CodecName &&
+                                    codec.name != cricket::kVp9CodecName;
+                           });
+  video_codecs_vpx.erase(it, video_codecs_vpx.end());
+  EXPECT_EQ(video_codecs_vpx.size(), 2u);  // VP8, VP9
+  EXPECT_TRUE(send_transceiver->SetCodecPreferences(video_codecs_vpx).ok());
+
+  auto offer = caller->CreateOfferAndSetAsLocal();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_video()
+                    ->codecs();
+
+  EXPECT_EQ(codecs.size(), 2u);  // VP8, VP9
+  EXPECT_TRUE(CompareCodecs(video_codecs_vpx, codecs));
+
+  callee->SetRemoteDescription(std::move(offer));
+
+  auto recv_transceiver = callee->pc()->GetTransceivers().front();
+  auto video_codecs_vp8_rtx = video_codecs;
+  it = std::remove_if(video_codecs_vp8_rtx.begin(), video_codecs_vp8_rtx.end(),
+                      [](const webrtc::RtpCodecCapability& codec) {
+                        bool r = codec.name != cricket::kVp8CodecName &&
+                                 codec.name != cricket::kRtxCodecName;
+                        return r;
+                      });
+  video_codecs_vp8_rtx.erase(it, video_codecs_vp8_rtx.end());
+  EXPECT_EQ(video_codecs_vp8_rtx.size(), 2u);  // VP8, RTX
+  recv_transceiver->SetCodecPreferences(video_codecs_vp8_rtx);
+
+  auto answer = callee->CreateAnswerAndSetAsLocal();
+
+  auto recv_codecs = answer->description()
+                         ->contents()[0]
+                         .media_description()
+                         ->as_video()
+                         ->codecs();
+  EXPECT_EQ(recv_codecs.size(), 1u);  // VP8
+}
+
+TEST_F(PeerConnectionMediaTestUnifiedPlan,
+       SetCodecPreferencesVideoCodecsNegotiationReverseOrder) {
+  auto caller_fake_engine = absl::make_unique<FakeMediaEngine>();
+  auto caller_video_codecs = caller_fake_engine->video().codecs();
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kVp8CodecName));
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kRtxCodecName));
+  caller_video_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(caller_video_codecs.back().id - 1);
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kVp9CodecName));
+  caller_video_codecs.push_back(cricket::VideoCodec(
+      caller_video_codecs.back().id + 1, cricket::kRtxCodecName));
+  caller_video_codecs.back().params[cricket::kCodecParamAssociatedPayloadType] =
+      std::to_string(caller_video_codecs.back().id - 1);
+  caller_fake_engine->SetVideoCodecs(caller_video_codecs);
+
+  auto callee_fake_engine = absl::make_unique<FakeMediaEngine>();
+  callee_fake_engine->SetVideoCodecs(caller_video_codecs);
+
+  auto caller = CreatePeerConnectionWithVideo(std::move(caller_fake_engine));
+  auto callee = CreatePeerConnection(std::move(callee_fake_engine));
+
+  auto video_codecs = caller->pc_factory()
+                          ->GetRtpSenderCapabilities(cricket::MEDIA_TYPE_VIDEO)
+                          .codecs;
+
+  auto send_transceiver = caller->pc()->GetTransceivers().front();
+
+  auto video_codecs_vpx = video_codecs;
+  auto it = std::remove_if(video_codecs_vpx.begin(), video_codecs_vpx.end(),
+                           [](const webrtc::RtpCodecCapability& codec) {
+                             return codec.name != cricket::kVp8CodecName &&
+                                    codec.name != cricket::kVp9CodecName;
+                           });
+  video_codecs_vpx.erase(it, video_codecs_vpx.end());
+  EXPECT_EQ(video_codecs_vpx.size(), 2u);  // VP8, VP9
+  EXPECT_TRUE(send_transceiver->SetCodecPreferences(video_codecs_vpx).ok());
+
+  auto video_codecs_vpx_reverse = video_codecs_vpx;
+  absl::c_reverse(video_codecs_vpx_reverse);
+
+  auto offer = caller->CreateOfferAndSetAsLocal();
+  auto codecs = offer->description()
+                    ->contents()[0]
+                    .media_description()
+                    ->as_video()
+                    ->codecs();
+  EXPECT_EQ(codecs.size(), 2u);  // VP9, VP8
+  EXPECT_TRUE(CompareCodecs(video_codecs_vpx, codecs));
+
+  callee->SetRemoteDescription(std::move(offer));
+
+  auto recv_transceiver = callee->pc()->GetTransceivers().front();
+  recv_transceiver->SetCodecPreferences(video_codecs_vpx_reverse);
+
+  auto answer = callee->CreateAnswerAndSetAsLocal();
+
+  auto recv_codecs = answer->description()
+                         ->contents()[0]
+                         .media_description()
+                         ->as_video()
+                         ->codecs();
+
+  EXPECT_TRUE(CompareCodecs(video_codecs_vpx_reverse, recv_codecs));
+}
+
 INSTANTIATE_TEST_SUITE_P(PeerConnectionMediaTest,
                          PeerConnectionMediaTest,
                          Values(SdpSemantics::kPlanB,
diff --git a/pc/rtp_transceiver.cc b/pc/rtp_transceiver.cc
index a000b58..d8d1681 100644
--- a/pc/rtp_transceiver.cc
+++ b/pc/rtp_transceiver.cc
@@ -13,7 +13,9 @@
 #include <string>
 
 #include "absl/algorithm/container.h"
+#include "pc/channel_manager.h"
 #include "pc/rtp_media_utils.h"
+#include "pc/rtp_parameters_conversion.h"
 #include "rtc_base/checks.h"
 #include "rtc_base/logging.h"
 
@@ -28,8 +30,11 @@
 RtpTransceiver::RtpTransceiver(
     rtc::scoped_refptr<RtpSenderProxyWithInternal<RtpSenderInternal>> sender,
     rtc::scoped_refptr<RtpReceiverProxyWithInternal<RtpReceiverInternal>>
-        receiver)
-    : unified_plan_(true), media_type_(sender->media_type()) {
+        receiver,
+    cricket::ChannelManager* channel_manager)
+    : unified_plan_(true),
+      media_type_(sender->media_type()),
+      channel_manager_(channel_manager) {
   RTC_DCHECK(media_type_ == cricket::MEDIA_TYPE_AUDIO ||
              media_type_ == cricket::MEDIA_TYPE_VIDEO);
   RTC_DCHECK_EQ(sender->media_type(), receiver->media_type());
@@ -223,10 +228,129 @@
   current_direction_ = absl::nullopt;
 }
 
-void RtpTransceiver::SetCodecPreferences(
-    rtc::ArrayView<RtpCodecCapability> codecs) {
-  // TODO(steveanton): Implement this.
-  RTC_NOTREACHED() << "Not implemented";
+RTCError RtpTransceiver::SetCodecPreferences(
+    rtc::ArrayView<RtpCodecCapability> codec_capabilities) {
+  RTC_DCHECK(unified_plan_);
+
+  // 3. If codecs is an empty list, set transceiver's [[PreferredCodecs]] slot
+  // to codecs and abort these steps.
+  if (codec_capabilities.empty()) {
+    codec_preferences_.clear();
+    return RTCError::OK();
+  }
+
+  // 4. Remove any duplicate values in codecs.
+  std::vector<RtpCodecCapability> codecs;
+  absl::c_remove_copy_if(codec_capabilities, std::back_inserter(codecs),
+                         [&codecs](const RtpCodecCapability& codec) {
+                           return absl::c_linear_search(codecs, codec);
+                         });
+
+  if (media_type_ == cricket::MEDIA_TYPE_AUDIO) {
+    std::vector<cricket::AudioCodec> audio_codecs;
+
+    std::vector<cricket::AudioCodec> recv_codecs, send_codecs;
+    channel_manager_->GetSupportedAudioReceiveCodecs(&recv_codecs);
+    channel_manager_->GetSupportedAudioSendCodecs(&send_codecs);
+
+    // 6. If the intersection between codecs and
+    // RTCRtpSender.getCapabilities(kind).codecs or the intersection between
+    // codecs and RTCRtpReceiver.getCapabilities(kind).codecs only contains RTX,
+    // RED or FEC codecs or is an empty set, throw InvalidModificationError.
+    // This ensures that we always have something to offer, regardless of
+    // transceiver.direction.
+
+    if (!absl::c_any_of(
+            codecs, [&recv_codecs](const RtpCodecCapability& codec) {
+              return codec.name != cricket::kRtxCodecName &&
+                     codec.name != cricket::kRedCodecName &&
+                     codec.name != cricket::kFlexfecCodecName &&
+                     absl::c_any_of(
+                         recv_codecs,
+                         [&codec](const cricket::AudioCodec& recv_codec) {
+                           return recv_codec.MatchesCapability(codec);
+                         });
+            })) {
+      return RTCError(RTCErrorType::INVALID_MODIFICATION,
+                      "Invalid codec preferences: Missing codec from recv "
+                      "codec capabilities.");
+    }
+
+    if (!absl::c_any_of(
+            codecs, [&send_codecs](const RtpCodecCapability& codec) {
+              return codec.name != cricket::kRtxCodecName &&
+                     codec.name != cricket::kRedCodecName &&
+                     codec.name != cricket::kFlexfecCodecName &&
+                     absl::c_any_of(
+                         send_codecs,
+                         [&codec](const cricket::AudioCodec& send_codec) {
+                           return send_codec.MatchesCapability(codec);
+                         });
+            })) {
+      return RTCError(RTCErrorType::INVALID_MODIFICATION,
+                      "Invalid codec preferences: Missing codec from send "
+                      "codec capabilities.");
+    }
+
+    // 7. Let codecCapabilities be the union of
+    // RTCRtpSender.getCapabilities(kind).codecs and
+    // RTCRtpReceiver.getCapabilities(kind).codecs. 8.1 For each codec in
+    // codecs, If codec is not in codecCapabilities, throw
+    // InvalidModificationError.
+    for (const auto& codec_preference : codecs) {
+      bool is_recv_codec = absl::c_any_of(
+          recv_codecs, [&codec_preference](const cricket::AudioCodec& codec) {
+            return codec.MatchesCapability(codec_preference);
+          });
+
+      bool is_send_codec = absl::c_any_of(
+          send_codecs, [&codec_preference](const cricket::AudioCodec& codec) {
+            return codec.MatchesCapability(codec_preference);
+          });
+
+      if (!is_recv_codec && !is_send_codec) {
+        return RTCError(
+            RTCErrorType::INVALID_MODIFICATION,
+            std::string(
+                "Invalid codec preferences: invalid codec with name \"") +
+                codec_preference.name + "\".");
+      }
+    }
+  } else if (media_type_ == cricket::MEDIA_TYPE_VIDEO) {
+    std::vector<cricket::VideoCodec> video_codecs;
+    // Video codecs are both for the receive and send side, so the checks are
+    // simpler than the audio ones.
+    channel_manager_->GetSupportedVideoCodecs(&video_codecs);
+
+    // Validate codecs
+    for (const auto& codec_preference : codecs) {
+      if (!absl::c_any_of(video_codecs, [&codec_preference](
+                                            const cricket::VideoCodec& codec) {
+            return codec.MatchesCapability(codec_preference);
+          })) {
+        return RTCError(
+            RTCErrorType::INVALID_MODIFICATION,
+            std::string(
+                "Invalid codec preferences: invalid codec with name \"") +
+                codec_preference.name + "\".");
+      }
+    }
+  }
+
+  // Check we have a real codec (not just rtx, red or fec)
+  if (absl::c_all_of(codecs, [](const RtpCodecCapability& codec) {
+        return codec.name == cricket::kRtxCodecName ||
+               codec.name == cricket::kRedCodecName ||
+               codec.name == cricket::kUlpfecCodecName;
+      })) {
+    return RTCError(RTCErrorType::INVALID_MODIFICATION,
+                    "Invalid codec preferences: codec list must have a non "
+                    "RTX, RED or FEC entry.");
+  }
+
+  codec_preferences_ = codecs;
+
+  return RTCError::OK();
 }
 
 }  // namespace webrtc
diff --git a/pc/rtp_transceiver.h b/pc/rtp_transceiver.h
index 2c04bd7..990c3cc 100644
--- a/pc/rtp_transceiver.h
+++ b/pc/rtp_transceiver.h
@@ -16,6 +16,7 @@
 
 #include "api/rtp_transceiver_interface.h"
 #include "pc/channel_interface.h"
+#include "pc/channel_manager.h"
 #include "pc/rtp_receiver.h"
 #include "pc/rtp_sender.h"
 
@@ -66,7 +67,8 @@
   RtpTransceiver(
       rtc::scoped_refptr<RtpSenderProxyWithInternal<RtpSenderInternal>> sender,
       rtc::scoped_refptr<RtpReceiverProxyWithInternal<RtpReceiverInternal>>
-          receiver);
+          receiver,
+      cricket::ChannelManager* channel_manager);
   ~RtpTransceiver() override;
 
   // Returns the Voice/VideoChannel set for this transceiver. May be null if
@@ -175,7 +177,11 @@
   absl::optional<RtpTransceiverDirection> current_direction() const override;
   absl::optional<RtpTransceiverDirection> fired_direction() const override;
   void Stop() override;
-  void SetCodecPreferences(rtc::ArrayView<RtpCodecCapability> codecs) override;
+  RTCError SetCodecPreferences(
+      rtc::ArrayView<RtpCodecCapability> codecs) override;
+  std::vector<RtpCodecCapability> codec_preferences() const override {
+    return codec_preferences_;
+  }
 
  private:
   void OnFirstPacketReceived(cricket::ChannelInterface* channel);
@@ -198,6 +204,8 @@
   bool has_ever_been_used_to_send_ = false;
 
   cricket::ChannelInterface* channel_ = nullptr;
+  cricket::ChannelManager* channel_manager_ = nullptr;
+  std::vector<RtpCodecCapability> codec_preferences_;
 };
 
 BEGIN_SIGNALING_PROXY_MAP(RtpTransceiver)
@@ -212,7 +220,10 @@
 PROXY_CONSTMETHOD0(absl::optional<RtpTransceiverDirection>, current_direction)
 PROXY_CONSTMETHOD0(absl::optional<RtpTransceiverDirection>, fired_direction)
 PROXY_METHOD0(void, Stop)
-PROXY_METHOD1(void, SetCodecPreferences, rtc::ArrayView<RtpCodecCapability>)
+PROXY_METHOD1(webrtc::RTCError,
+              SetCodecPreferences,
+              rtc::ArrayView<RtpCodecCapability>)
+PROXY_CONSTMETHOD0(std::vector<RtpCodecCapability>, codec_preferences)
 END_PROXY_MAP()
 
 }  // namespace webrtc