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  }