github.com/pion/webrtc/v4@v4.0.1/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/pion/transport/v3/test"
    18  	"github.com/stretchr/testify/assert"
    19  )
    20  
    21  func TestExtractFingerprint(t *testing.T) {
    22  	t.Run("Good Session Fingerprint", func(t *testing.T) {
    23  		s := &sdp.SessionDescription{
    24  			Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo bar"}},
    25  		}
    26  
    27  		fingerprint, hash, err := extractFingerprint(s)
    28  		assert.NoError(t, err)
    29  		assert.Equal(t, fingerprint, "bar")
    30  		assert.Equal(t, hash, "foo")
    31  	})
    32  
    33  	t.Run("Good Media Fingerprint", func(t *testing.T) {
    34  		s := &sdp.SessionDescription{
    35  			MediaDescriptions: []*sdp.MediaDescription{
    36  				{Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo bar"}}},
    37  			},
    38  		}
    39  
    40  		fingerprint, hash, err := extractFingerprint(s)
    41  		assert.NoError(t, err)
    42  		assert.Equal(t, fingerprint, "bar")
    43  		assert.Equal(t, hash, "foo")
    44  	})
    45  
    46  	t.Run("No Fingerprint", func(t *testing.T) {
    47  		s := &sdp.SessionDescription{}
    48  
    49  		_, _, err := extractFingerprint(s)
    50  		assert.Equal(t, ErrSessionDescriptionNoFingerprint, err)
    51  	})
    52  
    53  	t.Run("Invalid Fingerprint", func(t *testing.T) {
    54  		s := &sdp.SessionDescription{
    55  			Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo"}},
    56  		}
    57  
    58  		_, _, err := extractFingerprint(s)
    59  		assert.Equal(t, ErrSessionDescriptionInvalidFingerprint, err)
    60  	})
    61  
    62  	t.Run("Conflicting Fingerprint", func(t *testing.T) {
    63  		s := &sdp.SessionDescription{
    64  			Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo"}},
    65  			MediaDescriptions: []*sdp.MediaDescription{
    66  				{Attributes: []sdp.Attribute{{Key: "fingerprint", Value: "foo blah"}}},
    67  			},
    68  		}
    69  
    70  		_, _, err := extractFingerprint(s)
    71  		assert.Equal(t, ErrSessionDescriptionConflictingFingerprints, err)
    72  	})
    73  }
    74  
    75  func TestExtractICEDetails(t *testing.T) {
    76  	const defaultUfrag = "defaultPwd"
    77  	const defaultPwd = "defaultUfrag"
    78  
    79  	t.Run("Missing ice-pwd", func(t *testing.T) {
    80  		s := &sdp.SessionDescription{
    81  			MediaDescriptions: []*sdp.MediaDescription{
    82  				{Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}}},
    83  			},
    84  		}
    85  
    86  		_, _, _, err := extractICEDetails(s, nil)
    87  		assert.Equal(t, err, ErrSessionDescriptionMissingIcePwd)
    88  	})
    89  
    90  	t.Run("Missing ice-ufrag", func(t *testing.T) {
    91  		s := &sdp.SessionDescription{
    92  			MediaDescriptions: []*sdp.MediaDescription{
    93  				{Attributes: []sdp.Attribute{{Key: "ice-pwd", Value: defaultPwd}}},
    94  			},
    95  		}
    96  
    97  		_, _, _, err := extractICEDetails(s, nil)
    98  		assert.Equal(t, err, ErrSessionDescriptionMissingIceUfrag)
    99  	})
   100  
   101  	t.Run("ice details at session level", func(t *testing.T) {
   102  		s := &sdp.SessionDescription{
   103  			Attributes: []sdp.Attribute{
   104  				{Key: "ice-ufrag", Value: defaultUfrag},
   105  				{Key: "ice-pwd", Value: defaultPwd},
   106  			},
   107  			MediaDescriptions: []*sdp.MediaDescription{},
   108  		}
   109  
   110  		ufrag, pwd, _, err := extractICEDetails(s, nil)
   111  		assert.Equal(t, ufrag, defaultUfrag)
   112  		assert.Equal(t, pwd, defaultPwd)
   113  		assert.NoError(t, err)
   114  	})
   115  
   116  	t.Run("ice details at media level", func(t *testing.T) {
   117  		s := &sdp.SessionDescription{
   118  			MediaDescriptions: []*sdp.MediaDescription{
   119  				{
   120  					Attributes: []sdp.Attribute{
   121  						{Key: "ice-ufrag", Value: defaultUfrag},
   122  						{Key: "ice-pwd", Value: defaultPwd},
   123  					},
   124  				},
   125  			},
   126  		}
   127  
   128  		ufrag, pwd, _, err := extractICEDetails(s, nil)
   129  		assert.Equal(t, ufrag, defaultUfrag)
   130  		assert.Equal(t, pwd, defaultPwd)
   131  		assert.NoError(t, err)
   132  	})
   133  
   134  	t.Run("Conflict ufrag", func(t *testing.T) {
   135  		s := &sdp.SessionDescription{
   136  			Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: "invalidUfrag"}},
   137  			MediaDescriptions: []*sdp.MediaDescription{
   138  				{Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}}},
   139  			},
   140  		}
   141  
   142  		_, _, _, err := extractICEDetails(s, nil)
   143  		assert.Equal(t, err, ErrSessionDescriptionConflictingIceUfrag)
   144  	})
   145  
   146  	t.Run("Conflict pwd", func(t *testing.T) {
   147  		s := &sdp.SessionDescription{
   148  			Attributes: []sdp.Attribute{{Key: "ice-pwd", Value: "invalidPwd"}},
   149  			MediaDescriptions: []*sdp.MediaDescription{
   150  				{Attributes: []sdp.Attribute{{Key: "ice-ufrag", Value: defaultUfrag}, {Key: "ice-pwd", Value: defaultPwd}}},
   151  			},
   152  		}
   153  
   154  		_, _, _, err := extractICEDetails(s, nil)
   155  		assert.Equal(t, err, ErrSessionDescriptionConflictingIcePwd)
   156  	})
   157  }
   158  
   159  func TestTrackDetailsFromSDP(t *testing.T) {
   160  	t.Run("Tracks unknown, audio and video with RTX", func(t *testing.T) {
   161  		s := &sdp.SessionDescription{
   162  			MediaDescriptions: []*sdp.MediaDescription{
   163  				{
   164  					MediaName: sdp.MediaName{
   165  						Media: "foobar",
   166  					},
   167  					Attributes: []sdp.Attribute{
   168  						{Key: "mid", Value: "0"},
   169  						{Key: "sendrecv"},
   170  						{Key: "ssrc", Value: "1000 msid:unknown_trk_label unknown_trk_guid"},
   171  					},
   172  				},
   173  				{
   174  					MediaName: sdp.MediaName{
   175  						Media: "audio",
   176  					},
   177  					Attributes: []sdp.Attribute{
   178  						{Key: "mid", Value: "1"},
   179  						{Key: "sendrecv"},
   180  						{Key: "ssrc", Value: "2000 msid:audio_trk_label audio_trk_guid"},
   181  					},
   182  				},
   183  				{
   184  					MediaName: sdp.MediaName{
   185  						Media: "video",
   186  					},
   187  					Attributes: []sdp.Attribute{
   188  						{Key: "mid", Value: "2"},
   189  						{Key: "sendrecv"},
   190  						{Key: "ssrc-group", Value: "FID 3000 4000"},
   191  						{Key: "ssrc", Value: "3000 msid:video_trk_label video_trk_guid"},
   192  						{Key: "ssrc", Value: "4000 msid:rtx_trk_label rtx_trck_guid"},
   193  					},
   194  				},
   195  				{
   196  					MediaName: sdp.MediaName{
   197  						Media: "video",
   198  					},
   199  					Attributes: []sdp.Attribute{
   200  						{Key: "mid", Value: "3"},
   201  						{Key: "sendonly"},
   202  						{Key: "msid", Value: "video_stream_id video_trk_id"},
   203  						{Key: "ssrc", Value: "5000"},
   204  					},
   205  				},
   206  				{
   207  					MediaName: sdp.MediaName{
   208  						Media: "video",
   209  					},
   210  					Attributes: []sdp.Attribute{
   211  						{Key: "sendonly"},
   212  						{Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"},
   213  					},
   214  				},
   215  			},
   216  		}
   217  
   218  		tracks := trackDetailsFromSDP(nil, s)
   219  		assert.Equal(t, 3, len(tracks))
   220  		if trackDetail := trackDetailsForSSRC(tracks, 1000); trackDetail != nil {
   221  			assert.Fail(t, "got the unknown track ssrc:1000 which should have been skipped")
   222  		}
   223  		if track := trackDetailsForSSRC(tracks, 2000); track == nil {
   224  			assert.Fail(t, "missing audio track with ssrc:2000")
   225  		} else {
   226  			assert.Equal(t, RTPCodecTypeAudio, track.kind)
   227  			assert.Equal(t, SSRC(2000), track.ssrcs[0])
   228  			assert.Equal(t, "audio_trk_label", track.streamID)
   229  		}
   230  		if track := trackDetailsForSSRC(tracks, 3000); track == nil {
   231  			assert.Fail(t, "missing video track with ssrc:3000")
   232  		} else {
   233  			assert.Equal(t, RTPCodecTypeVideo, track.kind)
   234  			assert.Equal(t, SSRC(3000), track.ssrcs[0])
   235  			assert.Equal(t, "video_trk_label", track.streamID)
   236  		}
   237  		if track := trackDetailsForSSRC(tracks, 4000); track != nil {
   238  			assert.Fail(t, "got the rtx track ssrc:3000 which should have been skipped")
   239  		}
   240  		if track := trackDetailsForSSRC(tracks, 5000); track == nil {
   241  			assert.Fail(t, "missing video track with ssrc:5000")
   242  		} else {
   243  			assert.Equal(t, RTPCodecTypeVideo, track.kind)
   244  			assert.Equal(t, SSRC(5000), track.ssrcs[0])
   245  			assert.Equal(t, "video_trk_id", track.id)
   246  			assert.Equal(t, "video_stream_id", track.streamID)
   247  		}
   248  	})
   249  
   250  	t.Run("inactive and recvonly tracks ignored", func(t *testing.T) {
   251  		s := &sdp.SessionDescription{
   252  			MediaDescriptions: []*sdp.MediaDescription{
   253  				{
   254  					MediaName: sdp.MediaName{
   255  						Media: "video",
   256  					},
   257  					Attributes: []sdp.Attribute{
   258  						{Key: "inactive"},
   259  						{Key: "ssrc", Value: "6000"},
   260  					},
   261  				},
   262  				{
   263  					MediaName: sdp.MediaName{
   264  						Media: "video",
   265  					},
   266  					Attributes: []sdp.Attribute{
   267  						{Key: "recvonly"},
   268  						{Key: "ssrc", Value: "7000"},
   269  					},
   270  				},
   271  			},
   272  		}
   273  		assert.Equal(t, 0, len(trackDetailsFromSDP(nil, s)))
   274  	})
   275  
   276  	t.Run("ssrc-group after ssrc", func(t *testing.T) {
   277  		s := &sdp.SessionDescription{
   278  			MediaDescriptions: []*sdp.MediaDescription{
   279  				{
   280  					MediaName: sdp.MediaName{
   281  						Media: "video",
   282  					},
   283  					Attributes: []sdp.Attribute{
   284  						{Key: "mid", Value: "0"},
   285  						{Key: "sendrecv"},
   286  						{Key: "ssrc", Value: "3000 msid:video_trk_label video_trk_guid"},
   287  						{Key: "ssrc", Value: "4000 msid:rtx_trk_label rtx_trck_guid"},
   288  						{Key: "ssrc-group", Value: "FID 3000 4000"},
   289  					},
   290  				},
   291  				{
   292  					MediaName: sdp.MediaName{
   293  						Media: "video",
   294  					},
   295  					Attributes: []sdp.Attribute{
   296  						{Key: "mid", Value: "1"},
   297  						{Key: "sendrecv"},
   298  						{Key: "ssrc-group", Value: "FID 5000 6000"},
   299  						{Key: "ssrc", Value: "5000 msid:video_trk_label video_trk_guid"},
   300  						{Key: "ssrc", Value: "6000 msid:rtx_trk_label rtx_trck_guid"},
   301  					},
   302  				},
   303  			},
   304  		}
   305  
   306  		tracks := trackDetailsFromSDP(nil, s)
   307  		assert.Equal(t, 2, len(tracks))
   308  		assert.Equal(t, SSRC(4000), *tracks[0].repairSsrc)
   309  		assert.Equal(t, SSRC(6000), *tracks[1].repairSsrc)
   310  	})
   311  }
   312  
   313  func TestHaveApplicationMediaSection(t *testing.T) {
   314  	t.Run("Audio only", func(t *testing.T) {
   315  		s := &sdp.SessionDescription{
   316  			MediaDescriptions: []*sdp.MediaDescription{
   317  				{
   318  					MediaName: sdp.MediaName{
   319  						Media: "audio",
   320  					},
   321  					Attributes: []sdp.Attribute{
   322  						{Key: "sendrecv"},
   323  						{Key: "ssrc", Value: "2000"},
   324  					},
   325  				},
   326  			},
   327  		}
   328  
   329  		assert.False(t, haveApplicationMediaSection(s))
   330  	})
   331  
   332  	t.Run("Application", func(t *testing.T) {
   333  		s := &sdp.SessionDescription{
   334  			MediaDescriptions: []*sdp.MediaDescription{
   335  				{
   336  					MediaName: sdp.MediaName{
   337  						Media: mediaSectionApplication,
   338  					},
   339  				},
   340  			},
   341  		}
   342  
   343  		assert.True(t, haveApplicationMediaSection(s))
   344  	})
   345  }
   346  
   347  func TestMediaDescriptionFingerprints(t *testing.T) {
   348  	engine := &MediaEngine{}
   349  	assert.NoError(t, engine.RegisterDefaultCodecs())
   350  
   351  	api := NewAPI(WithMediaEngine(engine))
   352  
   353  	sk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   354  	assert.NoError(t, err)
   355  
   356  	certificate, err := GenerateCertificate(sk)
   357  	assert.NoError(t, err)
   358  
   359  	media := []mediaSection{
   360  		{
   361  			id: "video",
   362  			transceivers: []*RTPTransceiver{{
   363  				kind:   RTPCodecTypeVideo,
   364  				api:    api,
   365  				codecs: engine.getCodecsByKind(RTPCodecTypeVideo),
   366  			}},
   367  		},
   368  		{
   369  			id: "audio",
   370  			transceivers: []*RTPTransceiver{{
   371  				kind:   RTPCodecTypeAudio,
   372  				api:    api,
   373  				codecs: engine.getCodecsByKind(RTPCodecTypeAudio),
   374  			}},
   375  		},
   376  		{
   377  			id:   "application",
   378  			data: true,
   379  		},
   380  	}
   381  
   382  	for i := 0; i < 2; i++ {
   383  		media[i].transceivers[0].setSender(&RTPSender{})
   384  		media[i].transceivers[0].setDirection(RTPTransceiverDirectionSendonly)
   385  	}
   386  
   387  	fingerprintTest := func(SDPMediaDescriptionFingerprints bool, expectedFingerprintCount int) func(t *testing.T) {
   388  		return func(t *testing.T) {
   389  			s := &sdp.SessionDescription{}
   390  
   391  			dtlsFingerprints, err := certificate.GetFingerprints()
   392  			assert.NoError(t, err)
   393  
   394  			s, err = populateSDP(s, false,
   395  				dtlsFingerprints,
   396  				SDPMediaDescriptionFingerprints,
   397  				false, true, engine, sdp.ConnectionRoleActive, []ICECandidate{}, ICEParameters{}, media, ICEGatheringStateNew, nil)
   398  			assert.NoError(t, err)
   399  
   400  			sdparray, err := s.Marshal()
   401  			assert.NoError(t, err)
   402  
   403  			assert.Equal(t, strings.Count(string(sdparray), "sha-256"), expectedFingerprintCount)
   404  		}
   405  	}
   406  
   407  	t.Run("Per-Media Description Fingerprints", fingerprintTest(true, 3))
   408  	t.Run("Per-Session Description Fingerprints", fingerprintTest(false, 1))
   409  }
   410  
   411  func TestPopulateSDP(t *testing.T) {
   412  	t.Run("rid", func(t *testing.T) {
   413  		se := SettingEngine{}
   414  
   415  		me := &MediaEngine{}
   416  		assert.NoError(t, me.RegisterDefaultCodecs())
   417  		api := NewAPI(WithMediaEngine(me))
   418  
   419  		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   420  		tr.setDirection(RTPTransceiverDirectionRecvonly)
   421  		rids := []*simulcastRid{
   422  			{
   423  				id:        "ridkey",
   424  				attrValue: "some",
   425  			},
   426  			{
   427  				id:        "ridPaused",
   428  				attrValue: "some2",
   429  				paused:    true,
   430  			},
   431  		}
   432  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}, rids: rids}}
   433  
   434  		d := &sdp.SessionDescription{}
   435  
   436  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   437  		assert.Nil(t, err)
   438  
   439  		// Test contains rid map keys
   440  		var ridFound int
   441  		for _, desc := range offerSdp.MediaDescriptions {
   442  			if desc.MediaName.Media != "video" {
   443  				continue
   444  			}
   445  			ridsInSDP := getRids(desc)
   446  			for _, rid := range ridsInSDP {
   447  				if rid.id == "ridkey" && !rid.paused {
   448  					ridFound++
   449  				}
   450  				if rid.id == "ridPaused" && rid.paused {
   451  					ridFound++
   452  				}
   453  			}
   454  		}
   455  		assert.Equal(t, 2, ridFound, "All rid keys should be present")
   456  	})
   457  	t.Run("SetCodecPreferences", func(t *testing.T) {
   458  		se := SettingEngine{}
   459  
   460  		me := &MediaEngine{}
   461  		assert.NoError(t, me.RegisterDefaultCodecs())
   462  		api := NewAPI(WithMediaEngine(me))
   463  		me.pushCodecs(me.videoCodecs, RTPCodecTypeVideo)
   464  		me.pushCodecs(me.audioCodecs, RTPCodecTypeAudio)
   465  
   466  		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   467  		tr.setDirection(RTPTransceiverDirectionRecvonly)
   468  		codecErr := tr.SetCodecPreferences([]RTPCodecParameters{
   469  			{
   470  				RTPCodecCapability: RTPCodecCapability{MimeTypeVP8, 90000, 0, "", nil},
   471  				PayloadType:        96,
   472  			},
   473  		})
   474  		assert.NoError(t, codecErr)
   475  
   476  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
   477  
   478  		d := &sdp.SessionDescription{}
   479  
   480  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   481  		assert.Nil(t, err)
   482  
   483  		// Test codecs
   484  		foundVP8 := false
   485  		for _, desc := range offerSdp.MediaDescriptions {
   486  			if desc.MediaName.Media != "video" {
   487  				continue
   488  			}
   489  			for _, a := range desc.Attributes {
   490  				if strings.Contains(a.Key, "rtpmap") {
   491  					if a.Value == "98 VP9/90000" {
   492  						t.Fatal("vp9 should not be present in sdp")
   493  					} else if a.Value == "96 VP8/90000" {
   494  						foundVP8 = true
   495  					}
   496  				}
   497  			}
   498  		}
   499  		assert.Equal(t, true, foundVP8, "vp8 should be present in sdp")
   500  	})
   501  	t.Run("ice-lite", func(t *testing.T) {
   502  		se := SettingEngine{}
   503  		se.SetLite(true)
   504  
   505  		offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil)
   506  		assert.Nil(t, err)
   507  
   508  		var found bool
   509  		// ice-lite is an session-level attribute
   510  		for _, a := range offerSdp.Attributes {
   511  			if a.Key == sdp.AttrKeyICELite {
   512  				// ice-lite does not have value (e.g. ":<value>") and it should be an empty string
   513  				if a.Value == "" {
   514  					found = true
   515  					break
   516  				}
   517  			}
   518  		}
   519  		assert.Equal(t, true, found, "ICELite key should be present")
   520  	})
   521  	t.Run("rejected track", func(t *testing.T) {
   522  		se := SettingEngine{}
   523  
   524  		me := &MediaEngine{}
   525  		registerCodecErr := me.RegisterCodec(RTPCodecParameters{
   526  			RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
   527  			PayloadType:        96,
   528  		}, RTPCodecTypeVideo)
   529  		assert.NoError(t, registerCodecErr)
   530  		api := NewAPI(WithMediaEngine(me))
   531  
   532  		videoTransceiver := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   533  		audioTransceiver := &RTPTransceiver{kind: RTPCodecTypeAudio, api: api, codecs: []RTPCodecParameters{}}
   534  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{videoTransceiver}}, {id: "audio", transceivers: []*RTPTransceiver{audioTransceiver}}}
   535  
   536  		d := &sdp.SessionDescription{}
   537  
   538  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   539  		assert.NoError(t, err)
   540  
   541  		// Test codecs
   542  		foundRejectedTrack := false
   543  		for _, desc := range offerSdp.MediaDescriptions {
   544  			if desc.MediaName.Media != "audio" {
   545  				continue
   546  			}
   547  			assert.True(t, desc.ConnectionInformation != nil, "connection information must be provided for rejected tracks")
   548  			assert.Equal(t, desc.MediaName.Formats, []string{"0"}, "rejected tracks have 0 for Formats")
   549  			assert.Equal(t, desc.MediaName.Port, sdp.RangedPort{Value: 0}, "rejected tracks have 0 for Port")
   550  			foundRejectedTrack = true
   551  		}
   552  		assert.Equal(t, true, foundRejectedTrack, "rejected track wasn't present")
   553  	})
   554  	t.Run("allow mixed extmap", func(t *testing.T) {
   555  		se := SettingEngine{}
   556  		offerSdp, err := populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil)
   557  		assert.Nil(t, err)
   558  
   559  		var found bool
   560  		// session-level attribute
   561  		for _, a := range offerSdp.Attributes {
   562  			if a.Key == sdp.AttrKeyExtMapAllowMixed {
   563  				if a.Value == "" {
   564  					found = true
   565  					break
   566  				}
   567  			}
   568  		}
   569  		assert.Equal(t, true, found, "AllowMixedExtMap key should be present")
   570  
   571  		offerSdp, err = populateSDP(&sdp.SessionDescription{}, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, false, &MediaEngine{}, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, []mediaSection{}, ICEGatheringStateComplete, nil)
   572  		assert.Nil(t, err)
   573  
   574  		found = false
   575  		// session-level attribute
   576  		for _, a := range offerSdp.Attributes {
   577  			if a.Key == sdp.AttrKeyExtMapAllowMixed {
   578  				if a.Value == "" {
   579  					found = true
   580  					break
   581  				}
   582  			}
   583  		}
   584  		assert.Equal(t, false, found, "AllowMixedExtMap key should not be present")
   585  	})
   586  	t.Run("bundle all", func(t *testing.T) {
   587  		se := SettingEngine{}
   588  
   589  		me := &MediaEngine{}
   590  		assert.NoError(t, me.RegisterDefaultCodecs())
   591  		api := NewAPI(WithMediaEngine(me))
   592  
   593  		tr := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   594  		tr.setDirection(RTPTransceiverDirectionRecvonly)
   595  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tr}}}
   596  
   597  		d := &sdp.SessionDescription{}
   598  
   599  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, nil)
   600  		assert.Nil(t, err)
   601  
   602  		bundle, ok := offerSdp.Attribute(sdp.AttrKeyGroup)
   603  		assert.True(t, ok)
   604  		assert.Equal(t, "BUNDLE video", bundle)
   605  	})
   606  	t.Run("bundle matched", func(t *testing.T) {
   607  		se := SettingEngine{}
   608  
   609  		me := &MediaEngine{}
   610  		assert.NoError(t, me.RegisterDefaultCodecs())
   611  		api := NewAPI(WithMediaEngine(me))
   612  
   613  		tra := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   614  		tra.setDirection(RTPTransceiverDirectionRecvonly)
   615  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tra}}}
   616  
   617  		trv := &RTPTransceiver{kind: RTPCodecTypeAudio, api: api, codecs: me.audioCodecs}
   618  		trv.setDirection(RTPTransceiverDirectionRecvonly)
   619  		mediaSections = append(mediaSections, mediaSection{id: "audio", transceivers: []*RTPTransceiver{trv}})
   620  
   621  		d := &sdp.SessionDescription{}
   622  
   623  		matchedBundle := "audio"
   624  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, &matchedBundle)
   625  		assert.Nil(t, err)
   626  
   627  		bundle, ok := offerSdp.Attribute(sdp.AttrKeyGroup)
   628  		assert.True(t, ok)
   629  		assert.Equal(t, "BUNDLE audio", bundle)
   630  
   631  		mediaVideo := offerSdp.MediaDescriptions[0]
   632  		mid, ok := mediaVideo.Attribute(sdp.AttrKeyMID)
   633  		assert.True(t, ok)
   634  		assert.Equal(t, "video", mid)
   635  		assert.True(t, mediaVideo.MediaName.Port.Value == 0)
   636  	})
   637  	t.Run("empty bundle group", func(t *testing.T) {
   638  		se := SettingEngine{}
   639  
   640  		me := &MediaEngine{}
   641  		assert.NoError(t, me.RegisterDefaultCodecs())
   642  		api := NewAPI(WithMediaEngine(me))
   643  
   644  		tra := &RTPTransceiver{kind: RTPCodecTypeVideo, api: api, codecs: me.videoCodecs}
   645  		tra.setDirection(RTPTransceiverDirectionRecvonly)
   646  		mediaSections := []mediaSection{{id: "video", transceivers: []*RTPTransceiver{tra}}}
   647  
   648  		d := &sdp.SessionDescription{}
   649  
   650  		matchedBundle := ""
   651  		offerSdp, err := populateSDP(d, false, []DTLSFingerprint{}, se.sdpMediaLevelFingerprints, se.candidates.ICELite, true, me, connectionRoleFromDtlsRole(defaultDtlsRoleOffer), []ICECandidate{}, ICEParameters{}, mediaSections, ICEGatheringStateComplete, &matchedBundle)
   652  		assert.Nil(t, err)
   653  
   654  		_, ok := offerSdp.Attribute(sdp.AttrKeyGroup)
   655  		assert.False(t, ok)
   656  	})
   657  }
   658  
   659  func TestGetRIDs(t *testing.T) {
   660  	m := []*sdp.MediaDescription{
   661  		{
   662  			MediaName: sdp.MediaName{
   663  				Media: "video",
   664  			},
   665  			Attributes: []sdp.Attribute{
   666  				{Key: "sendonly"},
   667  				{Key: sdpAttributeRid, Value: "f send pt=97;max-width=1280;max-height=720"},
   668  			},
   669  		},
   670  	}
   671  
   672  	rids := getRids(m[0])
   673  
   674  	assert.NotEmpty(t, rids, "Rid mapping should be present")
   675  	found := false
   676  	for _, rid := range rids {
   677  		if rid.id == "f" {
   678  			found = true
   679  			break
   680  		}
   681  	}
   682  	if !found {
   683  		assert.Fail(t, "rid values should contain 'f'")
   684  	}
   685  }
   686  
   687  func TestCodecsFromMediaDescription(t *testing.T) {
   688  	t.Run("Codec Only", func(t *testing.T) {
   689  		codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{
   690  			MediaName: sdp.MediaName{
   691  				Media:   "audio",
   692  				Formats: []string{"111"},
   693  			},
   694  			Attributes: []sdp.Attribute{
   695  				{Key: "rtpmap", Value: "111 opus/48000/2"},
   696  			},
   697  		})
   698  
   699  		assert.Equal(t, codecs, []RTPCodecParameters{
   700  			{
   701  				RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "", []RTCPFeedback{}},
   702  				PayloadType:        111,
   703  			},
   704  		})
   705  		assert.NoError(t, err)
   706  	})
   707  
   708  	t.Run("Codec with fmtp/rtcp-fb", func(t *testing.T) {
   709  		codecs, err := codecsFromMediaDescription(&sdp.MediaDescription{
   710  			MediaName: sdp.MediaName{
   711  				Media:   "audio",
   712  				Formats: []string{"111"},
   713  			},
   714  			Attributes: []sdp.Attribute{
   715  				{Key: "rtpmap", Value: "111 opus/48000/2"},
   716  				{Key: "fmtp", Value: "111 minptime=10;useinbandfec=1"},
   717  				{Key: "rtcp-fb", Value: "111 goog-remb"},
   718  				{Key: "rtcp-fb", Value: "111 ccm fir"},
   719  				{Key: "rtcp-fb", Value: "* ccm fir"},
   720  				{Key: "rtcp-fb", Value: "* nack"},
   721  			},
   722  		})
   723  
   724  		assert.Equal(t, codecs, []RTPCodecParameters{
   725  			{
   726  				RTPCodecCapability: RTPCodecCapability{MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", []RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}}},
   727  				PayloadType:        111,
   728  			},
   729  		})
   730  		assert.NoError(t, err)
   731  	})
   732  }
   733  
   734  func TestRtpExtensionsFromMediaDescription(t *testing.T) {
   735  	extensions, err := rtpExtensionsFromMediaDescription(&sdp.MediaDescription{
   736  		MediaName: sdp.MediaName{
   737  			Media:   "audio",
   738  			Formats: []string{"111"},
   739  		},
   740  		Attributes: []sdp.Attribute{
   741  			{Key: "extmap", Value: "1 " + sdp.ABSSendTimeURI},
   742  			{Key: "extmap", Value: "3 " + sdp.SDESMidURI},
   743  		},
   744  	})
   745  
   746  	assert.NoError(t, err)
   747  	assert.Equal(t, extensions[sdp.ABSSendTimeURI], 1)
   748  	assert.Equal(t, extensions[sdp.SDESMidURI], 3)
   749  }
   750  
   751  // Assert that FEC and RTX SSRCes are present if they are enabled in the MediaEngine
   752  func Test_SSRC_Groups(t *testing.T) {
   753  	const offerWithRTX = `v=0
   754  o=- 930222930247584370 1727933945 IN IP4 0.0.0.0
   755  s=-
   756  t=0 0
   757  a=msid-semantic:WMS*
   758  a=fingerprint:sha-256 11:3F:1C:8D:D4:1D:8D:E7:E1:3E:AF:38:06:0D:1D:40:22:DC:FE:C9:93:E4:80:D8:0B:17:9F:2E:C1:CA:C8:3D
   759  a=extmap-allow-mixed
   760  a=group:BUNDLE 0 1
   761  m=audio 9 UDP/TLS/RTP/SAVPF 101
   762  c=IN IP4 0.0.0.0
   763  a=setup:actpass
   764  a=mid:0
   765  a=ice-ufrag:yIgpPUMarFReduuM
   766  a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
   767  a=rtcp-mux
   768  a=rtcp-rsize
   769  a=rtpmap:101 opus/90000
   770  a=rtcp-fb:101 transport-cc
   771  a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
   772  a=ssrc:3566446228 cname:stream-id
   773  a=ssrc:3566446228 msid:stream-id audio-id
   774  a=ssrc:3566446228 mslabel:stream-id
   775  a=ssrc:3566446228 label:audio-id
   776  a=msid:stream-id audio-id
   777  a=sendrecv
   778  m=video 9 UDP/TLS/RTP/SAVPF 96 97
   779  c=IN IP4 0.0.0.0
   780  a=setup:actpass
   781  a=mid:1
   782  a=ice-ufrag:yIgpPUMarFReduuM
   783  a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
   784  a=rtpmap:96 VP8/90000
   785  a=rtcp-fb:96 nack
   786  a=rtcp-fb:96 nack pli
   787  a=rtcp-fb:96 transport-cc
   788  a=rtpmap:97 rtx/90000
   789  a=fmtp:97 apt=96
   790  a=ssrc-group:FID 1701050765 2578535262
   791  a=ssrc:1701050765 cname:stream-id
   792  a=ssrc:1701050765 msid:stream-id track-id
   793  a=ssrc:1701050765 mslabel:stream-id
   794  a=ssrc:1701050765 label:track-id
   795  a=msid:stream-id track-id
   796  a=sendrecv
   797  `
   798  
   799  	const offerNoRTX = `v=0
   800  o=- 930222930247584370 1727933945 IN IP4 0.0.0.0
   801  s=-
   802  t=0 0
   803  a=msid-semantic:WMS*
   804  a=fingerprint:sha-256 11:3F:1C:8D:D4:1D:8D:E7:E1:3E:AF:38:06:0D:1D:40:22:DC:FE:C9:93:E4:80:D8:0B:17:9F:2E:C1:CA:C8:3D
   805  a=extmap-allow-mixed
   806  a=group:BUNDLE 0 1
   807  m=audio 9 UDP/TLS/RTP/SAVPF 101
   808  a=mid:0
   809  a=ice-ufrag:yIgpPUMarFReduuM
   810  a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
   811  a=rtcp-mux
   812  a=rtcp-rsize
   813  a=rtpmap:101 opus/90000
   814  a=rtcp-fb:101 transport-cc
   815  a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
   816  a=ssrc:3566446228 cname:stream-id
   817  a=ssrc:3566446228 msid:stream-id audio-id
   818  a=ssrc:3566446228 mslabel:stream-id
   819  a=ssrc:3566446228 label:audio-id
   820  a=msid:stream-id audio-id
   821  a=sendrecv
   822  m=video 9 UDP/TLS/RTP/SAVPF 96
   823  c=IN IP4 0.0.0.0
   824  a=setup:actpass
   825  a=mid:1
   826  a=ice-ufrag:yIgpPUMarFReduuM
   827  a=ice-pwd:VmnVaqCByWiOTatFoDBbMGhSFGlsxviz
   828  a=rtpmap:96 VP8/90000
   829  a=rtcp-fb:96 nack
   830  a=rtcp-fb:96 nack pli
   831  a=rtcp-fb:96 transport-cc
   832  a=ssrc-group:FID 1701050765 2578535262
   833  a=ssrc:1701050765 cname:stream-id
   834  a=ssrc:1701050765 msid:stream-id track-id
   835  a=ssrc:1701050765 mslabel:stream-id
   836  a=ssrc:1701050765 label:track-id
   837  a=msid:stream-id track-id
   838  a=sendrecv
   839  `
   840  	defer test.CheckRoutines(t)()
   841  
   842  	for _, testCase := range []struct {
   843  		name                   string
   844  		enableRTXInMediaEngine bool
   845  		rtxExpected            bool
   846  		remoteOffer            string
   847  	}{
   848  		{"Offer", true, true, ""},
   849  		{"Offer no Local Groups", false, false, ""},
   850  		{"Answer", true, true, offerWithRTX},
   851  		{"Answer No Local Groups", false, false, offerWithRTX},
   852  		{"Answer No Remote Groups", true, false, offerNoRTX},
   853  	} {
   854  		t.Run(testCase.name, func(t *testing.T) {
   855  			checkRTXSupport := func(s *sdp.SessionDescription) {
   856  				// RTX is never enabled for audio
   857  				assert.Nil(t, trackDetailsFromSDP(nil, s)[0].repairSsrc)
   858  
   859  				// RTX is conditionally enabled for video
   860  				if testCase.rtxExpected {
   861  					assert.NotNil(t, trackDetailsFromSDP(nil, s)[1].repairSsrc)
   862  				} else {
   863  					assert.Nil(t, trackDetailsFromSDP(nil, s)[1].repairSsrc)
   864  				}
   865  			}
   866  
   867  			m := &MediaEngine{}
   868  			assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
   869  				RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeOpus, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
   870  				PayloadType:        101,
   871  			}, RTPCodecTypeAudio))
   872  			assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
   873  				RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8, ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil},
   874  				PayloadType:        96,
   875  			}, RTPCodecTypeVideo))
   876  			if testCase.enableRTXInMediaEngine {
   877  				assert.NoError(t, m.RegisterCodec(RTPCodecParameters{
   878  					RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeRTX, ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=96", RTCPFeedback: nil},
   879  					PayloadType:        97,
   880  				}, RTPCodecTypeVideo))
   881  			}
   882  
   883  			peerConnection, err := NewAPI(WithMediaEngine(m)).NewPeerConnection(Configuration{})
   884  			assert.NoError(t, err)
   885  
   886  			audioTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeOpus}, "audio-id", "stream-id")
   887  			assert.NoError(t, err)
   888  
   889  			_, err = peerConnection.AddTrack(audioTrack)
   890  			assert.NoError(t, err)
   891  
   892  			videoTrack, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video-id", "stream-id")
   893  			assert.NoError(t, err)
   894  
   895  			_, err = peerConnection.AddTrack(videoTrack)
   896  			assert.NoError(t, err)
   897  
   898  			if testCase.remoteOffer == "" {
   899  				offer, err := peerConnection.CreateOffer(nil)
   900  				assert.NoError(t, err)
   901  				checkRTXSupport(offer.parsed)
   902  			} else {
   903  				assert.NoError(t, peerConnection.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: testCase.remoteOffer}))
   904  				answer, err := peerConnection.CreateAnswer(nil)
   905  				assert.NoError(t, err)
   906  				checkRTXSupport(answer.parsed)
   907  			}
   908  
   909  			assert.NoError(t, peerConnection.Close())
   910  		})
   911  	}
   912  }