Expose setCodecPreferences/getCapabilities for android

Bug: webrtc:15177
Change-Id: If61ef9a87bc4f68d73cef6e681461682ca48f034
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/304880
Reviewed-by: Sergey Silkin <ssilkin@webrtc.org>
Commit-Queue: Sergey Silkin <ssilkin@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#40071}
diff --git a/AUTHORS b/AUTHORS
index 83a968d..1e4161e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -154,6 +154,7 @@
 Intel Corporation <*@intel.com>
 LG Electronics, Inc. <*@lge.com>
 Life On Air Inc. <*@lifeonair.com>
+LiveKit <*@livekit.io>
 Meta Platforms, Inc. <*@meta.com>
 Microsoft Corporation <*@microsoft.com>
 MIPS Technologies <*@mips.com>
diff --git a/sdk/android/BUILD.gn b/sdk/android/BUILD.gn
index 5bf25ff..4c38644 100644
--- a/sdk/android/BUILD.gn
+++ b/sdk/android/BUILD.gn
@@ -287,6 +287,7 @@
       "api/org/webrtc/RTCStatsCollectorCallback.java",
       "api/org/webrtc/RTCStatsReport.java",
       "api/org/webrtc/RtcCertificatePem.java",
+      "api/org/webrtc/RtpCapabilities.java",
       "api/org/webrtc/RtpParameters.java",
       "api/org/webrtc/RtpReceiver.java",
       "api/org/webrtc/RtpSender.java",
@@ -734,6 +735,8 @@
       "src/jni/pc/rtc_certificate.h",
       "src/jni/pc/rtc_stats_collector_callback_wrapper.cc",
       "src/jni/pc/rtc_stats_collector_callback_wrapper.h",
+      "src/jni/pc/rtp_capabilities.cc",
+      "src/jni/pc/rtp_capabilities.h",
       "src/jni/pc/rtp_parameters.cc",
       "src/jni/pc/rtp_parameters.h",
       "src/jni/pc/rtp_receiver.cc",
@@ -1391,6 +1394,7 @@
       "api/org/webrtc/RTCStatsCollectorCallback.java",
       "api/org/webrtc/RTCStatsReport.java",
       "api/org/webrtc/RtcCertificatePem.java",
+      "api/org/webrtc/RtpCapabilities.java",
       "api/org/webrtc/RtpParameters.java",
       "api/org/webrtc/RtpReceiver.java",
       "api/org/webrtc/RtpSender.java",
@@ -1486,6 +1490,7 @@
         "instrumentationtests/src/org/webrtc/PeerConnectionTest.java",
         "instrumentationtests/src/org/webrtc/RendererCommonTest.java",
         "instrumentationtests/src/org/webrtc/RtcCertificatePemTest.java",
+        "instrumentationtests/src/org/webrtc/RtpCapabilitiesTest.java",
         "instrumentationtests/src/org/webrtc/RtpSenderTest.java",
         "instrumentationtests/src/org/webrtc/RtpTransceiverTest.java",
         "instrumentationtests/src/org/webrtc/SoftwareVideoDecoderFactoryTest.java",
diff --git a/sdk/android/api/org/webrtc/PeerConnectionFactory.java b/sdk/android/api/org/webrtc/PeerConnectionFactory.java
index ca67b3a..de89872 100644
--- a/sdk/android/api/org/webrtc/PeerConnectionFactory.java
+++ b/sdk/android/api/org/webrtc/PeerConnectionFactory.java
@@ -15,7 +15,9 @@
 import androidx.annotation.Nullable;
 import java.util.List;
 import org.webrtc.Logging.Severity;
+import org.webrtc.MediaStreamTrack;
 import org.webrtc.PeerConnection;
+import org.webrtc.RtpCapabilities;
 import org.webrtc.audio.AudioDeviceModule;
 import org.webrtc.audio.JavaAudioDeviceModule;
 
@@ -471,6 +473,16 @@
     return new AudioTrack(nativeCreateAudioTrack(nativeFactory, id, source.getNativeAudioSource()));
   }
 
