From 789da25928bad19a3c752b9912fcb1de175e0472 Mon Sep 17 00:00:00 2001 From: cnderrauber Date: Thu, 7 Sep 2023 10:05:06 +0800 Subject: [PATCH] Generate answer to match group bundle in offer Generate answer to match group bundle in offer --- peerconnection.go | 9 +++-- rtpcodec.go | 2 +- sdp.go | 46 +++++++++++++++++++++++-- sdp_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 129 insertions(+), 13 deletions(-) diff --git a/peerconnection.go b/peerconnection.go index 6e33bff0977..869f5d1eba2 100644 --- a/peerconnection.go +++ b/peerconnection.go @@ -2353,7 +2353,7 @@ func (pc *PeerConnection) generateUnmatchedSDP(transceivers []*RTPTransceiver, u return nil, err } - return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, true, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState()) + return populateSDP(d, isPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, true, pc.api.mediaEngine, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), candidates, iceParams, mediaSections, pc.ICEGatheringState(), nil) } // generateMatchedSDP generates a SDP and takes the remote state into account @@ -2451,6 +2451,7 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use } } + var bundleGroup *string // If we are offering also include unmatched local transceivers if includeUnmatched { if !detectedPlanB { @@ -2469,6 +2470,10 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use mediaSections = append(mediaSections, mediaSection{id: strconv.Itoa(len(mediaSections)), data: true}) } } + } else if remoteDescription != nil { + groupValue, _ := remoteDescription.parsed.Attribute(sdp.AttrKeyGroup) + groupValue = strings.TrimLeft(groupValue, "BUNDLE") + bundleGroup = &groupValue } if pc.configuration.SDPSemantics == SDPSemanticsUnifiedPlanWithFallback && detectedPlanB { @@ -2480,7 +2485,7 @@ func (pc *PeerConnection) generateMatchedSDP(transceivers []*RTPTransceiver, use return nil, err } - return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, isExtmapAllowMixed, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState()) + return populateSDP(d, detectedPlanB, dtlsFingerprints, pc.api.settingEngine.sdpMediaLevelFingerprints, pc.api.settingEngine.candidates.ICELite, isExtmapAllowMixed, pc.api.mediaEngine, connectionRole, candidates, iceParams, mediaSections, pc.ICEGatheringState(), bundleGroup) } func (pc *PeerConnection) setGatherCompleteHandler(handler func()) { diff --git a/rtpcodec.go b/rtpcodec.go index 5692d5f564d..4b8f81c4e74 100644 --- a/rtpcodec.go +++ b/rtpcodec.go @@ -24,7 +24,7 @@ const ( func (t RTPCodecType) String() string { switch t { case RTPCodecTypeAudio: - return "audio" + return "audio" //nolint: goconst case RTPCodecTypeVideo: return "video" //nolint: goconst default: diff --git a/sdp.go b/sdp.go index 139f40754d5..0622c759011 100644 --- a/sdp.go +++ b/sdp.go @@ -507,8 +507,39 @@ type mediaSection struct { ridMap map[string]string } +func bundleMatchFromRemote(matchBundleGroup *string) func(mid string) bool { + if matchBundleGroup == nil { + return func(midValue string) bool { + return true + } + } + bundleTags := strings.Split(*matchBundleGroup, " ") + return func(midValue string) bool { + for _, tag := range bundleTags { + if tag == midValue { + return true + } + } + return false + } +} + // populateSDP serializes a PeerConnections state into an SDP -func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, isExtmapAllowMixed bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState) (*sdp.SessionDescription, error) { +func populateSDP( + d *sdp.SessionDescription, + isPlanB bool, + dtlsFingerprints []DTLSFingerprint, + mediaDescriptionFingerprint bool, + isICELite bool, + isExtmapAllowMixed bool, + mediaEngine *MediaEngine, + connectionRole sdp.ConnectionRole, + candidates []ICECandidate, + iceParams ICEParameters, + mediaSections []mediaSection, + iceGatheringState ICEGatheringState, + matchBundleGroup *string, +) (*sdp.SessionDescription, error) { var err error mediaDtlsFingerprints := []DTLSFingerprint{} @@ -518,6 +549,8 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL bundleValue := "BUNDLE" bundleCount := 0 + + bundleMatch := bundleMatchFromRemote(matchBundleGroup) appendBundle := func(midValue string) { bundleValue += " " + midValue bundleCount++ @@ -544,7 +577,11 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL } if shouldAddID { - appendBundle(m.id) + if bundleMatch(m.id) { + appendBundle(m.id) + } else { + d.MediaDescriptions[len(d.MediaDescriptions)-1].MediaName.Port = sdp.RangedPort{Value: 0} + } } } @@ -563,7 +600,10 @@ func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTL d = d.WithPropertyAttribute(sdp.AttrKeyExtMapAllowMixed) } - return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil + if bundleCount > 0 { + d = d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue) + } + return d, nil } func getMidValue(media *sdp.MediaDescription) string { diff --git a/sdp_test.go b/sdp_test.go index a4dfdff2e05..a0d99b140f7 100644 --- a/sdp_test.go +++ b/sdp_test.go @@ -357,7 +357,7 @@ func TestMediaDescriptionFingerprints(t *testing.T) { s, err = populateSDP(s, false, dtlsFingerprints, SDPMediaDescriptionFingerprints, - false, true, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew) + false, true, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew, nil) assert.NoError(t, err) sdparray, err := s.Marshal() @@ -388,7 +388,7 @@ func TestPopulateSDP(t *testing.T) { d := &sdp.SessionDescription{} - offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete) + offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil) assert.Nil(t, err) // Test contains rid map keys @@ -431,7 +431,7 @@ func TestPopulateSDP(t *testing.T) { d := &sdp.SessionDescription{} - offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete) + offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil) assert.Nil(t, err) // Test codecs @@ -456,7 +456,7 @@ func TestPopulateSDP(t *testing.T) { se := SettingEngine{} se.SetLite(true) - offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete) + offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil) assert.Nil(t, err) var found bool @@ -489,7 +489,7 @@ func TestPopulateSDP(t *testing.T) { d := &sdp.SessionDescription{} - offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete) + offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil) assert.NoError(t, err) // Test codecs @@ -507,7 +507,7 @@ func TestPopulateSDP(t *testing.T) { }) t.Run("allow mixed extmap", func(t *testing.T) { se := SettingEngine{} - offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete) + offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil) assert.Nil(t, err) var found bool @@ -522,7 +522,7 @@ func TestPopulateSDP(t *testing.T) { } assert.Equal(t, true, found, "AllowMixedExtMap key should be present") - offerSdp, err = populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, false, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete) + offerSdp, err = populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, false, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil) assert.Nil(t, err) found = false @@ -537,6 +537,77 @@ func TestPopulateSDP(t *testing.T) { } assert.Equal(t, false, found, "AllowMixedExtMap key should not be present") }) + t.Run("bundle all", func(t *testing.T) { + 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) + mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}} + + d := &sdp.SessionDescription{} + + offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil) + assert.Nil(t, err) + + bundle, ok := offerSdp.Attribute(sdp.AttrKeyGroup) + assert.True(t, ok) + assert.Equal(t, "BUNDLE video", bundle) + }) + t.Run("bundle matched", func(t *testing.T) { + se := SettingEngine{} + + me := &MediaEngine{} + assert.NoError(t, me.RegisterDefaultCodecs()) + api := NewAPI(WithMediaEngine(me)) + + tra := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs} + tra.setDirection(RTPTransceiverDirectionRecvonly) + mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tra}}} + + trv := &RTPTransceiver{kind: RTPCodecTypeAudio, api: api, codecs: me.audioCodecs} + trv.setDirection(RTPTransceiverDirectionRecvonly) + mediaSections = append(mediaSections, mediaSection{id: "audio", transceivers: []*RTPTransceiver{trv}}) + + d := &sdp.SessionDescription{} + + matchedBundle := "audio" + offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, &matchedBundle) + assert.Nil(t, err) + + bundle, ok := offerSdp.Attribute(sdp.AttrKeyGroup) + assert.True(t, ok) + assert.Equal(t, "BUNDLE audio", bundle) + + mediaVideo := offerSdp.MediaDescriptions[0] + mid, ok := mediaVideo.Attribute(sdp.AttrKeyMID) + assert.True(t, ok) + assert.Equal(t, "video", mid) + assert.True(t, mediaVideo.MediaName.Port.Value == 0) + }) + t.Run("empty bundle group", func(t *testing.T) { + se := SettingEngine{} + + me := &MediaEngine{} + assert.NoError(t, me.RegisterDefaultCodecs()) + api := NewAPI(WithMediaEngine(me)) + + tra := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs} + tra.setDirection(RTPTransceiverDirectionRecvonly) + mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tra}}} + + d := &sdp.SessionDescription{} + + matchedBundle := "" + offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, &matchedBundle) + assert.Nil(t, err) + + _, ok := offerSdp.Attribute(sdp.AttrKeyGroup) + assert.False(t, ok) + }) } func TestGetRIDs(t *testing.T) {