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 }