Repository: pion/webrtc (go)

Issue:
Implement RTCRtpTransceiver.setCodecPreferences
This will allow users to set on a per Transceiver basis their preferences around codec choice. The `MediaEngine` is a global list supported codecs still.

[WC3 spec](https://www.w3.org/TR/webrtc/#dom-rtcrtptransceiver-setcodecpreferences)


```
setCodecPreferences
The setCodecPreferences method overrides the default codec preferences used by the user agent. When generating a session description using either createOffer or createAnswer, the user agent MUST use the indicated codecs, in the order specified in the codecs argument, for the media section corresponding to this RTCRtpTransceiver.

This method allows applications to disable the negotiation of specific codecs (including RTX/RED/FEC). It also allows an application to cause a remote peer to prefer the codec that appears first in the list for sending.

Codec preferences remain in effect for all calls to createOffer and createAnswer that include this RTCRtpTransceiver until this method is called again. Setting codecs to an empty sequence resets codec preferences to any default value.

NOTE
Codecs have their payload types listed under each m= section in the SDP, defining the mapping between payload types and codecs. These payload types are referenced by the m=video or m=audio lines in the order of preference, and codecs that are not negotiated do not appear in this list as defined in section 5.2.1 of [RFC8829]. A previously negotiated codec that is subsequently removed disappears from the m=video or m=audio line, and while its codec payload type is not to be reused in future offers or answers, its payload type may also be removed from the mapping of payload types in the SDP.
The codecs sequence passed into setCodecPreferences can only contain codecs that are returned by RTCRtpSender.getCapabilities(kind) or RTCRtpReceiver.getCapabilities(kind), where kind is the kind of the RTCRtpTransceiver on which the method is called. Additionally, the RTCRtpCodecCapability dictionary members cannot be modified. If codecs does not fulfill these requirements, the user agent MUST throw an InvalidModificationError.

NOTE
Due to a recommendation in [SDP], calls to createAnswer SHOULD use only the common subset of the codec preferences and the codecs that appear in the offer. For example, if codec preferences are "C, B, A", but only codecs "A, B" were offered, the answer should only contain codecs "B, A". However, [RFC8829] (section 5.3.1.) allows adding codecs that were not in the offer, so implementations can behave differently.
When setCodecPreferences() in invoked, the user agent MUST run the following steps:

Let transceiver be the RTCRtpTransceiver object this method was invoked on.

Let codecs be the first argument.

If codecs is an empty list, set transceiver.[[PreferredCodecs]] to codecs and abort these steps.

Remove any duplicate values in codecs. Start at the back of the list such that the priority of the codecs is maintained; the index of the first occurrence of a codec within the list is the same before and after this step.

Let kind be the transceiver's transceiver kind.

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.

Let codecCapabilities be the union of RTCRtpSender.getCapabilities(kind).codecs and RTCRtpReceiver.getCapabilities(kind).codecs.

For each codec in codecs,

If codec is not in codecCapabilities, throw InvalidModificationError.
Set transceiver.[[PreferredCodecs]] to codecs.

NOTE
If set, the offerer's codec preferences will decide the order of the codecs in the offer. If the answerer does not have any codec preferences then the same order will be used in the answer. However, if the answerer also has codec preferences, these preferences override the order in the answer. In this case, the offerer's preferences would affect which codecs were on offer but not the final order.
```

Pull request:
Implements SetCodecPreferences in RTPTransceiver
This allows to set supported codecs per transceiver.

Resolves #1847


----

@digitalix Instead of slowing down the review process I am just gonna take a few mins and fix up myself! Feel free to push to this branch also. Will leave notes on what I changed. If you agree when I am done go ahead and merge :) 