+  public RtpCapabilities getRtpReceiverCapabilities(MediaStreamTrack.MediaType mediaType) {
+    checkPeerConnectionFactoryExists();
+    return nativeGetRtpReceiverCapabilities(nativeFactory, mediaType);
+  }
+
+  public RtpCapabilities getRtpSenderCapabilities(MediaStreamTrack.MediaType mediaType) {
+    checkPeerConnectionFactoryExists();
+    return nativeGetRtpSenderCapabilities(nativeFactory, mediaType);
+  }
+
   // Starts recording an AEC dump. Ownership of the file is transfered to the
   // native code. If an AEC dump is already in progress, it will be stopped and
   // a new one will start using the provided file.
@@ -615,4 +627,8 @@
   private static native void nativeInjectLoggable(JNILogging jniLogging, int severity);
   private static native void nativeDeleteLoggable();
   private static native void nativePrintStackTrace(int tid);
+  private static native RtpCapabilities nativeGetRtpSenderCapabilities(
+      long factory, MediaStreamTrack.MediaType mediaType);
+  private static native RtpCapabilities nativeGetRtpReceiverCapabilities(
+      long factory, MediaStreamTrack.MediaType mediaType);
 }
diff --git a/sdk/android/api/org/webrtc/RtpCapabilities.java b/sdk/android/api/org/webrtc/RtpCapabilities.java
new file mode 100644
index 0000000..02d1704
--- /dev/null
+++ b/sdk/android/api/org/webrtc/RtpCapabilities.java
@@ -0,0 +1,125 @@
+/*
+ *  Copyright 2023 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc;
+
+import androidx.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+import org.webrtc.MediaStreamTrack;
+
+public class RtpCapabilities {
+  public static class CodecCapability {
+    public int preferredPayloadType;
+    // Name used to identify the codec. Equivalent to MIME subtype.
+    public String name;
+    // The media type of this codec. Equivalent to MIME top-level type.
+    public MediaStreamTrack.MediaType kind;
+    // Clock rate in Hertz.
+    public Integer clockRate;
+    // The number of audio channels used. Set to null for video codecs.
+    public Integer numChannels;
+    // The "format specific parameters" field from the "a=fmtp" line in the SDP
+    public Map<String, String> parameters;
+    // The MIME type of the codec. This is a convenience field.
+    public String mimeType;
+
+    public CodecCapability() {}
+
+    @CalledByNative("CodecCapability")
+    CodecCapability(int preferredPayloadType, String name, MediaStreamTrack.MediaType kind,
+        Integer clockRate, Integer numChannels, String mimeType, Map<String, String> parameters) {
+      this.preferredPayloadType = preferredPayloadType;
+      this.name = name;
+      this.kind = kind;
+      this.clockRate = clockRate;
+      this.numChannels = numChannels;
+      this.parameters = parameters;
+      this.mimeType = mimeType;
+    }
+
+    @CalledByNative("CodecCapability")
+    int getPreferredPayloadType() {
+      return preferredPayloadType;
+    }
+
+    @CalledByNative("CodecCapability")
+    String getName() {
+      return name;
+    }
+
+    @CalledByNative("CodecCapability")
+    MediaStreamTrack.MediaType getKind() {
+      return kind;
+    }
+
+    @CalledByNative("CodecCapability")
+    Integer getClockRate() {
+      return clockRate;
+    }
+
+    @CalledByNative("CodecCapability")
+    Integer getNumChannels() {
+      return numChannels;
+    }
+
+    @CalledByNative("CodecCapability")
+    Map getParameters() {
+      return parameters;
+    }
+  }
+
+  public static class HeaderExtensionCapability {
+    private final String uri;
+    private final int preferredId;
+    private final boolean preferredEncrypted;
+
+    @CalledByNative("HeaderExtensionCapability")
+    HeaderExtensionCapability(String uri, int preferredId, boolean preferredEncrypted) {
+      this.uri = uri;
+      this.preferredId = preferredId;
+      this.preferredEncrypted = preferredEncrypted;
+    }
+
+    @CalledByNative("HeaderExtensionCapability")
+    public String getUri() {
+      return uri;
+    }
+
+    @CalledByNative("HeaderExtensionCapability")
+    public int getPreferredId() {
+      return preferredId;
+    }
+
+    @CalledByNative("HeaderExtensionCapability")
+    public boolean getPreferredEncrypted() {
+      return preferredEncrypted;
+    }
+  }
+
+  public List<CodecCapability> codecs;
+  public List<HeaderExtensionCapability> headerExtensions;
+
+  @CalledByNative
+  RtpCapabilities(List<CodecCapability> codecs, List<HeaderExtensionCapability> headerExtensions) {
+    this.headerExtensions = headerExtensions;
+    this.codecs = codecs;
+  }
+
+  @CalledByNative
+  public List<HeaderExtensionCapability> getHeaderExtensions() {
+    return headerExtensions;
+  }
+
+  @CalledByNative
+  List<CodecCapability> getCodecs() {
+    return codecs;
+  }
+}
diff --git a/sdk/android/api/org/webrtc/RtpTransceiver.java b/sdk/android/api/org/webrtc/RtpTransceiver.java
index 1102bd7..d2ba733 100644
--- a/sdk/android/api/org/webrtc/RtpTransceiver.java
+++ b/sdk/android/api/org/webrtc/RtpTransceiver.java
@@ -215,6 +215,11 @@
     nativeStopInternal(nativeRtpTransceiver);
   }
 
+  public void setCodecPreferences(List<RtpCapabilities.CodecCapability> codecs) {
+    checkRtpTransceiverExists();
+    nativeSetCodecPreferences(nativeRtpTransceiver, codecs);
+  }
+
   /**
    * The StopInternal method stops the RtpTransceiver, like Stop, but goes
    * immediately to Stopped state.
@@ -263,4 +268,6 @@
   private static native void nativeStopStandard(long rtpTransceiver);
   private static native boolean nativeSetDirection(
       long rtpTransceiver, RtpTransceiverDirection rtpTransceiverDirection);
+  private static native void nativeSetCodecPreferences(
+      long rtpTransceiver, List<RtpCapabilities.CodecCapability> codecs);
 }
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/PeerConnectionEndToEndTest.java b/sdk/android/instrumentationtests/src/org/webrtc/PeerConnectionEndToEndTest.java
index e433fa4..64b1ac0 100644
--- a/sdk/android/instrumentationtests/src/org/webrtc/PeerConnectionEndToEndTest.java
+++ b/sdk/android/instrumentationtests/src/org/webrtc/PeerConnectionEndToEndTest.java
@@ -582,7 +582,7 @@
     }
   }
 
-  private static class SdpObserverLatch implements SdpObserver {
+  static class SdpObserverLatch implements SdpObserver {
     private boolean success;
     private @Nullable SessionDescription sdp;
     private @Nullable String error;
diff --git a/sdk/android/instrumentationtests/src/org/webrtc/RtpCapabilitiesTest.java b/sdk/android/instrumentationtests/src/org/webrtc/RtpCapabilitiesTest.java
new file mode 100644
index 0000000..961ecd5
--- /dev/null
+++ b/sdk/android/instrumentationtests/src/org/webrtc/RtpCapabilitiesTest.java
@@ -0,0 +1,217 @@
+/*
+ *  Copyright 2023 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import androidx.annotation.Nullable;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Test;
+import org.webrtc.PeerConnectionEndToEndTest.SdpObserverLatch;
+import org.webrtc.RtpParameters.Encoding;
+import org.webrtc.RtpTransceiver.RtpTransceiverInit;
+
+/**
+ * Unit-tests for {@link RtpCapabilities}.
+ */
+public class RtpCapabilitiesTest {
+  private PeerConnectionFactory factory;
+  private PeerConnection pc;
+
+  static class CustomHardwareVideoEncoderFactory implements VideoEncoderFactory {
+    private final List<VideoCodecInfo> supportedCodecs;
+
+    public CustomHardwareVideoEncoderFactory(List<VideoCodecInfo> supportedCodecs) {
+      this.supportedCodecs = supportedCodecs;
+    }
+
+    @Override
+    public @Nullable VideoEncoder createEncoder(VideoCodecInfo info) {
+      return null;
+    }
+
+    @Override
+    public VideoCodecInfo[] getSupportedCodecs() {
+      return supportedCodecs.toArray(new VideoCodecInfo[0]);
+    }
+  }
+
+  static class CustomHardwareVideoDecoderFactory implements VideoDecoderFactory {
+    private final List<VideoCodecInfo> supportedCodecs;
+
+    public CustomHardwareVideoDecoderFactory(List<VideoCodecInfo> supportedCodecs) {
+      this.supportedCodecs = supportedCodecs;
+    }
+
+    @Nullable
+    @Override
+    public VideoDecoder createDecoder(VideoCodecInfo videoCodecInfo) {
+      return null;
+    }
+
+    @Override
+    public VideoCodecInfo[] getSupportedCodecs() {
+      return supportedCodecs.toArray(new VideoCodecInfo[0]);
+    }
+  }
+
+  @Before
+  public void setUp() {
+    PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions
+                                         .builder(InstrumentationRegistry.getTargetContext())
+                                         .setNativeLibraryName(TestConstants.NATIVE_LIBRARY)
+                                         .createInitializationOptions());
+
+    VideoCodecInfo vp8Codec = new VideoCodecInfo("VP8", new HashMap<>());
+    VideoCodecInfo h264Codec = new VideoCodecInfo("H264", new HashMap<>());
+    List<VideoCodecInfo> supportedCodecs = new ArrayList<>();
+    supportedCodecs.add(vp8Codec);
+    supportedCodecs.add(h264Codec);
+
+    factory = PeerConnectionFactory.builder()
+                  .setVideoEncoderFactory(new CustomHardwareVideoEncoderFactory(supportedCodecs))
+                  .setVideoDecoderFactory(new CustomHardwareVideoDecoderFactory(supportedCodecs))
+                  .createPeerConnectionFactory();
+
+    PeerConnection.RTCConfiguration config =
+        new PeerConnection.RTCConfiguration(Collections.emptyList());
+    // RtpTranceiver is part of new unified plan semantics.
+    config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
+    pc = factory.createPeerConnection(config, mock(PeerConnection.Observer.class));
+  }
+
+  /**
+   * Test that getRtpSenderCapabilities returns the codecs supported by the video encoder factory.
+   */
+  @Test
+  @SmallTest
+  public void testGetSenderCapabilities() {
+    RtpCapabilities capabilities =
+        factory.getRtpSenderCapabilities(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO);
+    List<String> codecNames = new ArrayList<>();
+    for (RtpCapabilities.CodecCapability codec : capabilities.getCodecs()) {
+      codecNames.add(codec.name);
+    }
+
+    assertTrue(codecNames.containsAll(Arrays.asList("VP8", "H264")));
+  }
+
+  /**
+   * Test that getRtpReceiverCapabilities returns the codecs supported by the video decoder factory.
+   */
+  @Test
+  @SmallTest
+  public void testGetReceiverCapabilities() {
+    RtpCapabilities capabilities =
+        factory.getRtpReceiverCapabilities(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO);
+    List<String> codecNames = new ArrayList<>();
+    for (RtpCapabilities.CodecCapability codec : capabilities.getCodecs()) {
+      codecNames.add(codec.name);
+    }
+
+    assertTrue(codecNames.containsAll(Arrays.asList("VP8", "H264")));
+  }
+
+  /**
+   * Test that a created offer reflects the codec capabilities passed into setCodecPreferences.
+   */
+  @Test
+  @SmallTest
+  public void testSetCodecPreferences() {
+    List<Encoding> encodings = new ArrayList<>();
+    encodings.add(new Encoding("1", true, null));
+
+    RtpTransceiverInit init = new RtpTransceiverInit(
+        RtpTransceiver.RtpTransceiverDirection.SEND_ONLY, Collections.emptyList(), encodings);
+    RtpTransceiver transceiver =
+        pc.addTransceiver(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO, init);
+
+    RtpCapabilities capabilities =
+        factory.getRtpSenderCapabilities(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO);
+    RtpCapabilities.CodecCapability targetCodec = null;
+    for (RtpCapabilities.CodecCapability codec : capabilities.getCodecs()) {
+      if (codec.name.equals("VP8")) {
+        targetCodec = codec;
+        break;
+      }
+    }
+
+    assertNotNull(targetCodec);
+
+    transceiver.setCodecPreferences(Arrays.asList(targetCodec));
+
+    SdpObserverLatch sdpLatch = new SdpObserverLatch();
+    pc.createOffer(sdpLatch, new MediaConstraints());
+    assertTrue(sdpLatch.await());
+
+    SessionDescription offerSdp = sdpLatch.getSdp();
+    assertNotNull(offerSdp);
+
+    String[] sdpDescriptionLines = offerSdp.description.split("\r\n");
+    List<String> mediaDescriptions = getMediaDescriptions(sdpDescriptionLines);
+    assertEquals(1, mediaDescriptions.size());
+
+    List<String> payloads = getMediaPayloads(mediaDescriptions.get(0));
+    assertEquals(1, payloads.size());
+
+    String targetPayload = payloads.get(0);
+    List<String> rtpMaps = getRtpMaps(sdpDescriptionLines);
+    assertEquals(1, rtpMaps.size());
+
+    String rtpMap = rtpMaps.get(0);
+
+    String expected =
+        "a=rtpmap:" + targetPayload + " " + targetCodec.name + "/" + targetCodec.getClockRate();
+    assertEquals(expected, rtpMap);
+  }
+
+  private List<String> getMediaDescriptions(String[] sdpDescriptionLines) {
+    List<String> mediaDescriptions = new ArrayList<>();
+    for (String line : sdpDescriptionLines) {
+      if (line.charAt(0) == 'm') {
+        mediaDescriptions.add(line);
+      }
+    }
+
+    return mediaDescriptions;
+  }
+
+  private List<String> getMediaPayloads(String mediaDescription) {
+    String[] fields = mediaDescription.split(" ");
+    assertTrue(fields.length >= 4);
+
+    // Media formats are described from the fourth field onwards.
+    return Arrays.asList(Arrays.copyOfRange(fields, 3, fields.length));
+  }
+
+  private List<String> getRtpMaps(String[] sdpDescriptionLines) {
+    List<String> rtpMaps = new ArrayList<>();
+    for (String line : sdpDescriptionLines) {
+      if (line.startsWith("a=rtpmap")) {
+        rtpMaps.add(line);
+      }
+    }
+
+    return rtpMaps;
+  }
+}
diff --git a/sdk/android/src/jni/pc/peer_connection_factory.cc b/sdk/android/src/jni/pc/peer_connection_factory.cc
index 4c68208..c6099c2 100644
--- a/sdk/android/src/jni/pc/peer_connection_factory.cc
+++ b/sdk/android/src/jni/pc/peer_connection_factory.cc
@@ -40,8 +40,10 @@
 #include "sdk/android/src/jni/pc/android_network_monitor.h"
 #include "sdk/android/src/jni/pc/audio.h"
 #include "sdk/android/src/jni/pc/ice_candidate.h"
