diff --git a/pkg/sfu/downtrack.go b/pkg/sfu/downtrack.go index 766620405..301169cf2 100644 --- a/pkg/sfu/downtrack.go +++ b/pkg/sfu/downtrack.go @@ -23,7 +23,6 @@ import ( "math" "math/rand" "slices" - "strings" "sync" "time" @@ -511,12 +510,10 @@ func (d *DownTrack) Bind(t webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, return } - isFECEnabled := false + isFECEnabled := utils.IsFECEnabled(matchedUpstreamCodec, d.upstreamCodecs) if mime.IsMimeTypeStringRED(matchedUpstreamCodec.MimeType) { d.isRED = true for _, c := range d.upstreamCodecs { - isFECEnabled = strings.Contains(strings.ToLower(c.SDPFmtpLine), "useinbandfec=1") - // assume upstream primary codec is opus since we only support it for audio now if mime.IsMimeTypeStringOpus(c.MimeType) { d.upstreamPrimaryPT = uint8(c.PayloadType) @@ -539,8 +536,6 @@ func (d *DownTrack) Bind(t webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, ) } d.primaryPT = uint8(primaryPT) - } else if mime.IsMimeTypeStringAudio(matchedUpstreamCodec.MimeType) { - isFECEnabled = strings.Contains(strings.ToLower(matchedUpstreamCodec.SDPFmtpLine), "fec") } logFields := []any{ @@ -666,7 +661,7 @@ func (d *DownTrack) handleUpstreamCodecChange(mimeType string) { oldPT, oldRtxPT, oldCodec := d.payloadType.Load(), d.payloadTypeRTX.Load(), d.codec.Load().(webrtc.RTPCodecCapability) - var codec webrtc.RTPCodecParameters + var codec, matchedUpstreamCodec webrtc.RTPCodecParameters for _, c := range d.upstreamCodecs { if !mime.IsMimeTypeStringEqual(c.MimeType, mimeType) { continue @@ -675,6 +670,7 @@ func (d *DownTrack) handleUpstreamCodecChange(mimeType string) { matchCodec, err := utils.CodecParametersFuzzySearch(c, d.negotiatedCodecParameters) if err == nil { codec = matchCodec + matchedUpstreamCodec = c break } } @@ -698,7 +694,8 @@ func (d *DownTrack) handleUpstreamCodecChange(mimeType string) { d.rtpStats.SetClockRate(codec.RTPCodecCapability.ClockRate) d.rtpStatsRTX.SetClockRate(codec.RTPCodecCapability.ClockRate) - isFECEnabled := strings.Contains(strings.ToLower(codec.SDPFmtpLine), "fec") + // FEC is what the publisher encodes, so check the upstream codec, not the negotiated one + isFECEnabled := utils.IsFECEnabled(matchedUpstreamCodec, d.upstreamCodecs) d.bindLock.Unlock() d.params.Logger.Infow( @@ -706,6 +703,7 @@ func (d *DownTrack) handleUpstreamCodecChange(mimeType string) { "oldPT", oldPT, "newPT", d.payloadType.Load(), "oldRTXPT", oldRtxPT, "newRTXPT", d.payloadTypeRTX.Load(), "oldCodec", oldCodec, "newCodec", codec.RTPCodecCapability, + "isFECEnabled", isFECEnabled, ) receiver := d.Receiver() diff --git a/pkg/sfu/utils/helpers.go b/pkg/sfu/utils/helpers.go index c1a9da50a..9d4b8488e 100644 --- a/pkg/sfu/utils/helpers.go +++ b/pkg/sfu/utils/helpers.go @@ -17,6 +17,7 @@ package utils import ( "errors" "fmt" + "regexp" "strconv" "strings" @@ -66,6 +67,28 @@ func FindRTXPayloadType(needle webrtc.PayloadType, haystack []webrtc.RTPCodecPar return webrtc.PayloadType(0) } +// matches useinbandfec=1 as one fmtp parameter, ignoring case and spaces around the parameter and the "=" +var fecEnabledRegex = regexp.MustCompile(`(?i)(^|;)\s*useinbandfec\s*=\s*1\s*(;|$)`) + +// IsFECEnabled reports whether codec carries opus in-band FEC. For RED, it checks the opus codec in codecs. +func IsFECEnabled(codec webrtc.RTPCodecParameters, codecs []webrtc.RTPCodecParameters) bool { + switch { + case mime.IsMimeTypeStringRED(codec.MimeType): + for _, c := range codecs { + if mime.IsMimeTypeStringOpus(c.MimeType) { + return fecEnabledRegex.MatchString(c.SDPFmtpLine) + } + } + return false + + case mime.IsMimeTypeStringAudio(codec.MimeType): + return fecEnabledRegex.MatchString(codec.SDPFmtpLine) + + default: + return false + } +} + // GetHeaderExtensionID returns the ID of a header extension, or 0 if not found func GetHeaderExtensionID(extensions []interceptor.RTPHeaderExtension, extension webrtc.RTPHeaderExtensionCapability) int { for _, h := range extensions { diff --git a/pkg/sfu/utils/helpers_test.go b/pkg/sfu/utils/helpers_test.go index c2c30bb3e..73bcb8b9a 100644 --- a/pkg/sfu/utils/helpers_test.go +++ b/pkg/sfu/utils/helpers_test.go @@ -53,3 +53,62 @@ func TestFindRTXPayloadType(t *testing.T) { }) } } + +func TestIsFECEnabled(t *testing.T) { + codec := func(mimeType mime.MimeType, fmtp string) webrtc.RTPCodecParameters { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeType.String(), SDPFmtpLine: fmtp}, + } + } + opusOn := codec(mime.MimeTypeOpus, "minptime=10;useinbandfec=1") + opusOff := codec(mime.MimeTypeOpus, "minptime=10;useinbandfec=0") + red := codec(mime.MimeTypeRED, "111/111") + vp8 := codec(mime.MimeTypeVP8, "") + + t.Run("fmtp", func(t *testing.T) { + tests := []struct { + fmtp string + expected bool + }{ + {"minptime=10;useinbandfec=1", true}, + {"useinbandfec=1", true}, + {"useinbandfec=1;stereo=1", true}, + {"minptime=10; useinbandfec = 1", true}, + {"minptime=10; useinbandfec=1 ;stereo=1", true}, + {"minptime=10;UseInbandFEC=1", true}, + {"minptime=10;useinbandfec=0", false}, + {"minptime=10;useinbandfec=10", false}, + {"minptime=10;xuseinbandfec=1", false}, + {"minptime=10", false}, + {"", false}, + } + for _, tc := range tests { + t.Run(tc.fmtp, func(t *testing.T) { + opus := codec(mime.MimeTypeOpus, tc.fmtp) + require.Equal(t, tc.expected, IsFECEnabled(opus, []webrtc.RTPCodecParameters{opus})) + }) + } + }) + + t.Run("codecs", func(t *testing.T) { + tests := []struct { + name string + codec webrtc.RTPCodecParameters + codecs []webrtc.RTPCodecParameters + expected bool + }{ + {"opus with FEC", opusOn, []webrtc.RTPCodecParameters{opusOn}, true}, + {"opus without FEC", opusOff, []webrtc.RTPCodecParameters{opusOff}, false}, + {"RED over opus with FEC", red, []webrtc.RTPCodecParameters{red, opusOn}, true}, + {"RED over opus without FEC", red, []webrtc.RTPCodecParameters{red, opusOff}, false}, + {"RED without an opus codec", red, []webrtc.RTPCodecParameters{red}, false}, + {"video", vp8, []webrtc.RTPCodecParameters{vp8}, false}, + {"no codec", webrtc.RTPCodecParameters{}, []webrtc.RTPCodecParameters{opusOn}, false}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, IsFECEnabled(tc.codec, tc.codecs)) + }) + } + }) +}