github.com/pion/webrtc/v3@v3.2.24/sdp.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 "errors" 11 "fmt" 12 "net/url" 13 "regexp" 14 "strconv" 15 "strings" 16 "sync/atomic" 17 18 "github.com/pion/ice/v2" 19 "github.com/pion/logging" 20 "github.com/pion/sdp/v3" 21 ) 22 23 // trackDetails represents any media source that can be represented in a SDP 24 // This isn't keyed by SSRC because it also needs to support rid based sources 25 type trackDetails struct { 26 mid string 27 kind RTPCodecType 28 streamID string 29 id string 30 ssrcs []SSRC 31 repairSsrc *SSRC 32 rids []string 33 } 34 35 func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails { 36 for i := range trackDetails { 37 for j := range trackDetails[i].ssrcs { 38 if trackDetails[i].ssrcs[j] == ssrc { 39 return &trackDetails[i] 40 } 41 } 42 } 43 return nil 44 } 45 46 func trackDetailsForRID(trackDetails []trackDetails, mid, rid string) *trackDetails { 47 for i := range trackDetails { 48 if trackDetails[i].mid != mid { 49 continue 50 } 51 52 for j := range trackDetails[i].rids { 53 if trackDetails[i].rids[j] == rid { 54 return &trackDetails[i] 55 } 56 } 57 } 58 return nil 59 } 60 61 func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc SSRC) []trackDetails { 62 filtered := []trackDetails{} 63 doesTrackHaveSSRC := func(t trackDetails) bool { 64 for i := range t.ssrcs { 65 if t.ssrcs[i] == ssrc { 66 return true 67 } 68 } 69 70 return false 71 } 72 73 for i := range incomingTracks { 74 if !doesTrackHaveSSRC(incomingTracks[i]) { 75 filtered = append(filtered, incomingTracks[i]) 76 } 77 } 78 79 return filtered 80 } 81 82 // extract all trackDetails from an SDP. 83 func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) (incomingTracks []trackDetails) { // nolint:gocognit 84 for _, media := range s.MediaDescriptions { 85 tracksInMediaSection := []trackDetails{} 86 rtxRepairFlows := map[uint64]uint64{} 87 88 // Plan B can have multiple tracks in a signle media section 89 streamID := "" 90 trackID := "" 91 92 // If media section is recvonly or inactive skip 93 if _, ok := media.Attribute(sdp.AttrKeyRecvOnly); ok { 94 continue 95 } else if _, ok := media.Attribute(sdp.AttrKeyInactive); ok { 96 continue 97 } 98 99 midValue := getMidValue(media) 100 if midValue == "" { 101 continue 102 } 103 104 codecType := NewRTPCodecType(media.MediaName.Media) 105 if codecType == 0 { 106 continue 107 } 108 109 for _, attr := range media.Attributes { 110 switch attr.Key { 111 case sdp.AttrKeySSRCGroup: 112 split := strings.Split(attr.Value, " ") 113 if split[0] == sdp.SemanticTokenFlowIdentification { 114 // Add rtx ssrcs to blacklist, to avoid adding them as tracks 115 // Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section 116 // as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first 117 // (2231627014) as specified in RFC5576 118 if len(split) == 3 { 119 baseSsrc, err := strconv.ParseUint(split[1], 10, 32) 120 if err != nil { 121 log.Warnf("Failed to parse SSRC: %v", err) 122 continue 123 } 124 rtxRepairFlow, err := strconv.ParseUint(split[2], 10, 32) 125 if err != nil { 126 log.Warnf("Failed to parse SSRC: %v", err) 127 continue 128 } 129 rtxRepairFlows[rtxRepairFlow] = baseSsrc 130 tracksInMediaSection = filterTrackWithSSRC(tracksInMediaSection, SSRC(rtxRepairFlow)) // Remove if rtx was added as track before 131 } 132 } 133 134 // Handle `a=msid:<stream_id> <track_label>` for Unified plan. The first value is the same as MediaStream.id 135 // in the browser and can be used to figure out which tracks belong to the same stream. The browser should 136 // figure this out automatically when an ontrack event is emitted on RTCPeerConnection. 137 case sdp.AttrKeyMsid: 138 split := strings.Split(attr.Value, " ") 139 if len(split) == 2 { 140 streamID = split[0] 141 trackID = split[1] 142 } 143 144 case sdp.AttrKeySSRC: 145 split := strings.Split(attr.Value, " ") 146 ssrc, err := strconv.ParseUint(split[0], 10, 32) 147 if err != nil { 148 log.Warnf("Failed to parse SSRC: %v", err) 149 continue 150 } 151 152 if _, ok := rtxRepairFlows[ssrc]; ok { 153 continue // This ssrc is a RTX repair flow, ignore 154 } 155 156 if len(split) == 3 && strings.HasPrefix(split[1], "msid:") { 157 streamID = split[1][len("msid:"):] 158 trackID = split[2] 159 } 160 161 isNewTrack := true 162 trackDetails := &trackDetails{} 163 for i := range tracksInMediaSection { 164 for j := range tracksInMediaSection[i].ssrcs { 165 if tracksInMediaSection[i].ssrcs[j] == SSRC(ssrc) { 166 trackDetails = &tracksInMediaSection[i] 167 isNewTrack = false 168 } 169 } 170 } 171 172 trackDetails.mid = midValue 173 trackDetails.kind = codecType 174 trackDetails.streamID = streamID 175 trackDetails.id = trackID 176 trackDetails.ssrcs = []SSRC{SSRC(ssrc)} 177 178 for r, baseSsrc := range rtxRepairFlows { 179 if baseSsrc == ssrc { 180 repairSsrc := SSRC(r) 181 trackDetails.repairSsrc = &repairSsrc 182 } 183 } 184 185 if isNewTrack { 186 tracksInMediaSection = append(tracksInMediaSection, *trackDetails) 187 } 188 } 189 } 190 191 if rids := getRids(media); len(rids) != 0 && trackID != "" && streamID != "" { 192 simulcastTrack := trackDetails{ 193 mid: midValue, 194 kind: codecType, 195 streamID: streamID, 196 id: trackID, 197 rids: []string{}, 198 } 199 for rid := range rids { 200 simulcastTrack.rids = append(simulcastTrack.rids, rid) 201 } 202 203 tracksInMediaSection = []trackDetails{simulcastTrack} 204 } 205 206 incomingTracks = append(incomingTracks, tracksInMediaSection...) 207 } 208 209 return incomingTracks 210 } 211 212 func trackDetailsToRTPReceiveParameters(t *trackDetails) RTPReceiveParameters { 213 encodingSize := len(t.ssrcs) 214 if len(t.rids) >= encodingSize { 215 encodingSize = len(t.rids) 216 } 217 218 encodings := make([]RTPDecodingParameters, encodingSize) 219 for i := range encodings { 220 if len(t.rids) > i { 221 encodings[i].RID = t.rids[i] 222 } 223 if len(t.ssrcs) > i { 224 encodings[i].SSRC = t.ssrcs[i] 225 } 226 227 if t.repairSsrc != nil { 228 encodings[i].RTX.SSRC = *t.repairSsrc 229 } 230 } 231 232 return RTPReceiveParameters{Encodings: encodings} 233 } 234 235 func getRids(media *sdp.MediaDescription) map[string]*simulcastRid { 236 rids := map[string]*simulcastRid{} 237 var simulcastAttr string 238 for _, attr := range media.Attributes { 239 if attr.Key == sdpAttributeRid { 240 split := strings.Split(attr.Value, " ") 241 rids[split[0]] = &simulcastRid{attrValue: attr.Value} 242 } else if attr.Key == sdpAttributeSimulcast { 243 simulcastAttr = attr.Value 244 } 245 } 246 // process paused stream like "a=simulcast:send 1;~2;~3" 247 if simulcastAttr != "" { 248 if space := strings.Index(simulcastAttr, " "); space > 0 { 249 simulcastAttr = simulcastAttr[space+1:] 250 } 251 ridStates := strings.Split(simulcastAttr, ";") 252 for _, ridState := range ridStates { 253 if ridState[:1] == "~" { 254 rid := ridState[1:] 255 if r, ok := rids[rid]; ok { 256 r.paused = true 257 } 258 } 259 } 260 } 261 return rids 262 } 263 264 func addCandidatesToMediaDescriptions(candidates []ICECandidate, m *sdp.MediaDescription, iceGatheringState ICEGatheringState) error { 265 appendCandidateIfNew := func(c ice.Candidate, attributes []sdp.Attribute) { 266 marshaled := c.Marshal() 267 for _, a := range attributes { 268 if marshaled == a.Value { 269 return 270 } 271 } 272 273 m.WithValueAttribute("candidate", marshaled) 274 } 275 276 for _, c := range candidates { 277 candidate, err := c.toICE() 278 if err != nil { 279 return err 280 } 281 282 candidate.SetComponent(1) 283 appendCandidateIfNew(candidate, m.Attributes) 284 285 candidate.SetComponent(2) 286 appendCandidateIfNew(candidate, m.Attributes) 287 } 288 289 if iceGatheringState != ICEGatheringStateComplete { 290 return nil 291 } 292 for _, a := range m.Attributes { 293 if a.Key == "end-of-candidates" { 294 return nil 295 } 296 } 297 298 m.WithPropertyAttribute("end-of-candidates") 299 return nil 300 } 301 302 func addDataMediaSection(d *sdp.SessionDescription, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState) error { 303 media := (&sdp.MediaDescription{ 304 MediaName: sdp.MediaName{ 305 Media: mediaSectionApplication, 306 Port: sdp.RangedPort{Value: 9}, 307 Protos: []string{"UDP", "DTLS", "SCTP"}, 308 Formats: []string{"webrtc-datachannel"}, 309 }, 310 ConnectionInformation: &sdp.ConnectionInformation{ 311 NetworkType: "IN", 312 AddressType: "IP4", 313 Address: &sdp.Address{ 314 Address: "0.0.0.0", 315 }, 316 }, 317 }). 318 WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). 319 WithValueAttribute(sdp.AttrKeyMID, midValue). 320 WithPropertyAttribute(RTPTransceiverDirectionSendrecv.String()). 321 WithPropertyAttribute("sctp-port:5000"). 322 WithICECredentials(iceParams.UsernameFragment, iceParams.Password) 323 324 for _, f := range dtlsFingerprints { 325 media = media.WithFingerprint(f.Algorithm, strings.ToUpper(f.Value)) 326 } 327 328 if shouldAddCandidates { 329 if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil { 330 return err 331 } 332 } 333 334 d.WithMedia(media) 335 return nil 336 } 337 338 func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGatherer, iceGatheringState ICEGatheringState) *SessionDescription { 339 if sessionDescription == nil || i == nil { 340 return sessionDescription 341 } 342 343 candidates, err := i.GetLocalCandidates() 344 if err != nil { 345 return sessionDescription 346 } 347 348 parsed := sessionDescription.parsed 349 if len(parsed.MediaDescriptions) > 0 { 350 m := parsed.MediaDescriptions[0] 351 if err = addCandidatesToMediaDescriptions(candidates, m, iceGatheringState); err != nil { 352 return sessionDescription 353 } 354 } 355 356 sdp, err := parsed.Marshal() 357 if err != nil { 358 return sessionDescription 359 } 360 361 return &SessionDescription{ 362 SDP: string(sdp), 363 Type: sessionDescription.Type, 364 parsed: parsed, 365 } 366 } 367 368 func addSenderSDP( 369 mediaSection mediaSection, 370 isPlanB bool, 371 media *sdp.MediaDescription, 372 ) { 373 for _, mt := range mediaSection.transceivers { 374 sender := mt.Sender() 375 if sender == nil { 376 continue 377 } 378 379 track := sender.Track() 380 if track == nil { 381 continue 382 } 383 384 sendParameters := sender.GetParameters() 385 for _, encoding := range sendParameters.Encodings { 386 media = media.WithMediaSource(uint32(encoding.SSRC), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID()) 387 if !isPlanB { 388 media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID()) 389 } 390 } 391 392 if len(sendParameters.Encodings) > 1 { 393 sendRids := make([]string, 0, len(sendParameters.Encodings)) 394 395 for _, encoding := range sendParameters.Encodings { 396 media.WithValueAttribute(sdpAttributeRid, encoding.RID+" send") 397 sendRids = append(sendRids, encoding.RID) 398 } 399 // Simulcast 400 media.WithValueAttribute(sdpAttributeSimulcast, "send "+strings.Join(sendRids, ";")) 401 } 402 403 if !isPlanB { 404 break 405 } 406 } 407 } 408 409 func addTransceiverSDP( 410 d *sdp.SessionDescription, 411 isPlanB bool, 412 shouldAddCandidates bool, 413 dtlsFingerprints []DTLSFingerprint, 414 mediaEngine *MediaEngine, 415 midValue string, 416 iceParams ICEParameters, 417 candidates []ICECandidate, 418 dtlsRole sdp.ConnectionRole, 419 iceGatheringState ICEGatheringState, 420 mediaSection mediaSection, 421 ) (bool, error) { 422 transceivers := mediaSection.transceivers 423 if len(transceivers) < 1 { 424 return false, errSDPZeroTransceivers 425 } 426 // Use the first transceiver to generate the section attributes 427 t := transceivers[0] 428 media := sdp.NewJSEPMediaDescription(t.kind.String(), []string{}). 429 WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()). 430 WithValueAttribute(sdp.AttrKeyMID, midValue). 431 WithICECredentials(iceParams.UsernameFragment, iceParams.Password). 432 WithPropertyAttribute(sdp.AttrKeyRTCPMux). 433 WithPropertyAttribute(sdp.AttrKeyRTCPRsize) 434 435 codecs := t.getCodecs() 436 for _, codec := range codecs { 437 name := strings.TrimPrefix(codec.MimeType, "audio/") 438 name = strings.TrimPrefix(name, "video/") 439 media.WithCodec(uint8(codec.PayloadType), name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine) 440 441 for _, feedback := range codec.RTPCodecCapability.RTCPFeedback { 442 media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter)) 443 } 444 } 445 if len(codecs) == 0 { 446 // If we are sender and we have no codecs throw an error early 447 if t.Sender() != nil { 448 return false, ErrSenderWithNoCodecs 449 } 450 451 // Explicitly reject track if we don't have the codec 452 // We need to include connection information even if we're rejecting a track, otherwise Firefox will fail to 453 // parse the SDP with an error like: 454 // SIPCC Failed to parse SDP: SDP Parse Error on line 50: c= connection line not specified for every media level, validation failed. 455 // In addition this makes our SDP compliant with RFC 4566 Section 5.7: https://datatracker.ietf.org/doc/html/rfc4566#section-5.7 456 d.WithMedia(&sdp.MediaDescription{ 457 MediaName: sdp.MediaName{ 458 Media: t.kind.String(), 459 Port: sdp.RangedPort{Value: 0}, 460 Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, 461 Formats: []string{"0"}, 462 }, 463 ConnectionInformation: &sdp.ConnectionInformation{ 464 NetworkType: "IN", 465 AddressType: "IP4", 466 Address: &sdp.Address{ 467 Address: "0.0.0.0", 468 }, 469 }, 470 }) 471 return false, nil 472 } 473 474 directions := []RTPTransceiverDirection{} 475 if t.Sender() != nil { 476 directions = append(directions, RTPTransceiverDirectionSendonly) 477 } 478 if t.Receiver() != nil { 479 directions = append(directions, RTPTransceiverDirectionRecvonly) 480 } 481 482 parameters := mediaEngine.getRTPParametersByKind(t.kind, directions) 483 for _, rtpExtension := range parameters.HeaderExtensions { 484 extURL, err := url.Parse(rtpExtension.URI) 485 if err != nil { 486 return false, err 487 } 488 media.WithExtMap(sdp.ExtMap{Value: rtpExtension.ID, URI: extURL}) 489 } 490 491 if len(mediaSection.ridMap) > 0 { 492 recvRids := make([]string, 0, len(mediaSection.ridMap)) 493 494 for rid := range mediaSection.ridMap { 495 media.WithValueAttribute(sdpAttributeRid, rid+" recv") 496 if mediaSection.ridMap[rid].paused { 497 rid = "~" + rid 498 } 499 recvRids = append(recvRids, rid) 500 } 501 // Simulcast 502 media.WithValueAttribute(sdpAttributeSimulcast, "recv "+strings.Join(recvRids, ";")) 503 } 504 505 addSenderSDP(mediaSection, isPlanB, media) 506 507 media = media.WithPropertyAttribute(t.Direction().String()) 508 509 for _, fingerprint := range dtlsFingerprints { 510 media = media.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value)) 511 } 512 513 if shouldAddCandidates { 514 if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil { 515 return false, err 516 } 517 } 518 519 d.WithMedia(media) 520 521 return true, nil 522 } 523 524 type simulcastRid struct { 525 attrValue string 526 paused bool 527 } 528 529 type mediaSection struct { 530 id string 531 transceivers []*RTPTransceiver 532 data bool 533 ridMap map[string]*simulcastRid 534 } 535 536 func bundleMatchFromRemote(matchBundleGroup *string) func(mid string) bool { 537 if matchBundleGroup == nil { 538 return func(midValue string) bool { 539 return true 540 } 541 } 542 bundleTags := strings.Split(*matchBundleGroup, " ") 543 return func(midValue string) bool { 544 for _, tag := range bundleTags { 545 if tag == midValue { 546 return true 547 } 548 } 549 return false 550 } 551 } 552 553 // populateSDP serializes a PeerConnections state into an SDP 554 func populateSDP( 555 d *sdp.SessionDescription, 556 isPlanB bool, 557 dtlsFingerprints []DTLSFingerprint, 558 mediaDescriptionFingerprint bool, 559 isICELite bool, 560 isExtmapAllowMixed bool, 561 mediaEngine *MediaEngine, 562 connectionRole sdp.ConnectionRole, 563 candidates []ICECandidate, 564 iceParams ICEParameters, 565 mediaSections []mediaSection, 566 iceGatheringState ICEGatheringState, 567 matchBundleGroup *string, 568 ) (*sdp.SessionDescription, error) { 569 var err error 570 mediaDtlsFingerprints := []DTLSFingerprint{} 571 572 if mediaDescriptionFingerprint { 573 mediaDtlsFingerprints = dtlsFingerprints 574 } 575 576 bundleValue := "BUNDLE" 577 bundleCount := 0 578 579 bundleMatch := bundleMatchFromRemote(matchBundleGroup) 580 appendBundle := func(midValue string) { 581 bundleValue += " " + midValue 582 bundleCount++ 583 } 584 585 for i, m := range mediaSections { 586 if m.data && len(m.transceivers) != 0 { 587 return nil, errSDPMediaSectionMediaDataChanInvalid 588 } else if !isPlanB && len(m.transceivers) > 1 { 589 return nil, errSDPMediaSectionMultipleTrackInvalid 590 } 591 592 shouldAddID := true 593 shouldAddCandidates := i == 0 594 if m.data { 595 if err = addDataMediaSection(d, shouldAddCandidates, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState); err != nil { 596 return nil, err 597 } 598 } else { 599 shouldAddID, err = addTransceiverSDP(d, isPlanB, shouldAddCandidates, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, m) 600 if err != nil { 601 return nil, err 602 } 603 } 604 605 if shouldAddID { 606 if bundleMatch(m.id) { 607 appendBundle(m.id) 608 } else { 609 d.MediaDescriptions[len(d.MediaDescriptions)-1].MediaName.Port = sdp.RangedPort{Value: 0} 610 } 611 } 612 } 613 614 if !mediaDescriptionFingerprint { 615 for _, fingerprint := range dtlsFingerprints { 616 d.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value)) 617 } 618 } 619 620 if isICELite { 621 // RFC 5245 S15.3 622 d = d.WithValueAttribute(sdp.AttrKeyICELite, "") 623 } 624 625 if isExtmapAllowMixed { 626 d = d.WithPropertyAttribute(sdp.AttrKeyExtMapAllowMixed) 627 } 628 629 if bundleCount > 0 { 630 d = d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue) 631 } 632 return d, nil 633 } 634 635 func getMidValue(media *sdp.MediaDescription) string { 636 for _, attr := range media.Attributes { 637 if attr.Key == "mid" { 638 return attr.Value 639 } 640 } 641 return "" 642 } 643 644 // SessionDescription contains a MediaSection with Multiple SSRCs, it is Plan-B 645 func descriptionIsPlanB(desc *SessionDescription, log logging.LeveledLogger) bool { 646 if desc == nil || desc.parsed == nil { 647 return false 648 } 649 650 // Store all MIDs that already contain a track 651 midWithTrack := map[string]bool{} 652 653 for _, trackDetail := range trackDetailsFromSDP(log, desc.parsed) { 654 if _, ok := midWithTrack[trackDetail.mid]; ok { 655 return true 656 } 657 midWithTrack[trackDetail.mid] = true 658 } 659 660 return false 661 } 662 663 // SessionDescription contains a MediaSection with name `audio`, `video` or `data` 664 // If only one SSRC is set we can't know if it is Plan-B or Unified. If users have 665 // set fallback mode assume it is Plan-B 666 func descriptionPossiblyPlanB(desc *SessionDescription) bool { 667 if desc == nil || desc.parsed == nil { 668 return false 669 } 670 671 detectionRegex := regexp.MustCompile(`(?i)^(audio|video|data)$`) 672 for _, media := range desc.parsed.MediaDescriptions { 673 if len(detectionRegex.FindStringSubmatch(getMidValue(media))) == 2 { 674 return true 675 } 676 } 677 return false 678 } 679 680 func getPeerDirection(media *sdp.MediaDescription) RTPTransceiverDirection { 681 for _, a := range media.Attributes { 682 if direction := NewRTPTransceiverDirection(a.Key); direction != RTPTransceiverDirection(Unknown) { 683 return direction 684 } 685 } 686 return RTPTransceiverDirection(Unknown) 687 } 688 689 func extractFingerprint(desc *sdp.SessionDescription) (string, string, error) { 690 fingerprints := []string{} 691 692 if fingerprint, haveFingerprint := desc.Attribute("fingerprint"); haveFingerprint { 693 fingerprints = append(fingerprints, fingerprint) 694 } 695 696 for _, m := range desc.MediaDescriptions { 697 if fingerprint, haveFingerprint := m.Attribute("fingerprint"); haveFingerprint { 698 fingerprints = append(fingerprints, fingerprint) 699 } 700 } 701 702 if len(fingerprints) < 1 { 703 return "", "", ErrSessionDescriptionNoFingerprint 704 } 705 706 for _, m := range fingerprints { 707 if m != fingerprints[0] { 708 return "", "", ErrSessionDescriptionConflictingFingerprints 709 } 710 } 711 712 parts := strings.Split(fingerprints[0], " ") 713 if len(parts) != 2 { 714 return "", "", ErrSessionDescriptionInvalidFingerprint 715 } 716 return parts[1], parts[0], nil 717 } 718 719 func extractICEDetails(desc *sdp.SessionDescription, log logging.LeveledLogger) (string, string, []ICECandidate, error) { // nolint:gocognit 720 candidates := []ICECandidate{} 721 remotePwds := []string{} 722 remoteUfrags := []string{} 723 724 if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag { 725 remoteUfrags = append(remoteUfrags, ufrag) 726 } 727 if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd { 728 remotePwds = append(remotePwds, pwd) 729 } 730 731 for _, m := range desc.MediaDescriptions { 732 if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag { 733 remoteUfrags = append(remoteUfrags, ufrag) 734 } 735 if pwd, havePwd := m.Attribute("ice-pwd"); havePwd { 736 remotePwds = append(remotePwds, pwd) 737 } 738 739 for _, a := range m.Attributes { 740 if a.IsICECandidate() { 741 c, err := ice.UnmarshalCandidate(a.Value) 742 if err != nil { 743 if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) { 744 log.Warnf("Discarding remote candidate: %s", err) 745 continue 746 } 747 return "", "", nil, err 748 } 749 750 candidate, err := newICECandidateFromICE(c) 751 if err != nil { 752 return "", "", nil, err 753 } 754 755 candidates = append(candidates, candidate) 756 } 757 } 758 } 759 760 if len(remoteUfrags) == 0 { 761 return "", "", nil, ErrSessionDescriptionMissingIceUfrag 762 } else if len(remotePwds) == 0 { 763 return "", "", nil, ErrSessionDescriptionMissingIcePwd 764 } 765 766 for _, m := range remoteUfrags { 767 if m != remoteUfrags[0] { 768 return "", "", nil, ErrSessionDescriptionConflictingIceUfrag 769 } 770 } 771 772 for _, m := range remotePwds { 773 if m != remotePwds[0] { 774 return "", "", nil, ErrSessionDescriptionConflictingIcePwd 775 } 776 } 777 778 return remoteUfrags[0], remotePwds[0], candidates, nil 779 } 780 781 func haveApplicationMediaSection(desc *sdp.SessionDescription) bool { 782 for _, m := range desc.MediaDescriptions { 783 if m.MediaName.Media == mediaSectionApplication { 784 return true 785 } 786 } 787 788 return false 789 } 790 791 func getByMid(searchMid string, desc *SessionDescription) *sdp.MediaDescription { 792 for _, m := range desc.parsed.MediaDescriptions { 793 if mid, ok := m.Attribute(sdp.AttrKeyMID); ok && mid == searchMid { 794 return m 795 } 796 } 797 return nil 798 } 799 800 // haveDataChannel return MediaDescription with MediaName equal application 801 func haveDataChannel(desc *SessionDescription) *sdp.MediaDescription { 802 for _, d := range desc.parsed.MediaDescriptions { 803 if d.MediaName.Media == mediaSectionApplication { 804 return d 805 } 806 } 807 return nil 808 } 809 810 func codecsFromMediaDescription(m *sdp.MediaDescription) (out []RTPCodecParameters, err error) { 811 s := &sdp.SessionDescription{ 812 MediaDescriptions: []*sdp.MediaDescription{m}, 813 } 814 815 for _, payloadStr := range m.MediaName.Formats { 816 payloadType, err := strconv.ParseUint(payloadStr, 10, 8) 817 if err != nil { 818 return nil, err 819 } 820 821 codec, err := s.GetCodecForPayloadType(uint8(payloadType)) 822 if err != nil { 823 if payloadType == 0 { 824 continue 825 } 826 return nil, err 827 } 828 829 channels := uint16(0) 830 val, err := strconv.ParseUint(codec.EncodingParameters, 10, 16) 831 if err == nil { 832 channels = uint16(val) 833 } 834 835 feedback := []RTCPFeedback{} 836 for _, raw := range codec.RTCPFeedback { 837 split := strings.Split(raw, " ") 838 entry := RTCPFeedback{Type: split[0]} 839 if len(split) == 2 { 840 entry.Parameter = split[1] 841 } 842 843 feedback = append(feedback, entry) 844 } 845 846 out = append(out, RTPCodecParameters{ 847 RTPCodecCapability: RTPCodecCapability{m.MediaName.Media + "/" + codec.Name, codec.ClockRate, channels, codec.Fmtp, feedback}, 848 PayloadType: PayloadType(payloadType), 849 }) 850 } 851 852 return out, nil 853 } 854 855 func rtpExtensionsFromMediaDescription(m *sdp.MediaDescription) (map[string]int, error) { 856 out := map[string]int{} 857 858 for _, a := range m.Attributes { 859 if a.Key == sdp.AttrKeyExtMap { 860 e := sdp.ExtMap{} 861 if err := e.Unmarshal(a.String()); err != nil { 862 return nil, err 863 } 864 865 out[e.URI.String()] = e.Value 866 } 867 } 868 869 return out, nil 870 } 871 872 // updateSDPOrigin saves sdp.Origin in PeerConnection when creating 1st local SDP; 873 // for subsequent calling, it updates Origin for SessionDescription from saved one 874 // and increments session version by one. 875 // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-25#section-5.2.2 876 func updateSDPOrigin(origin *sdp.Origin, d *sdp.SessionDescription) { 877 if atomic.CompareAndSwapUint64(&origin.SessionVersion, 0, d.Origin.SessionVersion) { // store 878 atomic.StoreUint64(&origin.SessionID, d.Origin.SessionID) 879 } else { // load 880 for { // awaiting for saving session id 881 d.Origin.SessionID = atomic.LoadUint64(&origin.SessionID) 882 if d.Origin.SessionID != 0 { 883 break 884 } 885 } 886 d.Origin.SessionVersion = atomic.AddUint64(&origin.SessionVersion, 1) 887 } 888 } 889 890 func isIceLiteSet(desc *sdp.SessionDescription) bool { 891 for _, a := range desc.Attributes { 892 if strings.TrimSpace(a.Key) == sdp.AttrKeyICELite { 893 return true 894 } 895 } 896 897 return false 898 } 899 900 func isExtMapAllowMixedSet(desc *sdp.SessionDescription) bool { 901 for _, a := range desc.Attributes { 902 if strings.TrimSpace(a.Key) == sdp.AttrKeyExtMapAllowMixed { 903 return true 904 } 905 } 906 907 return false 908 }