+#include "sdk/android/src/jni/pc/media_stream_track.h"
 #include "sdk/android/src/jni/pc/owned_factory_and_threads.h"
 #include "sdk/android/src/jni/pc/peer_connection.h"
+#include "sdk/android/src/jni/pc/rtp_capabilities.h"
 #include "sdk/android/src/jni/pc/ssl_certificate_verifier_wrapper.h"
 #include "sdk/android/src/jni/pc/video.h"
 #include "system_wrappers/include/field_trial.h"
@@ -393,6 +395,27 @@
   return jlongFromPointer(track.release());
 }
 
+ScopedJavaLocalRef<jobject> JNI_PeerConnectionFactory_GetRtpSenderCapabilities(
+    JNIEnv* jni,
+    jlong native_factory,
+    const JavaParamRef<jobject>& media_type) {
+  auto factory = PeerConnectionFactoryFromJava(native_factory);
+  return NativeToJavaRtpCapabilities(
+      jni, factory->GetRtpSenderCapabilities(
+               JavaToNativeMediaType(jni, media_type)));
+}
+
+ScopedJavaLocalRef<jobject>
+JNI_PeerConnectionFactory_GetRtpReceiverCapabilities(
+    JNIEnv* jni,
+    jlong native_factory,
+    const JavaParamRef<jobject>& media_type) {
+  auto factory = PeerConnectionFactoryFromJava(native_factory);
+  return NativeToJavaRtpCapabilities(
+      jni, factory->GetRtpReceiverCapabilities(
+               JavaToNativeMediaType(jni, media_type)));
+}
+
 static jboolean JNI_PeerConnectionFactory_StartAecDump(
     JNIEnv* jni,
     jlong native_factory,
diff --git a/sdk/android/src/jni/pc/rtp_capabilities.cc b/sdk/android/src/jni/pc/rtp_capabilities.cc
new file mode 100644
index 0000000..09157be
--- /dev/null
+++ b/sdk/android/src/jni/pc/rtp_capabilities.cc
@@ -0,0 +1,119 @@
+/*
+ *  Copyright 2023 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "sdk/android/src/jni/pc/rtp_capabilities.h"
+
+#include "sdk/android/generated_peerconnection_jni/RtpCapabilities_jni.h"
+#include "sdk/android/native_api/jni/java_types.h"
+#include "sdk/android/src/jni/jni_helpers.h"
+#include "sdk/android/src/jni/pc/media_stream_track.h"
+
+namespace webrtc {
+namespace jni {
+
+namespace {
+
+ScopedJavaLocalRef<jobject> NativeToJavaRtpCodecParameter(
+    JNIEnv* env,
+    const RtpCodecCapability& codec) {
+  return Java_CodecCapability_Constructor(
+      env, codec.preferred_payload_type.value(),
+      NativeToJavaString(env, codec.name),
+      NativeToJavaMediaType(env, codec.kind),
+      NativeToJavaInteger(env, codec.clock_rate),
+      NativeToJavaInteger(env, codec.num_channels),
+      NativeToJavaString(env, codec.mime_type()),
+      NativeToJavaStringMap(env, codec.parameters));
+}
+
+ScopedJavaLocalRef<jobject> NativeToJavaRtpHeaderExtensionParameter(
+    JNIEnv* env,
+    const RtpHeaderExtensionCapability& extension) {
+  return Java_HeaderExtensionCapability_Constructor(
+      env, NativeToJavaString(env, extension.uri),
+      extension.preferred_id.value(), extension.preferred_encrypt);
+}
+}  // namespace
+
+RtpCapabilities JavaToNativeRtpCapabilities(
+    JNIEnv* jni,
+    const JavaRef<jobject>& j_capabilities) {
+  RtpCapabilities capabilities;
+
+  ScopedJavaLocalRef<jobject> j_header_extensions =
+      Java_RtpCapabilities_getHeaderExtensions(jni, j_capabilities);
+  for (const JavaRef<jobject>& j_header_extension :
+       Iterable(jni, j_header_extensions)) {
+    RtpHeaderExtensionCapability header_extension;
+    header_extension.uri = JavaToStdString(
+        jni, Java_HeaderExtensionCapability_getUri(jni, j_header_extension));
+    header_extension.preferred_id =
+        Java_HeaderExtensionCapability_getPreferredId(jni, j_header_extension);
+    header_extension.preferred_encrypt =
+        Java_HeaderExtensionCapability_getPreferredEncrypted(
+            jni, j_header_extension);
+    capabilities.header_extensions.push_back(header_extension);
+  }
+
+  // Convert codecs.
+  ScopedJavaLocalRef<jobject> j_codecs =
+      Java_RtpCapabilities_getCodecs(jni, j_capabilities);
+  for (const JavaRef<jobject>& j_codec : Iterable(jni, j_codecs)) {
+    RtpCodecCapability codec;
+    codec.preferred_payload_type =
+        Java_CodecCapability_getPreferredPayloadType(jni, j_codec);
+    codec.name =
+        JavaToStdString(jni, Java_CodecCapability_getName(jni, j_codec));
+    codec.kind =
+        JavaToNativeMediaType(jni, Java_CodecCapability_getKind(jni, j_codec));
+    codec.clock_rate = JavaToNativeOptionalInt(
+        jni, Java_CodecCapability_getClockRate(jni, j_codec));
+    codec.num_channels = JavaToNativeOptionalInt(
+        jni, Java_CodecCapability_getNumChannels(jni, j_codec));
+    auto parameters_map = JavaToNativeStringMap(
+        jni, Java_CodecCapability_getParameters(jni, j_codec));
+    codec.parameters.insert(parameters_map.begin(), parameters_map.end());
+    capabilities.codecs.push_back(codec);
+  }
+  return capabilities;
+}
+
+ScopedJavaLocalRef<jobject> NativeToJavaRtpCapabilities(
+    JNIEnv* env,
+    const RtpCapabilities& capabilities) {
+  return Java_RtpCapabilities_Constructor(
+      env,
+      NativeToJavaList(env, capabilities.codecs,
+                       &NativeToJavaRtpCodecParameter),
+      NativeToJavaList(env, capabilities.header_extensions,
+                       &NativeToJavaRtpHeaderExtensionParameter));
+}
+
+RtpCodecCapability JavaToNativeRtpCodecCapability(
+    JNIEnv* jni,
+    const JavaRef<jobject>& j_codec) {
+  RtpCodecCapability codec;
+  codec.preferred_payload_type =
+      Java_CodecCapability_getPreferredPayloadType(jni, j_codec);
+  codec.name = JavaToStdString(jni, Java_CodecCapability_getName(jni, j_codec));
+  codec.kind =
+      JavaToNativeMediaType(jni, Java_CodecCapability_getKind(jni, j_codec));
+  codec.clock_rate = JavaToNativeOptionalInt(
+      jni, Java_CodecCapability_getClockRate(jni, j_codec));
+  codec.num_channels = JavaToNativeOptionalInt(
+      jni, Java_CodecCapability_getNumChannels(jni, j_codec));
+  auto parameters_map = JavaToNativeStringMap(
+      jni, Java_CodecCapability_getParameters(jni, j_codec));
+  codec.parameters.insert(parameters_map.begin(), parameters_map.end());
+  return codec;
+}
+
+}  // namespace jni
+}  // namespace webrtc
diff --git a/sdk/android/src/jni/pc/rtp_capabilities.h b/sdk/android/src/jni/pc/rtp_capabilities.h
new file mode 100644
index 0000000..07be9f0
--- /dev/null
+++ b/sdk/android/src/jni/pc/rtp_capabilities.h
@@ -0,0 +1,37 @@
+/*
+ *  Copyright 2023 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef SDK_ANDROID_SRC_JNI_PC_RTP_CAPABILITIES_H_
+#define SDK_ANDROID_SRC_JNI_PC_RTP_CAPABILITIES_H_
+
+#include <jni.h>
+
+#include "api/rtp_parameters.h"
+#include "sdk/android/native_api/jni/scoped_java_ref.h"
+
+namespace webrtc {
+namespace jni {
+
+RtpCapabilities JavaToNativeRtpCapabilities(
+    JNIEnv* jni,
+    const JavaRef<jobject>& j_capabilities);
+
+ScopedJavaLocalRef<jobject> NativeToJavaRtpCapabilities(
+    JNIEnv* jni,
+    const RtpCapabilities& capabilities);
+
+RtpCodecCapability JavaToNativeRtpCodecCapability(
+    JNIEnv* jni,
+    const JavaRef<jobject>& j_codec_capability);
+
+}  // namespace jni
+}  // namespace webrtc
+
+#endif  // SDK_ANDROID_SRC_JNI_PC_RTP_CAPABILITIES_H_
diff --git a/sdk/android/src/jni/pc/rtp_transceiver.cc b/sdk/android/src/jni/pc/rtp_transceiver.cc
index 1d46846..748dbb7 100644
--- a/sdk/android/src/jni/pc/rtp_transceiver.cc
+++ b/sdk/android/src/jni/pc/rtp_transceiver.cc
@@ -16,6 +16,7 @@
 #include "sdk/android/native_api/jni/java_types.h"
 #include "sdk/android/src/jni/jni_helpers.h"
 #include "sdk/android/src/jni/pc/media_stream_track.h"
+#include "sdk/android/src/jni/pc/rtp_capabilities.h"
 #include "sdk/android/src/jni/pc/rtp_parameters.h"
 #include "sdk/android/src/jni/pc/rtp_receiver.h"
 #include "sdk/android/src/jni/pc/rtp_sender.h"
@@ -139,6 +140,17 @@
                    : nullptr;
 }
 
+void JNI_RtpTransceiver_SetCodecPreferences(
+    JNIEnv* jni,
+    jlong j_rtp_transceiver_pointer,
+    const JavaParamRef<jobject>& j_codecs) {
+  std::vector<RtpCodecCapability> codecs =
+      JavaListToNativeVector<RtpCodecCapability, jobject>(
+          jni, j_codecs, &JavaToNativeRtpCodecCapability);
+  reinterpret_cast<RtpTransceiverInterface*>(j_rtp_transceiver_pointer)
+      ->SetCodecPreferences(codecs);
+}
+
 void JNI_RtpTransceiver_StopInternal(JNIEnv* jni,
                                      jlong j_rtp_transceiver_pointer) {
   reinterpret_cast<RtpTransceiverInterface*>(j_rtp_transceiver_pointer)