diff --git a/rtpcodec.go b/rtpcodec.go index d4733dbd07e..d03dba0bd30 100644 --- a/rtpcodec.go +++ b/rtpcodec.go @@ -5,6 +5,7 @@ package webrtc import ( "fmt" + "strconv" "strings" "github.com/pion/webrtc/v4/internal/fmtp" @@ -155,6 +156,38 @@ func findRTXPayloadType(needle PayloadType, haystack []RTPCodecParameters) Paylo return PayloadType(0) } +// Given needle CodecParameters, returns if needle is RTX and +// if primary codec corresponding to that needle is in the haystack of codecs. +func primaryPayloadTypeForRTXExists(needle RTPCodecParameters, haystack []RTPCodecParameters) ( + isRTX bool, primaryExists bool, +) { + if !strings.EqualFold(needle.MimeType, MimeTypeRTX) { + return + } + + isRTX = true + parsed := fmtp.Parse(needle.MimeType, needle.ClockRate, needle.Channels, needle.SDPFmtpLine) + aptPayload, ok := parsed.Parameter("apt") + if !ok { + return + } + + primaryPayloadType, err := strconv.Atoi(aptPayload) + if err != nil || primaryPayloadType < 0 || primaryPayloadType > 255 { + return + } + + for _, c := range haystack { + if c.PayloadType == PayloadType(primaryPayloadType) { + primaryExists = true + + return + } + } + + return +} + // For now, only FlexFEC is supported. func findFECPayloadType(haystack []RTPCodecParameters) PayloadType { for _, c := range haystack { diff --git a/rtpcodec_test.go b/rtpcodec_test.go index 0328f36f7f2..8b5923decfc 100644 --- a/rtpcodec_test.go +++ b/rtpcodec_test.go @@ -9,6 +9,199 @@ import ( "github.com/stretchr/testify/assert" ) +func TestFindPrimaryPayloadTypeForRTX(t *testing.T) { + for _, test := range []struct { + Name string + Needle RTPCodecParameters + Haystack []RTPCodecParameters + ResultIsRTX bool + ResultPrimaryExists bool + }{ + { + Name: "not RTX", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + SDPFmtpLine: "apt=2", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: false, + ResultPrimaryExists: false, + }, + { + Name: "incorrect fmtp", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "incorrect-fmtp", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: false, + }, + { + Name: "incomplete fmtp", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "apt=", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: false, + }, + { + Name: "primary payload type outside range (negative)", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "apt=-10", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: false, + }, + { + Name: "primary payload type outside range (high positive)", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "apt=1000", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: false, + }, + { + Name: "non-matching needle", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "apt=23", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: false, + }, + { + Name: "matching needle", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "apt=1", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: true, + }, + { + Name: "matching fmtp is a substring", + Needle: RTPCodecParameters{ + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + SDPFmtpLine: "apt=1;rtx-time:2000", + }, + }, + Haystack: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH264, + ClockRate: 90000, + }, + }, + }, + ResultIsRTX: true, + ResultPrimaryExists: true, + }, + } { + t.Run(test.Name, func(t *testing.T) { + isRTX, primaryExists := primaryPayloadTypeForRTXExists(test.Needle, test.Haystack) + assert.Equal(t, test.ResultIsRTX, isRTX) + assert.Equal(t, test.ResultPrimaryExists, primaryExists) + }) + } +} + func TestFindFECPayloadType(t *testing.T) { for _, test := range []struct { Haystack []RTPCodecParameters diff --git a/rtptransceiver.go b/rtptransceiver.go index 5ce0f2d4819..3741e932ff8 100644 --- a/rtptransceiver.go +++ b/rtptransceiver.go @@ -8,6 +8,7 @@ package webrtc import ( "fmt" + "strings" "sync" "sync/atomic" @@ -60,6 +61,19 @@ func (t *RTPTransceiver) SetCodecPreferences(codecs []RTPCodecParameters) error } } + // remove RTX codecs if there is no corresponding primary codec + for i := len(codecs) - 1; i >= 0; i-- { + c := codecs[i] + if !strings.EqualFold(c.MimeType, MimeTypeRTX) { + continue + } + + if isRTX, primaryExists := primaryPayloadTypeForRTXExists(c, codecs); isRTX && !primaryExists { + // no primary for RTX, remove the RTX + codecs = append(codecs[:i], codecs[i+1:]...) + } + } + t.codecs = codecs return nil