sdp_test.go 21 KB

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