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  }