diff --git a/mediaengine.go b/mediaengine.go index d9f48d81..07d96721 100644 --- a/mediaengine.go +++ b/mediaengine.go @@ -273,6 +273,44 @@ func (m *MediaEngine) RegisterCodec(codec RTPCodecParameters, typ RTPCodecType) return err } +func (m *MediaEngine) autoConfigRTXCodecs() error { + additionalRTXCodecs := []RTPCodecParameters{} + for _, codec := range m.videoCodecs { + // ignore FEC & RTX + if strings.Contains(codec.MimeType, MimeTypeFlexFEC) || codec.MimeType == MimeTypeRTX { + continue + } + haveNACK := false + for _, fb := range codec.RTCPFeedback { + if fb.Type == "nack" { + haveNACK = true + + break + } + } + if haveNACK { + additionalRTXCodecs = append(additionalRTXCodecs, RTPCodecParameters{ + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: fmt.Sprintf("apt=%d", codec.PayloadType), + RTCPFeedback: nil, + }, + PayloadType: codec.PayloadType + 1, + }) + } + } + for i := range additionalRTXCodecs { + err := m.RegisterCodec(additionalRTXCodecs[i], RTPCodecTypeVideo) + if err != nil { + return err + } + } + + return nil +} + // RegisterHeaderExtension adds a header extension to the MediaEngine // To determine the negotiated value use `GetHeaderExtensionID` after signaling is complete. // diff --git a/mediaengine_test.go b/mediaengine_test.go index a4d4d6f3..ca867306 100644 --- a/mediaengine_test.go +++ b/mediaengine_test.go @@ -961,3 +961,158 @@ a=ssrc:4281768245 msid:6ff05509-be96-4ef1-a74f-425e14720983 16d5d7fe-d076-4718-9 assert.Len(t, mediaEngine.negotiatedVideoCodecs, 2) }) } + +func TestAutoConfigRTXCodecs(t *testing.T) { + for _, test := range []struct { + Original []RTPCodecParameters + ExpectedResult []RTPCodecParameters + ExpectedError error + }{ + { + // no video codec + Original: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeFlexFEC03, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "repair-window=10000000", + RTCPFeedback: nil, + }, + }, + }, + ExpectedResult: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeFlexFEC03, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "repair-window=10000000", + RTCPFeedback: nil, + }, + }, + }, + ExpectedError: nil, + }, + { + // one video codec with no nack rtcp feedback + Original: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH265, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: nil, + }, + }, + }, + ExpectedResult: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH265, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: nil, + }, + }, + }, + ExpectedError: nil, + }, + { + // one video codec with nack and pli rtcp feedback + Original: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH265, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: []RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + }, + }, + }, + }, + ExpectedResult: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH265, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: []RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + }, + }, + }, + { + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "apt=1", + RTCPFeedback: nil, + }, + }, + }, + ExpectedError: nil, + }, + { + // multiple video codec, expect error because of PayloadType collision + Original: []RTPCodecParameters{ + { + PayloadType: 1, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeH265, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: []RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + }, + }, + }, + { + PayloadType: 2, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeVP8, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: []RTCPFeedback{ + {Type: "nack", Parameter: ""}, + {Type: "nack", Parameter: "pli"}, + }, + }, + }, + }, + ExpectedResult: nil, + ExpectedError: ErrCodecAlreadyRegistered, + }, + } { + m := &MediaEngine{ + videoCodecs: test.Original, + } + err := m.autoConfigRTXCodecs() + assert.Equal(t, err, test.ExpectedError) + if err == nil { + for i := range m.videoCodecs { + // ignore for following assert + m.videoCodecs[i].statsID = "" + } + assert.Equal(t, m.videoCodecs, test.ExpectedResult) + } + } +} diff --git a/peerconnection.go b/peerconnection.go index c1fce0c8..231144b7 100644 --- a/peerconnection.go +++ b/peerconnection.go @@ -142,8 +142,9 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, pc.iceConnectionState.Store(ICEConnectionStateNew) pc.connectionState.Store(PeerConnectionStateNew) - i, err := api.interceptorRegistry.Build("") - if err != nil { + var i interceptor.Interceptor + var err error + if i, err = api.interceptorRegistry.Build(""); err != nil { return nil, err } @@ -152,6 +153,12 @@ func (api *API) NewPeerConnection(configuration Configuration) (*PeerConnection, interceptor: i, } + if api.settingEngine.autoConfigRTXCodec { + if err = api.mediaEngine.autoConfigRTXCodecs(); err != nil { + return nil, err + } + } + if api.settingEngine.disableMediaEngineCopy { pc.api.mediaEngine = api.mediaEngine } else { diff --git a/settingengine.go b/settingengine.go index ad75f2a7..552dd8c9 100644 --- a/settingengine.go +++ b/settingengine.go @@ -106,6 +106,7 @@ type SettingEngine struct { fireOnTrackBeforeFirstRTP bool disableCloseByDTLS bool dataChannelBlockWrite bool + autoConfigRTXCodec bool } func (e *SettingEngine) getSCTPMaxMessageSize() uint32 { @@ -551,3 +552,8 @@ func (e *SettingEngine) SetFireOnTrackBeforeFirstRTP(fireOnTrackBeforeFirstRTP b func (e *SettingEngine) DisableCloseByDTLS(isEnabled bool) { e.disableCloseByDTLS = isEnabled } + +// AutoConfigRTXCodec sets if the RTX codec should be automatically configured. +func (e *SettingEngine) AutoConfigRTXCodec(autoConfigRTXCodec bool) { + e.autoConfigRTXCodec = autoConfigRTXCodec +} diff --git a/settingengine_test.go b/settingengine_test.go index 1724b65c..b2d6b0af 100644 --- a/settingengine_test.go +++ b/settingengine_test.go @@ -464,3 +464,58 @@ func TestEnableDataChannelBlockWrite(t *testing.T) { assert.ErrorIs(t, err, context.DeadlineExceeded) closePairNow(t, offer, answer) } + +func TestAutoConfigRTXCodec(t *testing.T) { + lim := test.TimeOut(time.Second * 30) + defer lim.Stop() + + report := test.CheckRoutines(t) + defer report() + + settingEngine := SettingEngine{} + settingEngine.DisableMediaEngineCopy(true) + settingEngine.AutoConfigRTXCodec(true) + mediaEngine := &MediaEngine{} + err := mediaEngine.RegisterCodec( + RTPCodecParameters{ + PayloadType: 96, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeVP8, + ClockRate: 90000, + }, + }, + RTPCodecTypeVideo, + ) + assert.Equal(t, err, nil) + api := NewAPI( + WithMediaEngine(mediaEngine), + WithSettingEngine(settingEngine), + ) + config := Configuration{ + ICEServers: []ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } + var pc *PeerConnection + pc, err = api.NewPeerConnection(config) + assert.Equal(t, err, nil) + for i := range mediaEngine.videoCodecs { + mediaEngine.videoCodecs[i].statsID = "" + } + assert.Equal(t, len(mediaEngine.videoCodecs), 2) + assert.Equal(t, mediaEngine.videoCodecs[1], + RTPCodecParameters{ + PayloadType: 97, + RTPCodecCapability: RTPCodecCapability{ + MimeType: MimeTypeRTX, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "apt=96", + RTCPFeedback: nil, + }, + }, + ) + assert.NoError(t, pc.close(true)) +}