github.com/livekit/protocol@v1.39.3/sdp/sdp.go (about) 1 // Copyright 2023 LiveKit, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package sdp 16 17 import ( 18 "errors" 19 "fmt" 20 "strconv" 21 "strings" 22 23 "github.com/pion/sdp/v3" 24 "github.com/pion/webrtc/v4" 25 ) 26 27 func GetMidValue(media *sdp.MediaDescription) string { 28 for _, attr := range media.Attributes { 29 if attr.Key == sdp.AttrKeyMID { 30 return attr.Value 31 } 32 } 33 return "" 34 } 35 36 func ExtractFingerprint(desc *sdp.SessionDescription) (string, string, error) { 37 fingerprints := make([]string, 0) 38 39 if fingerprint, haveFingerprint := desc.Attribute("fingerprint"); haveFingerprint { 40 fingerprints = append(fingerprints, fingerprint) 41 } 42 43 for _, m := range desc.MediaDescriptions { 44 if fingerprint, haveFingerprint := m.Attribute("fingerprint"); haveFingerprint { 45 fingerprints = append(fingerprints, fingerprint) 46 } 47 } 48 49 if len(fingerprints) < 1 { 50 return "", "", webrtc.ErrSessionDescriptionNoFingerprint 51 } 52 53 for _, m := range fingerprints { 54 if m != fingerprints[0] { 55 return "", "", webrtc.ErrSessionDescriptionConflictingFingerprints 56 } 57 } 58 59 parts := strings.Split(fingerprints[0], " ") 60 if len(parts) != 2 { 61 return "", "", webrtc.ErrSessionDescriptionInvalidFingerprint 62 } 63 return parts[1], parts[0], nil 64 } 65 66 func ExtractDTLSRole(desc *sdp.SessionDescription) webrtc.DTLSRole { 67 for _, md := range desc.MediaDescriptions { 68 setup, ok := md.Attribute(sdp.AttrKeyConnectionSetup) 69 if !ok { 70 continue 71 } 72 73 if setup == sdp.ConnectionRoleActive.String() { 74 return webrtc.DTLSRoleClient 75 } 76 77 if setup == sdp.ConnectionRolePassive.String() { 78 return webrtc.DTLSRoleServer 79 } 80 } 81 82 // 83 // If 'setup' attribute is not available, use client role 84 // as that is the default behaviour of answerers 85 // 86 // There seems to be some differences in how role is decided. 87 // libwebrtc (Chrome) code - (https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/pc/jsep_transport.cc;l=592;drc=369fb686729e7eb20d2bd09717cec14269a399d7) 88 // does not mention anything about ICE role when determining 89 // DTLS Role. 90 // 91 // But, ORTC has this - https://github.com/w3c/ortc/issues/167#issuecomment-69409953 92 // and pion/webrtc follows that (https://github.com/pion/webrtc/blob/e071a4eded1efd5d9b401bcfc4efacb3a2a5a53c/dtlstransport.go#L269) 93 // 94 // So if remote is ice-lite, pion will use DTLSRoleServer when answering 95 // while browsers pick DTLSRoleClient. 96 // 97 return webrtc.DTLSRoleClient 98 } 99 100 func ExtractICECredential(desc *sdp.SessionDescription) (string, string, error) { 101 pwds := []string{} 102 ufrags := []string{} 103 104 if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag { 105 ufrags = append(ufrags, ufrag) 106 } 107 if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd { 108 pwds = append(pwds, pwd) 109 } 110 111 for _, m := range desc.MediaDescriptions { 112 if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag { 113 ufrags = append(ufrags, ufrag) 114 } 115 if pwd, havePwd := m.Attribute("ice-pwd"); havePwd { 116 pwds = append(pwds, pwd) 117 } 118 } 119 120 if len(ufrags) == 0 { 121 return "", "", webrtc.ErrSessionDescriptionMissingIceUfrag 122 } else if len(pwds) == 0 { 123 return "", "", webrtc.ErrSessionDescriptionMissingIcePwd 124 } 125 126 for _, m := range ufrags { 127 if m != ufrags[0] { 128 return "", "", webrtc.ErrSessionDescriptionConflictingIceUfrag 129 } 130 } 131 132 for _, m := range pwds { 133 if m != pwds[0] { 134 return "", "", webrtc.ErrSessionDescriptionConflictingIcePwd 135 } 136 } 137 138 return ufrags[0], pwds[0], nil 139 } 140 141 func ExtractStreamID(media *sdp.MediaDescription) (string, bool) { 142 var streamID string 143 msid, ok := media.Attribute(sdp.AttrKeyMsid) 144 if !ok { 145 return "", false 146 } 147 ids := strings.Split(msid, " ") 148 if len(ids) < 2 { 149 streamID = msid 150 } else { 151 streamID = ids[1] 152 } 153 return streamID, true 154 } 155 156 func GetMediaStreamTrack(m *sdp.MediaDescription) string { 157 mst := "" 158 msid, ok := m.Attribute(sdp.AttrKeyMsid) 159 if ok { 160 if parts := strings.Split(msid, " "); len(parts) == 2 { 161 mst = parts[1] 162 } 163 } 164 165 if mst == "" { 166 attr, ok := m.Attribute(sdp.AttrKeySSRC) 167 if ok { 168 parts := strings.Split(attr, " ") 169 if len(parts) == 3 && strings.HasPrefix(strings.ToLower(parts[1]), "msid:") { 170 mst = parts[2] 171 } 172 } 173 } 174 return mst 175 } 176 177 func GetSimulcastRids(m *sdp.MediaDescription) ([]string, bool) { 178 val, ok := m.Attribute("simulcast") 179 if !ok { 180 return nil, false 181 } 182 183 parts := strings.Split(val, " ") 184 if len(parts) != 2 || parts[0] != "send" { 185 return nil, false 186 } 187 188 return strings.Split(parts[1], ";"), true 189 } 190 191 func CodecsFromMediaDescription(m *sdp.MediaDescription) (out []sdp.Codec, err error) { 192 s := &sdp.SessionDescription{ 193 MediaDescriptions: []*sdp.MediaDescription{m}, 194 } 195 196 for _, payloadStr := range m.MediaName.Formats { 197 payloadType, err := strconv.ParseUint(payloadStr, 10, 8) 198 if err != nil { 199 return nil, err 200 } 201 202 codec, err := s.GetCodecForPayloadType(uint8(payloadType)) 203 if err != nil { 204 if payloadType == 0 { 205 continue 206 } 207 return nil, err 208 } 209 210 out = append(out, codec) 211 } 212 213 return out, nil 214 } 215 216 func GetBundleMid(parsed *sdp.SessionDescription) (string, bool) { 217 if groupAttribute, found := parsed.Attribute(sdp.AttrKeyGroup); found { 218 bundleIDs := strings.Split(groupAttribute, " ") 219 if len(bundleIDs) > 1 && strings.EqualFold(bundleIDs[0], "BUNDLE") { 220 return bundleIDs[1], true 221 } 222 } 223 224 return "", false 225 } 226 227 type sdpFragmentICE struct { 228 ufrag string 229 pwd string 230 lite *bool 231 options string 232 } 233 234 func (i *sdpFragmentICE) Unmarshal(attributes []sdp.Attribute) error { 235 getAttr := func(key string) (string, bool) { 236 for _, a := range attributes { 237 if a.Key == key { 238 return a.Value, true 239 } 240 } 241 242 return "", false 243 } 244 245 iceUfrag, found := getAttr("ice-ufrag") 246 if found { 247 i.ufrag = iceUfrag 248 } 249 250 icePwd, found := getAttr("ice-pwd") 251 if found { 252 i.pwd = icePwd 253 } 254 255 _, found = getAttr(sdp.AttrKeyICELite) 256 if found { 257 lite := true 258 i.lite = &lite 259 } 260 261 iceOptions, found := getAttr("ice-options") 262 if found { 263 i.options = iceOptions 264 } 265 266 return nil 267 } 268 269 func (i *sdpFragmentICE) Marshal() (string, error) { 270 iceFragment := []byte{} 271 addKeyValue := func(key string, value string) { 272 iceFragment = append(iceFragment, key...) 273 if value != "" { 274 iceFragment = append(iceFragment, value...) 275 } 276 iceFragment = append(iceFragment, "\r\n"...) 277 } 278 279 if i.ufrag != "" { 280 addKeyValue("a=ice-ufrag:", i.ufrag) 281 } 282 if i.pwd != "" { 283 addKeyValue("a=ice-pwd:", i.pwd) 284 } 285 if i.lite != nil && *i.lite { 286 addKeyValue("a=ice-lite", "") 287 } 288 if i.options != "" { 289 addKeyValue("a=ice-options:", i.options) 290 } 291 292 return string(iceFragment), nil 293 } 294 295 type sdpFragmentMedia struct { 296 info string 297 mid string 298 ice *sdpFragmentICE 299 candidates []string 300 endOfCandidates *bool 301 } 302 303 func (m *sdpFragmentMedia) Unmarshal(md *sdp.MediaDescription) error { 304 // MediaName conversion to string taken from github.com/pion/sdp 305 var info []byte 306 appendList := func(list []string, sep byte) { 307 for i, p := range list { 308 if i != 0 && i != len(list) { 309 info = append(info, sep) 310 } 311 info = append(info, p...) 312 } 313 } 314 315 info = append(append(info, md.MediaName.Media...), ' ') 316 317 info = strconv.AppendInt(info, int64(md.MediaName.Port.Value), 10) 318 if md.MediaName.Port.Range != nil { 319 info = append(info, '/') 320 info = strconv.AppendInt(info, int64(*md.MediaName.Port.Range), 10) 321 } 322 info = append(info, ' ') 323 324 appendList(md.MediaName.Protos, '/') 325 info = append(info, ' ') 326 appendList(md.MediaName.Formats, ' ') 327 m.info = string(info) 328 329 mid, found := md.Attribute(sdp.AttrKeyMID) 330 if found { 331 m.mid = mid 332 } 333 334 m.ice = &sdpFragmentICE{} 335 if err := m.ice.Unmarshal(md.Attributes); err != nil { 336 return err 337 } 338 339 for _, a := range md.Attributes { 340 if a.IsICECandidate() { 341 m.candidates = append(m.candidates, a.Value) 342 } 343 } 344 345 _, found = md.Attribute(sdp.AttrKeyEndOfCandidates) 346 if found { 347 endOfCandidates := true 348 m.endOfCandidates = &endOfCandidates 349 } 350 return nil 351 } 352 353 func (m *sdpFragmentMedia) Marshal() (string, error) { 354 mediaFragment := []byte{} 355 addKeyValue := func(key string, value string) { 356 mediaFragment = append(mediaFragment, key...) 357 if value != "" { 358 mediaFragment = append(mediaFragment, value...) 359 } 360 mediaFragment = append(mediaFragment, "\r\n"...) 361 } 362 363 if m.info != "" { 364 addKeyValue("m=", m.info) 365 } 366 367 if m.mid != "" { 368 addKeyValue("a=mid:", m.mid) 369 } 370 371 if m.ice != nil { 372 iceFragment, err := m.ice.Marshal() 373 if err != nil { 374 return "", err 375 } 376 mediaFragment = append(mediaFragment, iceFragment...) 377 } 378 379 for _, c := range m.candidates { 380 addKeyValue("a=candidate:", c) 381 } 382 if m.endOfCandidates != nil && *m.endOfCandidates { 383 addKeyValue("a=end-of-candidates", "") 384 } 385 386 return string(mediaFragment), nil 387 } 388 389 type SDPFragment struct { 390 group string 391 ice *sdpFragmentICE 392 media *sdpFragmentMedia 393 } 394 395 // primarily for use with WHIP Trickle ICE - https://www.rfc-editor.org/rfc/rfc9725.html#name-trickle-ice 396 func (s *SDPFragment) Unmarshal(frag string) error { 397 s.ice = &sdpFragmentICE{} 398 399 lines := strings.Split(frag, "\n") 400 for _, line := range lines { 401 line = strings.TrimRight(line, " \r") 402 if len(line) == 0 { 403 continue 404 } 405 406 if line[0] == 'm' { 407 if s.media != nil { 408 return errors.New("too many media sections") 409 } 410 411 s.media = &sdpFragmentMedia{} 412 s.media.ice = &sdpFragmentICE{} 413 s.media.info = line[2:] 414 } 415 416 if line[0] != 'a' { 417 // not an attribute, skip 418 continue 419 } 420 421 if line[1] != '=' { 422 return errors.New("invalid attribute") 423 } 424 425 line = line[2:] 426 delimIndex := strings.Index(line, ":") 427 if delimIndex < 0 { 428 if line == sdp.AttrKeyICELite { 429 lite := true 430 if s.media != nil { 431 s.media.ice.lite = &lite 432 } else { 433 s.ice.lite = &lite 434 } 435 } 436 continue 437 } 438 439 value := line[delimIndex+1:] 440 switch line[:delimIndex] { 441 case sdp.AttrKeyGroup: 442 s.group = value 443 444 case "ice-ufrag": 445 if s.media != nil { 446 s.media.ice.ufrag = value 447 } else { 448 s.ice.ufrag = value 449 } 450 451 case "ice-pwd": 452 if s.media != nil { 453 s.media.ice.pwd = value 454 } else { 455 s.ice.pwd = value 456 } 457 458 case "ice-options": 459 if s.media != nil { 460 s.media.ice.options = value 461 } else { 462 s.ice.options = value 463 } 464 465 case sdp.AttrKeyMID: 466 if s.media != nil { 467 s.media.mid = value 468 } 469 470 case sdp.AttrKeyCandidate: 471 if s.media != nil { 472 s.media.candidates = append(s.media.candidates, value) 473 } 474 475 case sdp.AttrKeyEndOfCandidates: 476 endOfCandidates := true 477 if s.media != nil { 478 s.media.endOfCandidates = &endOfCandidates 479 } 480 } 481 } 482 483 if s.media == nil { 484 return errors.New("missing media section") 485 } 486 487 if s.group != "" { 488 bundleIDs := strings.Split(s.group, " ") 489 if len(bundleIDs) > 1 && strings.EqualFold(bundleIDs[0], "BUNDLE") { 490 if s.media.mid != bundleIDs[1] { 491 return fmt.Errorf("bundle media mismatch, expected: %s, got: %s", bundleIDs[1], s.media.mid) 492 } 493 } 494 } 495 496 return nil 497 } 498 499 // primarily for use with WHIP ICE Restart - https://www.rfc-editor.org/rfc/rfc9725.html#name-ice-restarts 500 func (s *SDPFragment) Marshal() (string, error) { 501 sdpFragment := []byte{} 502 addKeyValue := func(key string, value string) { 503 sdpFragment = append(sdpFragment, key...) 504 if value != "" { 505 sdpFragment = append(sdpFragment, value...) 506 } 507 sdpFragment = append(sdpFragment, "\r\n"...) 508 } 509 510 if s.group != "" { 511 addKeyValue("a=group:", s.group) 512 } 513 514 if s.ice != nil { 515 iceFragment, err := s.ice.Marshal() 516 if err != nil { 517 return "", err 518 } 519 sdpFragment = append(sdpFragment, iceFragment...) 520 } 521 522 if s.media != nil { 523 mediaFragment, err := s.media.Marshal() 524 if err != nil { 525 return "", err 526 } 527 sdpFragment = append(sdpFragment, mediaFragment...) 528 } 529 530 return string(sdpFragment), nil 531 } 532 533 func (s *SDPFragment) Mid() string { 534 if s.media != nil { 535 return s.media.mid 536 } 537 538 return "" 539 } 540 541 func (s *SDPFragment) Candidates() []string { 542 if s.media != nil { 543 return s.media.candidates 544 } 545 546 return nil 547 } 548 549 func (s *SDPFragment) ExtractICECredential() (string, string, error) { 550 pwds := []string{} 551 ufrags := []string{} 552 553 if s.ice != nil { 554 if s.ice.ufrag != "" { 555 ufrags = append(ufrags, s.ice.ufrag) 556 } 557 if s.ice.pwd != "" { 558 pwds = append(pwds, s.ice.pwd) 559 } 560 } 561 562 if s.media != nil { 563 if s.media.ice.ufrag != "" { 564 ufrags = append(ufrags, s.media.ice.ufrag) 565 } 566 if s.media.ice.pwd != "" { 567 pwds = append(pwds, s.media.ice.pwd) 568 } 569 } 570 571 if len(ufrags) == 0 { 572 return "", "", webrtc.ErrSessionDescriptionMissingIceUfrag 573 } else if len(pwds) == 0 { 574 return "", "", webrtc.ErrSessionDescriptionMissingIcePwd 575 } 576 577 for _, m := range ufrags { 578 if m != ufrags[0] { 579 return "", "", webrtc.ErrSessionDescriptionConflictingIceUfrag 580 } 581 } 582 583 for _, m := range pwds { 584 if m != pwds[0] { 585 return "", "", webrtc.ErrSessionDescriptionConflictingIcePwd 586 } 587 } 588 589 return ufrags[0], pwds[0], nil 590 } 591 592 // primarily for use with WHIP ICE Restart - https://www.rfc-editor.org/rfc/rfc9725.html#name-ice-restarts 593 func (s *SDPFragment) PatchICECredentialAndCandidatesIntoSDP(parsed *sdp.SessionDescription) error { 594 // ice-options and ice-lite should match 595 if s.ice != nil && (s.ice.lite != nil || s.ice.options != "") { 596 for _, a := range parsed.Attributes { 597 switch a.Key { 598 case "ice-lite": 599 if s.ice.lite == nil || !*s.ice.lite { 600 return errors.New("ice lite mismatch") 601 } 602 case "ice-options": 603 if s.ice.options != "" && s.ice.options != a.Value { 604 return errors.New("ice options mismatch") 605 } 606 } 607 } 608 } 609 610 foundMediaMid := false 611 if s.media != nil && s.media.mid != "" { 612 for _, md := range parsed.MediaDescriptions { 613 mid, found := md.Attribute(sdp.AttrKeyMID) 614 if found && mid == s.media.mid { 615 foundMediaMid = true 616 break 617 } 618 } 619 } 620 if !foundMediaMid { 621 return errors.New("could not find media mid") 622 } 623 624 if s.media != nil && s.media.ice != nil && (s.media.ice.lite != nil || s.media.ice.options != "") { 625 for _, md := range parsed.MediaDescriptions { 626 for _, a := range md.Attributes { 627 switch a.Key { 628 case "ice-lite": 629 if s.media.ice.lite == nil || !*s.media.ice.lite { 630 return errors.New("ice lite mismatch") 631 } 632 case "ice-options": 633 if s.media.ice.options != "" && s.media.ice.options != a.Value { 634 return errors.New("ice options mismatch") 635 } 636 } 637 } 638 } 639 } 640 641 if s.ice != nil && s.ice.ufrag != "" && s.ice.pwd != "" { 642 for idx, a := range parsed.Attributes { 643 switch a.Key { 644 case "ice-ufrag": 645 parsed.Attributes[idx] = sdp.Attribute{ 646 Key: "ice-ufrag", 647 Value: s.ice.ufrag, 648 } 649 case "ice-pwd": 650 parsed.Attributes[idx] = sdp.Attribute{ 651 Key: "ice-pwd", 652 Value: s.ice.pwd, 653 } 654 } 655 } 656 } 657 658 if s.media != nil { 659 for _, md := range parsed.MediaDescriptions { 660 for idx, a := range md.Attributes { 661 switch a.Key { 662 case "ice-ufrag": 663 if s.media.ice.ufrag != "" { 664 md.Attributes[idx] = sdp.Attribute{ 665 Key: "ice-ufrag", 666 Value: s.media.ice.ufrag, 667 } 668 } 669 case "ice-pwd": 670 if s.media.ice.pwd != "" { 671 md.Attributes[idx] = sdp.Attribute{ 672 Key: "ice-pwd", 673 Value: s.media.ice.pwd, 674 } 675 } 676 } 677 } 678 679 // clean out existing candidates and patch in new ones 680 for idx, a := range md.Attributes { 681 if a.IsICECandidate() || a.Key == sdp.AttrKeyEndOfCandidates { 682 md.Attributes = append(md.Attributes[:idx], md.Attributes[idx+1:]...) 683 } 684 } 685 686 for _, ic := range s.media.candidates { 687 md.Attributes = append( 688 md.Attributes, 689 sdp.Attribute{ 690 Key: sdp.AttrKeyCandidate, 691 Value: ic, 692 }, 693 ) 694 } 695 696 if s.media.endOfCandidates != nil && *s.media.endOfCandidates { 697 md.Attributes = append( 698 md.Attributes, 699 sdp.Attribute{Key: sdp.AttrKeyEndOfCandidates}, 700 ) 701 } 702 } 703 } 704 return nil 705 } 706 707 // primarily for use with WHIP ICE Restart - https://www.rfc-editor.org/rfc/rfc9725.html#name-ice-restarts 708 func ExtractSDPFragment(parsed *sdp.SessionDescription) (*SDPFragment, error) { 709 bundleMid, found := GetBundleMid(parsed) 710 if !found { 711 return nil, errors.New("could not get bundle mid") 712 } 713 714 s := &SDPFragment{} 715 if group, found := parsed.Attribute(sdp.AttrKeyGroup); found { 716 s.group = group 717 } 718 719 s.ice = &sdpFragmentICE{} 720 if err := s.ice.Unmarshal(parsed.Attributes); err != nil { 721 return nil, err 722 } 723 724 foundBundleMedia := false 725 for _, md := range parsed.MediaDescriptions { 726 mid, found := md.Attribute(sdp.AttrKeyMID) 727 if !found || mid != bundleMid { 728 continue 729 } 730 731 foundBundleMedia = true 732 733 s.media = &sdpFragmentMedia{} 734 if err := s.media.Unmarshal(md); err != nil { 735 return nil, err 736 } 737 break 738 } 739 740 if !foundBundleMedia { 741 return nil, fmt.Errorf("could not find bundle media: %s", bundleMid) 742 } 743 744 return s, nil 745 }