Patch:
diff --git a/errors.go b/errors.go
index 23d26200..31595b59 100644
--- a/errors.go
+++ b/errors.go
@@ -202,6 +202,7 @@ var (
 
 	errRTPTransceiverCannotChangeMid        = errors.New("errRTPSenderTrackNil")
 	errRTPTransceiverSetSendingInvalidState = errors.New("invalid state change in RTPTransceiver.setSending")
+	errRTPTransceiverCodecUnsupported       = errors.New("unsupported codec type by this transceiver")
 
 	errSCTPTransportDTLS = errors.New("DTLS not established")
 
diff --git a/mediaengine.go b/mediaengine.go
index c7384fe4..6c160358 100644
--- a/mediaengine.go
+++ b/mediaengine.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/pion/rtp"
@@ -57,6 +58,8 @@ type MediaEngine struct {
 
 	headerExtensions           []mediaEngineHeaderExtension
 	negotiatedHeaderExtensions map[int]mediaEngineHeaderExtension
+
+	mu sync.RWMutex
 }
 
 // RegisterDefaultCodecs registers the default codecs supported by Pion WebRTC.
@@ -196,6 +199,9 @@ func (m *MediaEngine) addCodec(codecs []RTPCodecParameters, codec RTPCodecParame
 // These are the list of codecs supported by this PeerConnection.
 // RegisterCodec is not safe for concurrent use.
 func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
 	codec.statsID = fmt.Sprintf("RTPCodec-%d", time.Now().UnixNano())
 	switch typ {
 	case RTPCodecTypeAudio:
@@ -211,6 +217,9 @@ func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType)
 // RegisterHeaderExtension adds a header extension to the MediaEngine
 // To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete
 func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapability, typ RTPCodecType, allowedDirections ...RTPTransceiverDirection) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
 	if m.negotiatedHeaderExtensions == nil {
 		m.negotiatedHeaderExtensions = map[int]mediaEngineHeaderExtension{}
 	}
@@ -251,6 +260,9 @@ func (m *MediaEngine) RegisterHeaderExtension(extension RTPHeaderExtensionCapabi
 
 // RegisterFeedback adds feedback mechanism to already registered codecs.
 func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
 	switch typ {
 	case RTPCodecTypeVideo:
 		for i, v := range m.videoCodecs {
@@ -268,6 +280,9 @@ func (m *MediaEngine) RegisterFeedback(feedback RTCPFeedback, typ RTPCodecType)
 // getHeaderExtensionID returns the negotiated ID for a header extension.
 // If the Header Extension isn't enabled ok will be false
 func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapability) (val int, audioNegotiated, videoNegotiated bool) {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
 	if m.negotiatedHeaderExtensions == nil {
 		return 0, false, false
 	}
@@ -284,6 +299,8 @@ func (m *MediaEngine) getHeaderExtensionID(extension RTPHeaderExtensionCapabilit
 // copy copies any user modifiable state of the MediaEngine
 // all internal state is reset
 func (m *MediaEngine) copy() *MediaEngine {
+	m.mu.Lock()
+	defer m.mu.Unlock()
 	cloned := &MediaEngine{
 		videoCodecs:      append([]RTPCodecParameters{}, m.videoCodecs...),
 		audioCodecs:      append([]RTPCodecParameters{}, m.audioCodecs...),
@@ -296,12 +313,24 @@ func (m *MediaEngine) copy() *MediaEngine {
 }
 
 func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParameters, RTPCodecType, error) {
-	for _, codec := range m.negotiatedVideoCodecs {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	codecs := m.negotiatedVideoCodecs
+	if !m.negotiatedVideo {
+		codecs = m.videoCodecs
+	}
+	for _, codec := range codecs {
 		if codec.PayloadType == payloadType {
 			return codec, RTPCodecTypeVideo, nil
 		}
 	}
-	for _, codec := range m.negotiatedAudioCodecs {
+
+	codecs = m.negotiatedAudioCodecs
+	if !m.negotiatedAudio {
+		codecs = m.audioCodecs
+	}
+	for _, codec := range codecs {
 		if codec.PayloadType == payloadType {
 			return codec, RTPCodecTypeAudio, nil
 		}
@@ -311,6 +340,9 @@ func (m *MediaEngine) getCodecByPayload(payloadType PayloadType) (RTPCodecParame
 }
 
 func (m *MediaEngine) collectStats(collector *statsReportCollector) {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
 	statsLoop := func(codecs []RTPCodecParameters) {
 		for _, codec := range codecs {
 			collector.Collecting()
@@ -418,6 +450,9 @@ func (m *MediaEngine) pushCodecs(codecs []RTPCodecParameters, typ RTPCodecType)
 
 // Update the MediaEngine from a remote description
 func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
 	for _, media := range desc.MediaDescriptions {
 		var typ RTPCodecType
 		switch {
@@ -478,6 +513,9 @@ func (m *MediaEngine) updateFromRemoteDescription(desc sdp.SessionDescription) e
 }
 
 func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
 	if typ == RTPCodecTypeVideo {
 		if m.negotiatedVideo {
 			return m.negotiatedVideoCodecs
@@ -496,6 +534,9 @@ func (m *MediaEngine) getCodecsByKind(typ RTPCodecType) []RTPCodecParameters {
 }
 
 func (m *MediaEngine) getRTPParametersByKind(typ RTPCodecType, directions []RTPTransceiverDirection) RTPParameters {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
 	headerExtensions := make([]RTPHeaderExtensionParameter, 0)
 
 	if m.negotiatedVideo && typ == RTPCodecTypeVideo ||
@@ -525,6 +566,8 @@ func (m *MediaEngine) getRTPParametersByPayloadType(payloadType PayloadType) (RT
 		return RTPParameters{}, err
 	}
 
+	m.mu.RLock()
+	defer m.mu.RUnlock()
 	headerExtensions := make([]RTPHeaderExtensionParameter, 0)
 	for id, e := range m.negotiatedHeaderExtensions {
 		if e.isAudio && typ == RTPCodecTypeAudio || e.isVideo && typ == RTPCodecTypeVideo {
diff --git a/peerconnection.go b/peerconnection.go
index 1afb120c..8ac7ec61 100644
--- a/peerconnection.go
+++ b/peerconnection.go
@@ -1052,7 +1052,7 @@ func (pc *PeerConnection) SetRemoteDescription(desc SessionDescription) error {
 					localDirection = RTPTransceiverDirectionSendonly
 				}
 
-				t = newRTPTransceiver(receiver, nil, localDirection, kind)
+				t = newRTPTransceiver(receiver, nil, localDirection, kind, pc.api)
 				pc.mu.Lock()
 				pc.addRTPTransceiver(t)
 				pc.mu.Unlock()
@@ -1634,7 +1634,7 @@ func (pc *PeerConnection) newTransceiverFromTrack(direction RTPTransceiverDirect
 	if err != nil {
 		return
 	}
-	return newRTPTransceiver(r, s, direction, track.Kind()), nil
+	return newRTPTransceiver(r, s, direction, track.Kind(), pc.api), nil
 }
 
 // AddTransceiverFromKind Create a new RtpTransceiver and adds it to the set of transceivers.
@@ -1668,7 +1668,7 @@ func (pc *PeerConnection) AddTransceiverFromKind(kind RTPCodecType, init ...RTPT
 		if err != nil {
 			return nil, err
 		}
-		t = newRTPTransceiver(receiver, nil, RTPTransceiverDirectionRecvonly, kind)
+		t = newRTPTransceiver(receiver, nil, RTPTransceiverDirectionRecvonly, kind, pc.api)
 	default:
 		return nil, errPeerConnAddTransceiverFromKindSupport
 	}
@@ -2208,7 +2208,7 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use
 				t, localTransceivers = satisfyTypeAndDirection(kind, direction, localTransceivers)
 				if t == nil {
 					if len(mediaTransceivers) == 0 {
-						t = &RTPTransceiver{kind: kind}
+						t = &RTPTransceiver{kind: kind, api: pc.api, codecs: pc.api.mediaEngine.getCodecsByKind(kind)}
 						t.setDirection(RTPTransceiverDirectionInactive)
 						mediaTransceivers = append(mediaTransceivers, t)
 					}
diff --git a/rtpreceiver.go b/rtpreceiver.go
index 446e8ae9..cec59339 100644
--- a/rtpreceiver.go
+++ b/rtpreceiver.go
@@ -38,6 +38,8 @@ type RTPReceiver struct {
 	closed, received chan interface{}
 	mu               sync.RWMutex
 
+	tr *RTPTransceiver
+
 	// A reference to the associated api object
 	api *API
 }
@@ -60,6 +62,12 @@ func (api *API) NewRTPReceiver(kind RTPCodecType, transport *DTLSTransport) (*RT
 	return r, nil
 }
 
+func (r *RTPReceiver) setRTPTransceiver(tr *RTPTransceiver) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	r.tr = tr
+}
+
 // Transport returns the currently-configured *DTLSTransport or nil
 // if one has not yet been configured
 func (r *RTPReceiver) Transport() *DTLSTransport {
@@ -68,10 +76,18 @@ func (r *RTPReceiver) Transport() *DTLSTransport {
 	return r.transport
 }
 
+func (r *RTPReceiver) getParameters() RTPParameters {
+	parameters := r.api.mediaEngine.getRTPParametersByKind(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+	parameters.Codecs = r.tr.getCodecs()
+	return parameters
+}
+
 // GetParameters describes the current configuration for the encoding and
 // transmission of media on the receiver's track.
 func (r *RTPReceiver) GetParameters() RTPParameters {
-	return r.api.mediaEngine.getRTPParametersByKind(r.kind, []RTPTransceiverDirection{RTPTransceiverDirectionRecvonly})
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+	return r.getParameters()
 }
 
 // Track returns the RtpTransceiver TrackRemote
@@ -119,7 +135,7 @@ func (r *RTPReceiver) Receive(parameters RTPReceiveParameters) error {
 			),
 		}
 
-		globalParams := r.GetParameters()
+		globalParams := r.getParameters()
 		codec := RTPCodecCapability{}
 		if len(globalParams.Codecs) != 0 {
 			codec = globalParams.Codecs[0].RTPCodecCapability
diff --git a/rtpsender.go b/rtpsender.go
index fb21d1bd..96cb926a 100644
--- a/rtpsender.go
+++ b/rtpsender.go
@@ -38,6 +38,8 @@ type RTPSender struct {
 	api *API
 	id  string
 
+	tr *RTPTransceiver
+
 	mu                     sync.RWMutex
 	sendCalled, stopCalled chan struct{}
 }
@@ -88,6 +90,12 @@ func (r *RTPSender) setNegotiated() {
 	r.negotiated = true
 }
 
+func (r *RTPSender) setRTPTransceiver(tr *RTPTransceiver) {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	r.tr = tr
+}
+
 // Transport returns the currently-configured *DTLSTransport or nil
 // if one has not yet been configured
 func (r *RTPSender) Transport() *DTLSTransport {
@@ -99,7 +107,7 @@ func (r *RTPSender) Transport() *DTLSTransport {
 // GetParameters describes the current configuration for the encoding and
 // transmission of media on the sender's track.
 func (r *RTPSender) GetParameters() RTPSendParameters {
-	return RTPSendParameters{
+	sendParameters := RTPSendParameters{
 		RTPParameters: r.api.mediaEngine.getRTPParametersByKind(
 			r.track.Kind(),
 			[]RTPTransceiverDirection{RTPTransceiverDirectionSendonly},
@@ -113,6 +121,8 @@ func (r *RTPSender) GetParameters() RTPSendParameters {
 			},
 		},
 	}
+	sendParameters.Codecs = r.tr.getCodecs()
+	return sendParameters
 }
 
 // Track returns the RTCRtpTransceiver track, or nil
diff --git a/rtptransceiver.go b/rtptransceiver.go
index 62291565..4d21b545 100644
--- a/rtptransceiver.go
+++ b/rtptransceiver.go
@@ -4,6 +4,7 @@ package webrtc
 
 import (
 	"fmt"
+	"sync"
 	"sync/atomic"
 
 	"github.com/pion/rtp"
@@ -16,8 +17,13 @@ type RTPTransceiver struct {
 	receiver  atomic.Value // *RTPReceiver
 	direction atomic.Value // RTPTransceiverDirection
 
+	codecs []RTPCodecParameters // User provided codecs via SetCodecPreferences
+
 	stopped bool
 	kind    RTPCodecType
+
+	api *API
+	mu  sync.RWMutex
 }
 
 func newRTPTransceiver(
@@ -25,14 +31,51 @@ func newRTPTransceiver(
 	sender *RTPSender,
 	direction RTPTransceiverDirection,
 	kind RTPCodecType,
+	api *API,
 ) *RTPTransceiver {
-	t := &RTPTransceiver{kind: kind}
+	t := &RTPTransceiver{kind: kind, api: api}
 	t.setReceiver(receiver)
 	t.setSender(sender)
 	t.setDirection(direction)
 	return t
 }
 
+// SetCodecPreferences sets preferred list of supported codecs
+// if codecs is empty or nil we reset to default from MediaEngine
+func (t *RTPTransceiver) SetCodecPreferences(codecs []RTPCodecParameters) error {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+
+	for _, codec := range codecs {
+		if _, matchType := codecParametersFuzzySearch(codec, t.api.mediaEngine.getCodecsByKind(t.kind)); matchType == codecMatchNone {
+			return fmt.Errorf("%w %s", errRTPTransceiverCodecUnsupported, codec.MimeType)
+		}
+	}
+
+	t.codecs = codecs
+	return nil
+}
+
+// Codecs returns list of supported codecs
+func (t *RTPTransceiver) getCodecs() []RTPCodecParameters {
+	t.mu.RLock()
+	defer t.mu.RUnlock()
+
+	mediaEngineCodecs := t.api.mediaEngine.getCodecsByKind(t.kind)
+	if len(t.codecs) == 0 {
+		return mediaEngineCodecs
+	}
+
+	filteredCodecs := []RTPCodecParameters{}
+	for _, codec := range t.codecs {
+		if c, matchType := codecParametersFuzzySearch(codec, mediaEngineCodecs); matchType != codecMatchNone {
+			filteredCodecs = append(filteredCodecs, c)
+		}
+	}
+
+	return filteredCodecs
+}
+
 // Sender returns the RTPTransceiver's RTPSender if it has one
 func (t *RTPTransceiver) Sender() *RTPSender {
 	if v := t.sender.Load(); v != nil {
@@ -49,6 +92,14 @@ func (t *RTPTransceiver) SetSender(s *RTPSender, track TrackLocal) error {
 }
 
 func (t *RTPTransceiver) setSender(s *RTPSender) {
+	if s != nil {
+		s.setRTPTransceiver(t)
+	}
+
+	if prevSender := t.Sender(); prevSender != nil {
+		prevSender.setRTPTransceiver(nil)
+	}
+
 	t.sender.Store(s)
 }
 
@@ -106,6 +157,14 @@ func (t *RTPTransceiver) Stop() error {
 }
 
 func (t *RTPTransceiver) setReceiver(r *RTPReceiver) {
+	if r != nil {
+		r.setRTPTransceiver(t)
+	}
+
+	if prevReceiver := t.Receiver(); prevReceiver != nil {
+		prevReceiver.setRTPTransceiver(nil)
+	}
+
 	t.receiver.Store(r)
 }
 
diff --git a/sdp.go b/sdp.go
index af4c3646..8aca0041 100644
--- a/sdp.go
+++ b/sdp.go
@@ -292,7 +292,7 @@ func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates b
 		WithPropertyAttribute(sdp.AttrKeyRTCPMux).
 		WithPropertyAttribute(sdp.AttrKeyRTCPRsize)
 
-	codecs := mediaEngine.getCodecsByKind(t.kind)
+	codecs := t.getCodecs()
 	for _, codec := range codecs {
 		name := strings.TrimPrefix(codec.MimeType, "audio/")
 		name = strings.TrimPrefix(name, "video/")


Test patch:
diff --git a/rtptransceiver_test.go b/rtptransceiver_test.go
new file mode 100644
index 00000000..f5d46337
--- /dev/null
+++ b/rtptransceiver_test.go
@@ -0,0 +1,133 @@
+// +build !js
+
+package webrtc
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_RTPTransceiver_SetCodecPreferences(t *testing.T) {
+	me := &MediaEngine{}
+	api := NewAPI(WithMediaEngine(me))
+	assert.NoError(t, me.RegisterDefaultCodecs())
+
+	me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo)
+	me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio)
+
+	tr := RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
+	assert.EqualValues(t, me.videoCodecs, tr.getCodecs())
+
+	failTestCases := [][]RTPCodecParameters{
+		{
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
+				PayloadType:        111,
+			},
+		},
+		{
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+				PayloadType:        96,
+			},
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
+				PayloadType:        111,
+			},
+		},
+	}
+
+	for _, testCase := range failTestCases {
+		assert.Error(t, tr.SetCodecPreferences(testCase), errRTPTransceiverCodecUnsupported)
+	}
+
+	successTestCases := [][]RTPCodecParameters{
+		{
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+				PayloadType:        96,
+			},
+		},
+		{
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+				PayloadType:        96,
+			},
+			{
+				RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=96", nil},
+				PayloadType:        97,
+			},
+
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeVP9, 90000, 0, "profile-id=0", nil},
+				PayloadType:        98,
+			},
+			{
+				RTPCodecCapability: RTPCodecCapability{"video/rtx", 90000, 0, "apt=98", nil},
+				PayloadType:        99,
+			},
+		},
+	}
+
+	for _, testCase := range successTestCases {
+		assert.NoError(t, tr.SetCodecPreferences(testCase))
+	}
+
+	assert.NoError(t, tr.SetCodecPreferences(nil))
+	assert.NotEqual(t, 0, len(tr.getCodecs()))
+
+	assert.NoError(t, tr.SetCodecPreferences([]RTPCodecParameters{}))
+	assert.NotEqual(t, 0, len(tr.getCodecs()))
+}
+
+// Assert that SetCodecPreferences properly filters codecs and PayloadTypes are respected
+func Test_RTPTransceiver_SetCodecPreferences_PayloadType(t *testing.T) {
+	testCodec := RTPCodecParameters{
+		RTPCodecCapability: RTPCodecCapability{"video/testCodec", 90000, 0, "", nil},
+		PayloadType:        50,
+	}
+
+	m := &MediaEngine{}
+	assert.NoError(t, m.RegisterDefaultCodecs())
+
+	offerPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+
+	assert.NoError(t, m.RegisterCodec(testCodec, RTPCodecTypeVideo))
+
+	answerPC, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
+	assert.NoError(t, err)
+
+	_, err = offerPC.AddTransceiverFromKind(RTPCodecTypeVideo)
+	assert.NoError(t, err)
+
+	answerTransceiver, err := answerPC.AddTransceiverFromKind(RTPCodecTypeVideo)
+	assert.NoError(t, err)
+
+	assert.NoError(t, answerTransceiver.SetCodecPreferences([]RTPCodecParameters{
+		testCodec,
+		{
+			RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+			PayloadType:        51,
+		},
+	}))
+
+	offer, err := offerPC.CreateOffer(nil)
+	assert.NoError(t, err)
+
+	assert.NoError(t, offerPC.SetLocalDescription(offer))
+	assert.NoError(t, answerPC.SetRemoteDescription(offer))
+
+	answer, err := answerPC.CreateAnswer(nil)
+	assert.NoError(t, err)
+
+	// VP8 with proper PayloadType
+	assert.NotEqual(t, -1, strings.Index(answer.SDP, "a=rtpmap:96 VP8/90000"))
+
+	// testCodec is ignored since offerer doesn't support
+	assert.Equal(t, -1, strings.Index(answer.SDP, "testCodec"))
+
+	closePairNow(t, offerPC, answerPC)
+}
diff --git a/sdp_test.go b/sdp_test.go
index 7bed5081..7a657bba 100644
--- a/sdp_test.go
+++ b/sdp_test.go
@@ -307,6 +307,8 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
 	engine := &MediaEngine{}
 	assert.NoError(t, engine.RegisterDefaultCodecs())
 
+	api := NewAPI(WithMediaEngine(engine))
+
 	sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 	assert.NoError(t, err)
 
@@ -317,13 +319,17 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
 		{
 			id: "video",
 			transceivers: []*RTPTransceiver{{
-				kind: RTPCodecTypeVideo,
+				kind:   RTPCodecTypeVideo,
+				api:    api,
+				codecs: engine.getCodecsByKind(RTPCodecTypeVideo),
 			}},
 		},
 		{
 			id: "audio",
 			transceivers: []*RTPTransceiver{{
-				kind: RTPCodecTypeAudio,
+				kind:   RTPCodecTypeAudio,
+				api:    api,
+				codecs: engine.getCodecsByKind(RTPCodecTypeAudio),
 			}},
 		},
 		{
@@ -363,21 +369,22 @@ func TestMediaDescriptionFingerprints(t *testing.T) {
 
 func TestPopulateSDP(t *testing.T) {
 	t.Run("Rid", func(t *testing.T) {
-		tr := &RTPTransceiver{kind: RTPCodecTypeVideo}
+		se := SettingEngine{}
+
+		me := &MediaEngine{}
+		assert.NoError(t, me.RegisterDefaultCodecs())
+		api := NewAPI(WithMediaEngine(me))
+
+		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
 		tr.setDirection(RTPTransceiverDirectionRecvonly)
 		ridMap := map[string]string{
 			"ridkey": "some",
 		}
 		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}, ridMap: ridMap}}
 
-		se := SettingEngine{}
-
-		m := MediaEngine{}
-		assert.NoError(t, m.RegisterDefaultCodecs())
-
 		d := &sdp.SessionDescription{}
 
-		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, &m, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
+		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
 		assert.Nil(t, err)
 
 		// Test contains rid map keys
@@ -397,6 +404,50 @@ func TestPopulateSDP(t *testing.T) {
 		}
 		assert.Equal(t, true, found, "Rid key should be present")
 	})
+	t.Run("SetCodecPreferences", func(t *testing.T) {
+		se := SettingEngine{}
+
+		me := &MediaEngine{}
+		assert.NoError(t, me.RegisterDefaultCodecs())
+		api := NewAPI(WithMediaEngine(me))
+		me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo)
+		me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio)
+
+		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
+		tr.setDirection(RTPTransceiverDirectionRecvonly)
+		codecErr := tr.SetCodecPreferences([]RTPCodecParameters{
+			{
+				RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
+				PayloadType:        96,
+			},
+		})
+		assert.NoError(t, codecErr)
+
+		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
+
+		d := &sdp.SessionDescription{}
+
+		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete)
+		assert.Nil(t, err)
+
+		// Test codecs
+		foundVP8 := false
+		for _, desc := range offerSdp.MediaDescriptions {
+			if desc.MediaName.Media != "video" {
+				continue
+			}
+			for _, a := range desc.Attributes {
+				if strings.Contains(a.Key, "rtpmap") {
+					if a.Value == "98 VP9/90000" {
+						t.Fatal("vp9 should not be present in sdp")
+					} else if a.Value == "96 VP8/90000" {
+						foundVP8 = true
+					}
+				}
+			}
+		}
+		assert.Equal(t, true, foundVP8, "vp8 should be present in sdp")
+	})
 }
 
 func TestGetRIDs(t *testing.T) {