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