github.com/pion/webrtc/v3@v3.2.24/sdp_test.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
     2  // SPDX-License-Identifier: MIT
     3  
     4  //go:build !js
     5  // +build !js
     6  
     7  package webrtc
     8  
     9  import (
    10  	"crypto/ecdsa"
    11  	"crypto/elliptic"
    12  	"crypto/rand"
    13  	"strings"
    14  	"testing"
    15  
    16  	"github.com/pion/sdp/v3"
    17  	"github.com/stretchr/testify/assert"
    18  )
    19  
    20  func TestExtractFingerprint(t *testing.T) {
    21  	t.Run("Good Session Fingerprint", func(t *testing.T) {
    22  		s := &sdp.SessionDescription{
    23  			Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo bar"}},
    24  		}
    25  
    26  		fingerprint, hash, err := extractFingerprint(s)
    27  		assert.NoError(t, err)
    28  		assert.Equal(t, fingerprint, "bar")
    29  		assert.Equal(t, hash, "foo")
    30  	})
    31  
    32  	t.Run("Good Media Fingerprint", func(t *testing.T) {
    33  		s := &sdp.SessionDescription{
    34  			MediaDescriptions: []*sdp.MediaDescription{
    35  				{Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo bar"}}},
    36  			},
    37  		}
    38  
    39  		fingerprint, hash, err := extractFingerprint(s)
    40  		assert.NoError(t, err)
    41  		assert.Equal(t, fingerprint, "bar")
    42  		assert.Equal(t, hash, "foo")
    43  	})
    44  
    45  	t.Run("No Fingerprint", func(t *testing.T) {
    46  		s := &sdp.SessionDescription{}
    47  
    48  		_, _, err := extractFingerprint(s)
    49  		assert.Equal(t, ErrSessionDescriptionNoFingerprint, err)
    50  	})
    51  
    52  	t.Run("Invalid Fingerprint", func(t *testing.T) {
    53  		s := &sdp.SessionDescription{
    54  			Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo"}},
    55  		}
    56  
    57  		_, _, err := extractFingerprint(s)
    58  		assert.Equal(t, ErrSessionDescriptionInvalidFingerprint, err)
    59  	})
    60  
    61  	t.Run("Conflicting Fingerprint", func(t *testing.T) {
    62  		s := &sdp.SessionDescription{
    63  			Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo"}},
    64  			MediaDescriptions: []*sdp.MediaDescription{
    65  				{Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo blah"}}},
    66  			},
    67  		}
    68  
    69  		_, _, err := extractFingerprint(s)
    70  		assert.Equal(t, ErrSessionDescriptionConflictingFingerprints, err)
    71  	})
    72  }
    73  
    74  func TestExtractICEDetails(t *testing.T) {
    75  	const defaultUfrag = "defaultPwd"
    76  	const defaultPwd = "defaultUfrag"
    77  
    78  	t.Run("Missing ice-pwd", func(t *testing.T) {
    79  		s := &sdp.SessionDescription{
    80  			MediaDescriptions: []*sdp.MediaDescription{
    81  				{Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}}},
    82  			},
    83  		}
    84  
    85  		_, _, _, err := extractICEDetails(s, nil)
    86  		assert.Equal(t, err, ErrSessionDescriptionMissingIcePwd)
    87  	})
    88  
    89  	t.Run("Missing ice-ufrag", func(t *testing.T) {
    90  		s := &sdp.SessionDescription{
    91  			MediaDescriptions: []*sdp.MediaDescription{
    92  				{Attributes: []sdp.Attribute{{Key: "ice-pwd", Value: defaultPwd}}},
    93  			},
    94  		}
    95  
    96  		_, _, _, err := extractICEDetails(s, nil)
    97  		assert.Equal(t, err, ErrSessionDescriptionMissingIceUfrag)
    98  	})
    99  
   100  	t.Run("ice details at session level", func(t *testing.T) {
   101  		s := &sdp.SessionDescription{
   102  			Attributes: []sdp.Attribute{
   103  				{Key: "ice-ufrag", Value: defaultUfrag},
   104  				{Key: "ice-pwd", Value: defaultPwd},
   105  			},
   106  			MediaDescriptions: []*sdp.MediaDescription{},
   107  		}
   108  
   109  		ufrag, pwd, _, err := extractICEDetails(s, nil)
   110  		assert.Equal(t, ufrag, defaultUfrag)
   111  		assert.Equal(t, pwd, defaultPwd)
   112  		assert.NoError(t, err)
   113  	})
   114  
   115  	t.Run("ice details at media level", func(t *testing.T) {
   116  		s := &sdp.SessionDescription{
   117  			MediaDescriptions: []*sdp.MediaDescription{
   118  				{
   119  					Attributes: []sdp.Attribute{
   120  						{Key: "ice-ufrag", Value: defaultUfrag},
   121  						{Key: "ice-pwd", Value: defaultPwd},
   122  					},
   123  				},
   124  			},
   125  		}
   126  
   127  		ufrag, pwd, _, err := extractICEDetails(s, nil)
   128  		assert.Equal(t, ufrag, defaultUfrag)
   129  		assert.Equal(t, pwd, defaultPwd)
   130  		assert.NoError(t, err)
   131  	})
   132  
   133  	t.Run("Conflict ufrag", func(t *testing.T) {
   134  		s := &sdp.SessionDescription{
   135  			Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: "invalidUfrag"}},
   136  			MediaDescriptions: []*sdp.MediaDescription{
   137  				{Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}}},
   138  			},
   139  		}
   140  
   141  		_, _, _, err := extractICEDetails(s, nil)
   142  		assert.Equal(t, err, ErrSessionDescriptionConflictingIceUfrag)
   143  	})
   144  
   145  	t.Run("Conflict pwd", func(t *testing.T) {
   146  		s := &sdp.SessionDescription{
   147  			Attributes: []sdp.Attribute{{Key: "ice-pwd", Value: "invalidPwd"}},
   148  			MediaDescriptions: []*sdp.MediaDescription{
   149  				{Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}}},
   150  			},
   151  		}
   152  
   153  		_, _, _, err := extractICEDetails(s, nil)
   154  		assert.Equal(t, err, ErrSessionDescriptionConflictingIcePwd)
   155  	})
   156  }
   157  
   158  func TestTrackDetailsFromSDP(t *testing.T) {
   159  	t.Run("Tracks unknown, audio and video with RTX", func(t *testing.T) {
   160  		s := &sdp.SessionDescription{
   161  			MediaDescriptions: []*sdp.MediaDescription{
   162  				{
   163  					MediaName: sdp.MediaName{
   164  						Media: "foobar",
   165  					},
   166  					Attributes: []sdp.Attribute{
   167  						{Key: "mid", Value: "0"},
   168  						{Key: "sendrecv"},
   169  						{Key: "ssrc", Value: "1000 msid:unknown_trk_label unknown_trk_guid"},
   170  					},
   171  				},
   172  				{
   173  					MediaName: sdp.MediaName{
   174  						Media: "audio",
   175  					},
   176  					Attributes: []sdp.Attribute{
   177  						{Key: "mid", Value: "1"},
   178  						{Key: "sendrecv"},
   179  						{Key: "ssrc", Value: "2000 msid:audio_trk_label audio_trk_guid"},
   180  					},
   181  				},
   182  				{
   183  					MediaName: sdp.MediaName{
   184  						Media: "video",
   185  					},
   186  					Attributes: []sdp.Attribute{
   187  						{Key: "mid", Value: "2"},
   188  						{Key: "sendrecv"},
   189  						{Key: "ssrc-group", Value: "FID 3000 4000"},
   190  						{Key: "ssrc", Value: "3000 msid:video_trk_label video_trk_guid"},
   191  						{Key: "ssrc", Value: "4000 msid:rtx_trk_label rtx_trck_guid"},
   192  					},
   193  				},
   194  				{
   195  					MediaName: sdp.MediaName{
   196  						Media: "video",
   197  					},
   198  					Attributes: []sdp.Attribute{
   199  						{Key: "mid", Value: "3"},
   200  						{Key: "sendonly"},
   201  						{Key: "msid", Value: "video_stream_id video_trk_id"},
   202  						{Key: "ssrc", Value: "5000"},
   203  					},
   204  				},
   205  				{
   206  					MediaName: sdp.MediaName{
   207  						Media: "video",
   208  					},
   209  					Attributes: []sdp.Attribute{
   210  						{Key: "sendonly"},
   211  						{Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"},
   212  					},
   213  				},
   214  			},
   215  		}
   216  
   217  		tracks := trackDetailsFromSDP(nil, s)
   218  		assert.Equal(t, 3, len(tracks))
   219  		if trackDetail := trackDetailsForSSRC(tracks, 1000); trackDetail != nil {
   220  			assert.Fail(t, "got the unknown track ssrc:1000 which should have been skipped")
   221  		}
   222  		if track := trackDetailsForSSRC(tracks, 2000); track == nil {
   223  			assert.Fail(t, "missing audio track with ssrc:2000")
   224  		} else {
   225  			assert.Equal(t, RTPCodecTypeAudio, track.kind)
   226  			assert.Equal(t, SSRC(2000), track.ssrcs[0])
   227  			assert.Equal(t, "audio_trk_label", track.streamID)
   228  		}
   229  		if track := trackDetailsForSSRC(tracks, 3000); track == nil {
   230  			assert.Fail(t, "missing video track with ssrc:3000")
   231  		} else {
   232  			assert.Equal(t, RTPCodecTypeVideo, track.kind)
   233  			assert.Equal(t, SSRC(3000), track.ssrcs[0])
   234  			assert.Equal(t, "video_trk_label", track.streamID)
   235  		}
   236  		if track := trackDetailsForSSRC(tracks, 4000); track != nil {
   237  			assert.Fail(t, "got the rtx track ssrc:3000 which should have been skipped")
   238  		}
   239  		if track := trackDetailsForSSRC(tracks, 5000); track == nil {
   240  			assert.Fail(t, "missing video track with ssrc:5000")
   241  		} else {
   242  			assert.Equal(t, RTPCodecTypeVideo, track.kind)
   243  			assert.Equal(t, SSRC(5000), track.ssrcs[0])
   244  			assert.Equal(t, "video_trk_id", track.id)
   245  			assert.Equal(t, "video_stream_id", track.streamID)
   246  		}
   247  	})
   248  
   249  	t.Run("inactive and recvonly tracks ignored", func(t *testing.T) {
   250  		s := &sdp.SessionDescription{
   251  			MediaDescriptions: []*sdp.MediaDescription{
   252  				{
   253  					MediaName: sdp.MediaName{
   254  						Media: "video",
   255  					},
   256  					Attributes: []sdp.Attribute{
   257  						{Key: "inactive"},
   258  						{Key: "ssrc", Value: "6000"},
   259  					},
   260  				},
   261  				{
   262  					MediaName: sdp.MediaName{
   263  						Media: "video",
   264  					},
   265  					Attributes: []sdp.Attribute{
   266  						{Key: "recvonly"},
   267  						{Key: "ssrc", Value: "7000"},
   268  					},
   269  				},
   270  			},
   271  		}
   272  		assert.Equal(t, 0, len(trackDetailsFromSDP(nil, s)))
   273  	})
   274  }
   275  
   276  func TestHaveApplicationMediaSection(t *testing.T) {
   277  	t.Run("Audio only", func(t *testing.T) {
   278  		s := &sdp.SessionDescription{
   279  			MediaDescriptions: []*sdp.MediaDescription{
   280  				{
   281  					MediaName: sdp.MediaName{
   282  						Media: "audio",
   283  					},
   284  					Attributes: []sdp.Attribute{
   285  						{Key: "sendrecv"},
   286  						{Key: "ssrc", Value: "2000"},
   287  					},
   288  				},
   289  			},
   290  		}
   291  
   292  		assert.False(t, haveApplicationMediaSection(s))
   293  	})
   294  
   295  	t.Run("Application", func(t *testing.T) {
   296  		s := &sdp.SessionDescription{
   297  			MediaDescriptions: []*sdp.MediaDescription{
   298  				{
   299  					MediaName: sdp.MediaName{
   300  						Media: mediaSectionApplication,
   301  					},
   302  				},
   303  			},
   304  		}
   305  
   306  		assert.True(t, haveApplicationMediaSection(s))
   307  	})
   308  }
   309  
   310  func TestMediaDescriptionFingerprints(t *testing.T) {
   311  	engine := &MediaEngine{}
   312  	assert.NoError(t, engine.RegisterDefaultCodecs())
   313  
   314  	api := NewAPI(WithMediaEngine(engine))
   315  
   316  	sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   317  	assert.NoError(t, err)
   318  
   319  	certificate, err := GenerateCertificate(sk)
   320  	assert.NoError(t, err)
   321  
   322  	media := []mediaSection{
   323  		{
   324  			id: "video",
   325  			transceivers: []*RTPTransceiver{{
   326  				kind:   RTPCodecTypeVideo,
   327  				api:    api,
   328  				codecs: engine.getCodecsByKind(RTPCodecTypeVideo),
   329  			}},
   330  		},
   331  		{
   332  			id: "audio",
   333  			transceivers: []*RTPTransceiver{{
   334  				kind:   RTPCodecTypeAudio,
   335  				api:    api,
   336  				codecs: engine.getCodecsByKind(RTPCodecTypeAudio),
   337  			}},
   338  		},
   339  		{
   340  			id:   "application",
   341  			data: true,
   342  		},
   343  	}
   344  
   345  	for i := 0; i < 2; i++ {
   346  		media[i].transceivers[0].setSender(&RTPSender{})
   347  		media[i].transceivers[0].setDirection(RTPTransceiverDirectionSendonly)
   348  	}
   349  
   350  	fingerprintTest := func(SDPMediaDescriptionFingerprints bool, expectedFingerprintCount int) func(t *testing.T) {
   351  		return func(t *testing.T) {
   352  			s := &sdp.SessionDescription{}
   353  
   354  			dtlsFingerprints, err := certificate.GetFingerprints()
   355  			assert.NoError(t, err)
   356  
   357  			s, err = populateSDP(s, false,
   358  				dtlsFingerprints,
   359  				SDPMediaDescriptionFingerprints,
   360  				false, true, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew, nil)
   361  			assert.NoError(t, err)
   362  
   363  			sdparray, err := s.Marshal()
   364  			assert.NoError(t, err)
   365  
   366  			assert.Equal(t, strings.Count(string(sdparray), "sha-256"), expectedFingerprintCount)
   367  		}
   368  	}
   369  
   370  	t.Run("Per-Media Description Fingerprints", fingerprintTest(true, 3))
   371  	t.Run("Per-Session Description Fingerprints", fingerprintTest(false, 1))
   372  }
   373  
   374  func TestPopulateSDP(t *testing.T) {
   375  	t.Run("rid", func(t *testing.T) {
   376  		se := SettingEngine{}
   377  
   378  		me := &MediaEngine{}
   379  		assert.NoError(t, me.RegisterDefaultCodecs())
   380  		api := NewAPI(WithMediaEngine(me))
   381  
   382  		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   383  		tr.setDirection(RTPTransceiverDirectionRecvonly)
   384  		ridMap := map[string]*simulcastRid{
   385  			"ridkey": {
   386  				attrValue: "some",
   387  			},
   388  			"ridPaused": {
   389  				attrValue: "some2",
   390  				paused:    true,
   391  			},
   392  		}
   393  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}, ridMap: ridMap}}
   394  
   395  		d := &sdp.SessionDescription{}
   396  
   397  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   398  		assert.Nil(t, err)
   399  
   400  		// Test contains rid map keys
   401  		var ridFound int
   402  		for _, desc := range offerSdp.MediaDescriptions {
   403  			if desc.MediaName.Media != "video" {
   404  				continue
   405  			}
   406  			ridInSDP := getRids(desc)
   407  			if ridKey, ok := ridInSDP["ridkey"]; ok && !ridKey.paused {
   408  				ridFound++
   409  			}
   410  			if ridPaused, ok := ridInSDP["ridPaused"]; ok && ridPaused.paused {
   411  				ridFound++
   412  			}
   413  		}
   414  		assert.Equal(t, 2, ridFound, "All rid keys should be present")
   415  	})
   416  	t.Run("SetCodecPreferences", func(t *testing.T) {
   417  		se := SettingEngine{}
   418  
   419  		me := &MediaEngine{}
   420  		assert.NoError(t, me.RegisterDefaultCodecs())
   421  		api := NewAPI(WithMediaEngine(me))
   422  		me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo)
   423  		me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio)
   424  
   425  		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   426  		tr.setDirection(RTPTransceiverDirectionRecvonly)
   427  		codecErr := tr.SetCodecPreferences([]RTPCodecParameters{
   428  			{
   429  				RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
   430  				PayloadType:        96,
   431  			},
   432  		})
   433  		assert.NoError(t, codecErr)
   434  
   435  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
   436  
   437  		d := &sdp.SessionDescription{}
   438  
   439  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   440  		assert.Nil(t, err)
   441  
   442  		// Test codecs
   443  		foundVP8 := false
   444  		for _, desc := range offerSdp.MediaDescriptions {
   445  			if desc.MediaName.Media != "video" {
   446  				continue
   447  			}
   448  			for _, a := range desc.Attributes {
   449  				if strings.Contains(a.Key, "rtpmap") {
   450  					if a.Value == "98 VP9/90000" {
   451  						t.Fatal("vp9 should not be present in sdp")
   452  					} else if a.Value == "96 VP8/90000" {
   453  						foundVP8 = true
   454  					}
   455  				}
   456  			}
   457  		}
   458  		assert.Equal(t, true, foundVP8, "vp8 should be present in sdp")
   459  	})
   460  	t.Run("ice-lite", func(t *testing.T) {
   461  		se := SettingEngine{}
   462  		se.SetLite(true)
   463  
   464  		offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil)
   465  		assert.Nil(t, err)
   466  
   467  		var found bool
   468  		// ice-lite is an session-level attribute
   469  		for _, a := range offerSdp.Attributes {
   470  			if a.Key == sdp.AttrKeyICELite {
   471  				// ice-lite does not have value (e.g. ":<value>") and it should be an empty string
   472  				if a.Value == "" {
   473  					found = true
   474  					break
   475  				}
   476  			}
   477  		}
   478  		assert.Equal(t, true, found, "ICELite key should be present")
   479  	})
   480  	t.Run("rejected track", func(t *testing.T) {
   481  		se := SettingEngine{}
   482  
   483  		me := &MediaEngine{}
   484  		registerCodecErr := me.RegisterCodec(RTPCodecParameters{
   485  			RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
   486  			PayloadType:        96,
   487  		}, RTPCodecTypeVideo)
   488  		assert.NoError(t, registerCodecErr)
   489  		api := NewAPI(WithMediaEngine(me))
   490  
   491  		videoTransceiver := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   492  		audioTransceiver := &RTPTransceiver{kind: RTPCodecTypeAudio, api: api, codecs: []RTPCodecParameters{}}
   493  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{videoTransceiver}}, {id: "audio", transceivers: []*RTPTransceiver{audioTransceiver}}}
   494  
   495  		d := &sdp.SessionDescription{}
   496  
   497  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   498  		assert.NoError(t, err)
   499  
   500  		// Test codecs
   501  		foundRejectedTrack := false
   502  		for _, desc := range offerSdp.MediaDescriptions {
   503  			if desc.MediaName.Media != "audio" {
   504  				continue
   505  			}
   506  			assert.True(t, desc.ConnectionInformation != nil, "connection information must be provided for rejected tracks")
   507  			assert.Equal(t, desc.MediaName.Formats, []string{"0"}, "rejected tracks have 0 for Formats")
   508  			assert.Equal(t, desc.MediaName.Port, sdp.RangedPort{Value: 0}, "rejected tracks have 0 for Port")
   509  			foundRejectedTrack = true
   510  		}
   511  		assert.Equal(t, true, foundRejectedTrack, "rejected track wasn't present")
   512  	})
   513  	t.Run("allow mixed extmap", func(t *testing.T) {
   514  		se := SettingEngine{}
   515  		offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil)
   516  		assert.Nil(t, err)
   517  
   518  		var found bool
   519  		// session-level attribute
   520  		for _, a := range offerSdp.Attributes {
   521  			if a.Key == sdp.AttrKeyExtMapAllowMixed {
   522  				if a.Value == "" {
   523  					found = true
   524  					break
   525  				}
   526  			}
   527  		}
   528  		assert.Equal(t, true, found, "AllowMixedExtMap key should be present")
   529  
   530  		offerSdp, err = populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, false, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil)
   531  		assert.Nil(t, err)
   532  
   533  		found = false
   534  		// session-level attribute
   535  		for _, a := range offerSdp.Attributes {
   536  			if a.Key == sdp.AttrKeyExtMapAllowMixed {
   537  				if a.Value == "" {
   538  					found = true
   539  					break
   540  				}
   541  			}
   542  		}
   543  		assert.Equal(t, false, found, "AllowMixedExtMap key should not be present")
   544  	})
   545  	t.Run("bundle all", func(t *testing.T) {
   546  		se := SettingEngine{}
   547  
   548  		me := &MediaEngine{}
   549  		assert.NoError(t, me.RegisterDefaultCodecs())
   550  		api := NewAPI(WithMediaEngine(me))
   551  
   552  		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   553  		tr.setDirection(RTPTransceiverDirectionRecvonly)
   554  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
   555  
   556  		d := &sdp.SessionDescription{}
   557  
   558  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   559  		assert.Nil(t, err)
   560  
   561  		bundle, ok := offerSdp.Attribute(sdp.AttrKeyGroup)
   562  		assert.True(t, ok)
   563  		assert.Equal(t, "BUNDLE video", bundle)
   564  	})
   565  	t.Run("bundle matched", func(t *testing.T) {
   566  		se := SettingEngine{}
   567  
   568  		me := &MediaEngine{}
   569  		assert.NoError(t, me.RegisterDefaultCodecs())
   570  		api := NewAPI(WithMediaEngine(me))
   571  
   572  		tra := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   573  		tra.setDirection(RTPTransceiverDirectionRecvonly)
   574  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tra}}}
   575  
   576  		trv := &RTPTransceiver{kind: RTPCodecTypeAudio, api: api, codecs: me.audioCodecs}
   577  		trv.setDirection(RTPTransceiverDirectionRecvonly)
   578  		mediaSections = append(mediaSections, mediaSection{id: "audio", transceivers: []*RTPTransceiver{trv}})
   579  
   580  		d := &sdp.SessionDescription{}
   581  
   582  		matchedBundle := "audio"
   583  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, &matchedBundle)
   584  		assert.Nil(t, err)
   585  
   586  		bundle, ok := offerSdp.Attribute(sdp.AttrKeyGroup)
   587  		assert.True(t, ok)
   588  		assert.Equal(t, "BUNDLE audio", bundle)
   589  
   590  		mediaVideo := offerSdp.MediaDescriptions[0]
   591  		mid, ok := mediaVideo.Attribute(sdp.AttrKeyMID)
   592  		assert.True(t, ok)
   593  		assert.Equal(t, "video", mid)
   594  		assert.True(t, mediaVideo.MediaName.Port.Value == 0)
   595  	})
   596  	t.Run("empty bundle group", func(t *testing.T) {
   597  		se := SettingEngine{}
   598  
   599  		me := &MediaEngine{}
   600  		assert.NoError(t, me.RegisterDefaultCodecs())
   601  		api := NewAPI(WithMediaEngine(me))
   602  
   603  		tra := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   604  		tra.setDirection(RTPTransceiverDirectionRecvonly)
   605  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tra}}}
   606  
   607  		d := &sdp.SessionDescription{}
   608  
   609  		matchedBundle := ""
   610  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, &matchedBundle)
   611  		assert.Nil(t, err)
   612  
   613  		_, ok := offerSdp.Attribute(sdp.AttrKeyGroup)
   614  		assert.False(t, ok)
   615  	})
   616  }
   617  
   618  func TestGetRIDs(t *testing.T) {
   619  	m := []*sdp.MediaDescription{
   620  		{
   621  			MediaName: sdp.MediaName{
   622  				Media: "video",
   623  			},
   624  			Attributes: []sdp.Attribute{
   625  				{Key: "sendonly"},
   626  				{Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"},
   627  			},
   628  		},
   629  	}
   630  
   631  	rids := getRids(m[0])
   632  
   633  	assert.NotEmpty(t, rids, "Rid mapping should be present")
   634  	if _, ok := rids["f"]; !ok {
   635  		assert.Fail(t, "rid values should contain 'f'")
   636  	}
   637  }
   638  
   639  func TestCodecsFromMediaDescription(t *testing.T) {
   640  	t.Run("Codec Only", func(t *testing.T) {
   641  		codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{
   642  			MediaName: sdp.MediaName{
   643  				Media:   "audio",
   644  				Formats: []string{"111"},
   645  			},
   646  			Attributes: []sdp.Attribute{
   647  				{Key: "rtpmap", Value: "111 opus/48000/2"},
   648  			},
   649  		})
   650  
   651  		assert.Equal(t, codecs, []RTPCodecParameters{
   652  			{
   653  				RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "", []RTCPFeedback{}},
   654  				PayloadType:        111,
   655  			},
   656  		})
   657  		assert.NoError(t, err)
   658  	})
   659  
   660  	t.Run("Codec with fmtp/rtcp-fb", func(t *testing.T) {
   661  		codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{
   662  			MediaName: sdp.MediaName{
   663  				Media:   "audio",
   664  				Formats: []string{"111"},
   665  			},
   666  			Attributes: []sdp.Attribute{
   667  				{Key: "rtpmap", Value: "111 opus/48000/2"},
   668  				{Key: "fmtp", Value: "111 minptime=10;useinbandfec=1"},
   669  				{Key: "rtcp-fb", Value: "111 goog-remb"},
   670  				{Key: "rtcp-fb", Value: "111 ccm fir"},
   671  			},
   672  		})
   673  
   674  		assert.Equal(t, codecs, []RTPCodecParameters{
   675  			{
   676  				RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}}},
   677  				PayloadType:        111,
   678  			},
   679  		})
   680  		assert.NoError(t, err)
   681  	})
   682  }
   683  
   684  func TestRtpExtensionsFromMediaDescription(t *testing.T) {
   685  	extensions, err := rtpExtensionsFromMediaDescription(&sdp.MediaDescription{
   686  		MediaName: sdp.MediaName{
   687  			Media:   "audio",
   688  			Formats: []string{"111"},
   689  		},
   690  		Attributes: []sdp.Attribute{
   691  			{Key: "extmap", Value: "1 " + sdp.ABSSendTimeURI},
   692  			{Key: "extmap", Value: "3 " + sdp.SDESMidURI},
   693  		},
   694  	})
   695  
   696  	assert.NoError(t, err)
   697  	assert.Equal(t, extensions[sdp.ABSSendTimeURI], 1)
   698  	assert.Equal(t, extensions[sdp.SDESMidURI], 3)
   699  }