summaryrefslogtreecommitdiff
path: root/vendor/github.com/pion/webrtc/v3/sdp.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/pion/webrtc/v3/sdp.go')
-rw-r--r--vendor/github.com/pion/webrtc/v3/sdp.go643
1 files changed, 643 insertions, 0 deletions
diff --git a/vendor/github.com/pion/webrtc/v3/sdp.go b/vendor/github.com/pion/webrtc/v3/sdp.go
new file mode 100644
index 0000000..7454696
--- /dev/null
+++ b/vendor/github.com/pion/webrtc/v3/sdp.go
@@ -0,0 +1,643 @@
+// +build !js
+
+package webrtc
+
+import (
+ "fmt"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/pion/ice/v2"
+ "github.com/pion/logging"
+ "github.com/pion/sdp/v3"
+)
+
+// trackDetails represents any media source that can be represented in a SDP
+// This isn't keyed by SSRC because it also needs to support rid based sources
+type trackDetails struct {
+ mid string
+ kind RTPCodecType
+ streamID string
+ id string
+ ssrc SSRC
+ rids []string
+}
+
+func trackDetailsForSSRC(trackDetails []trackDetails, ssrc SSRC) *trackDetails {
+ for i := range trackDetails {
+ if trackDetails[i].ssrc == ssrc {
+ return &trackDetails[i]
+ }
+ }
+ return nil
+}
+
+func filterTrackWithSSRC(incomingTracks []trackDetails, ssrc SSRC) []trackDetails {
+ filtered := []trackDetails{}
+ for i := range incomingTracks {
+ if incomingTracks[i].ssrc != ssrc {
+ filtered = append(filtered, incomingTracks[i])
+ }
+ }
+ return filtered
+}
+
+// extract all trackDetails from an SDP.
+func trackDetailsFromSDP(log logging.LeveledLogger, s *sdp.SessionDescription) []trackDetails { // nolint:gocognit
+ incomingTracks := []trackDetails{}
+ rtxRepairFlows := map[uint32]bool{}
+
+ for _, media := range s.MediaDescriptions {
+ // Plan B can have multiple tracks in a signle media section
+ streamID := ""
+ trackID := ""
+
+ // If media section is recvonly or inactive skip
+ if _, ok := media.Attribute(sdp.AttrKeyRecvOnly); ok {
+ continue
+ } else if _, ok := media.Attribute(sdp.AttrKeyInactive); ok {
+ continue
+ }
+
+ midValue := getMidValue(media)
+ if midValue == "" {
+ continue
+ }
+
+ codecType := NewRTPCodecType(media.MediaName.Media)
+ if codecType == 0 {
+ continue
+ }
+
+ for _, attr := range media.Attributes {
+ switch attr.Key {
+ case sdp.AttrKeySSRCGroup:
+ split := strings.Split(attr.Value, " ")
+ if split[0] == sdp.SemanticTokenFlowIdentification {
+ // Add rtx ssrcs to blacklist, to avoid adding them as tracks
+ // Essentially lines like `a=ssrc-group:FID 2231627014 632943048` are processed by this section
+ // as this declares that the second SSRC (632943048) is a rtx repair flow (RFC4588) for the first
+ // (2231627014) as specified in RFC5576
+ if len(split) == 3 {
+ _, err := strconv.ParseUint(split[1], 10, 32)
+ if err != nil {
+ log.Warnf("Failed to parse SSRC: %v", err)
+ continue
+ }
+ rtxRepairFlow, err := strconv.ParseUint(split[2], 10, 32)
+ if err != nil {
+ log.Warnf("Failed to parse SSRC: %v", err)
+ continue
+ }
+ rtxRepairFlows[uint32(rtxRepairFlow)] = true
+ incomingTracks = filterTrackWithSSRC(incomingTracks, SSRC(rtxRepairFlow)) // Remove if rtx was added as track before
+ }
+ }
+
+ // Handle `a=msid:<stream_id> <track_label>` for Unified plan. The first value is the same as MediaStream.id
+ // in the browser and can be used to figure out which tracks belong to the same stream. The browser should
+ // figure this out automatically when an ontrack event is emitted on RTCPeerConnection.
+ case sdp.AttrKeyMsid:
+ split := strings.Split(attr.Value, " ")
+ if len(split) == 2 {
+ streamID = split[0]
+ trackID = split[1]
+ }
+
+ case sdp.AttrKeySSRC:
+ split := strings.Split(attr.Value, " ")
+ ssrc, err := strconv.ParseUint(split[0], 10, 32)
+ if err != nil {
+ log.Warnf("Failed to parse SSRC: %v", err)
+ continue
+ }
+
+ if rtxRepairFlow := rtxRepairFlows[uint32(ssrc)]; rtxRepairFlow {
+ continue // This ssrc is a RTX repair flow, ignore
+ }
+
+ if len(split) == 3 && strings.HasPrefix(split[1], "msid:") {
+ streamID = split[1][len("msid:"):]
+ trackID = split[2]
+ }
+
+ isNewTrack := true
+ trackDetails := &trackDetails{}
+ for i := range incomingTracks {
+ if incomingTracks[i].ssrc == SSRC(ssrc) {
+ trackDetails = &incomingTracks[i]
+ isNewTrack = false
+ }
+ }
+
+ trackDetails.mid = midValue
+ trackDetails.kind = codecType
+ trackDetails.streamID = streamID
+ trackDetails.id = trackID
+ trackDetails.ssrc = SSRC(ssrc)
+
+ if isNewTrack {
+ incomingTracks = append(incomingTracks, *trackDetails)
+ }
+ }
+ }
+
+ if rids := getRids(media); len(rids) != 0 && trackID != "" && streamID != "" {
+ newTrack := trackDetails{
+ mid: midValue,
+ kind: codecType,
+ streamID: streamID,
+ id: trackID,
+ rids: []string{},
+ }
+ for rid := range rids {
+ newTrack.rids = append(newTrack.rids, rid)
+ }
+
+ incomingTracks = append(incomingTracks, newTrack)
+ }
+ }
+ return incomingTracks
+}
+
+func getRids(media *sdp.MediaDescription) map[string]string {
+ rids := map[string]string{}
+ for _, attr := range media.Attributes {
+ if attr.Key == "rid" {
+ split := strings.Split(attr.Value, " ")
+ rids[split[0]] = attr.Value
+ }
+ }
+ return rids
+}
+
+func addCandidatesToMediaDescriptions(candidates []ICECandidate, m *sdp.MediaDescription, iceGatheringState ICEGatheringState) error {
+ appendCandidateIfNew := func(c ice.Candidate, attributes []sdp.Attribute) {
+ marshaled := c.Marshal()
+ for _, a := range attributes {
+ if marshaled == a.Value {
+ return
+ }
+ }
+
+ m.WithValueAttribute("candidate", marshaled)
+ }
+
+ for _, c := range candidates {
+ candidate, err := c.toICE()
+ if err != nil {
+ return err
+ }
+
+ candidate.SetComponent(1)
+ appendCandidateIfNew(candidate, m.Attributes)
+
+ candidate.SetComponent(2)
+ appendCandidateIfNew(candidate, m.Attributes)
+ }
+
+ if iceGatheringState != ICEGatheringStateComplete {
+ return nil
+ }
+ for _, a := range m.Attributes {
+ if a.Key == "end-of-candidates" {
+ return nil
+ }
+ }
+
+ m.WithPropertyAttribute("end-of-candidates")
+ return nil
+}
+
+func addDataMediaSection(d *sdp.SessionDescription, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState) error {
+ media := (&sdp.MediaDescription{
+ MediaName: sdp.MediaName{
+ Media: mediaSectionApplication,
+ Port: sdp.RangedPort{Value: 9},
+ Protos: []string{"UDP", "DTLS", "SCTP"},
+ Formats: []string{"webrtc-datachannel"},
+ },
+ ConnectionInformation: &sdp.ConnectionInformation{
+ NetworkType: "IN",
+ AddressType: "IP4",
+ Address: &sdp.Address{
+ Address: "0.0.0.0",
+ },
+ },
+ }).
+ WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()).
+ WithValueAttribute(sdp.AttrKeyMID, midValue).
+ WithPropertyAttribute(RTPTransceiverDirectionSendrecv.String()).
+ WithPropertyAttribute("sctp-port:5000").
+ WithICECredentials(iceParams.UsernameFragment, iceParams.Password)
+
+ for _, f := range dtlsFingerprints {
+ media = media.WithFingerprint(f.Algorithm, strings.ToUpper(f.Value))
+ }
+
+ if shouldAddCandidates {
+ if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil {
+ return err
+ }
+ }
+
+ d.WithMedia(media)
+ return nil
+}
+
+func populateLocalCandidates(sessionDescription *SessionDescription, i *ICEGatherer, iceGatheringState ICEGatheringState) *SessionDescription {
+ if sessionDescription == nil || i == nil {
+ return sessionDescription
+ }
+
+ candidates, err := i.GetLocalCandidates()
+ if err != nil {
+ return sessionDescription
+ }
+
+ parsed := sessionDescription.parsed
+ if len(parsed.MediaDescriptions) > 0 {
+ m := parsed.MediaDescriptions[0]
+ if err = addCandidatesToMediaDescriptions(candidates, m, iceGatheringState); err != nil {
+ return sessionDescription
+ }
+ }
+
+ sdp, err := parsed.Marshal()
+ if err != nil {
+ return sessionDescription
+ }
+
+ return &SessionDescription{
+ SDP: string(sdp),
+ Type: sessionDescription.Type,
+ }
+}
+
+func addTransceiverSDP(d *sdp.SessionDescription, isPlanB, shouldAddCandidates bool, dtlsFingerprints []DTLSFingerprint, mediaEngine *MediaEngine, midValue string, iceParams ICEParameters, candidates []ICECandidate, dtlsRole sdp.ConnectionRole, iceGatheringState ICEGatheringState, mediaSection mediaSection) (bool, error) {
+ transceivers := mediaSection.transceivers
+ if len(transceivers) < 1 {
+ return false, errSDPZeroTransceivers
+ }
+ // Use the first transceiver to generate the section attributes
+ t := transceivers[0]
+ media := sdp.NewJSEPMediaDescription(t.kind.String(), []string{}).
+ WithValueAttribute(sdp.AttrKeyConnectionSetup, dtlsRole.String()).
+ WithValueAttribute(sdp.AttrKeyMID, midValue).
+ WithICECredentials(iceParams.UsernameFragment, iceParams.Password).
+ WithPropertyAttribute(sdp.AttrKeyRTCPMux).
+ WithPropertyAttribute(sdp.AttrKeyRTCPRsize)
+
+ codecs := mediaEngine.getCodecsByKind(t.kind)
+ for _, codec := range codecs {
+ name := strings.TrimPrefix(codec.MimeType, "audio/")
+ name = strings.TrimPrefix(name, "video/")
+ media.WithCodec(uint8(codec.PayloadType), name, codec.ClockRate, codec.Channels, codec.SDPFmtpLine)
+
+ for _, feedback := range codec.RTPCodecCapability.RTCPFeedback {
+ media.WithValueAttribute("rtcp-fb", fmt.Sprintf("%d %s %s", codec.PayloadType, feedback.Type, feedback.Parameter))
+ }
+ }
+ if len(codecs) == 0 {
+ // Explicitly reject track if we don't have the codec
+ d.WithMedia(&sdp.MediaDescription{
+ MediaName: sdp.MediaName{
+ Media: t.kind.String(),
+ Port: sdp.RangedPort{Value: 0},
+ Protos: []string{"UDP", "TLS", "RTP", "SAVPF"},
+ Formats: []string{"0"},
+ },
+ })
+ return false, nil
+ }
+
+ directions := []RTPTransceiverDirection{}
+ if t.Sender() != nil {
+ directions = append(directions, RTPTransceiverDirectionSendonly)
+ }
+ if t.Receiver() != nil {
+ directions = append(directions, RTPTransceiverDirectionRecvonly)
+ }
+
+ parameters := mediaEngine.getRTPParametersByKind(t.kind, directions)
+ for _, rtpExtension := range parameters.HeaderExtensions {
+ extURL, err := url.Parse(rtpExtension.URI)
+ if err != nil {
+ return false, err
+ }
+ media.WithExtMap(sdp.ExtMap{Value: rtpExtension.ID, URI: extURL})
+ }
+
+ if len(mediaSection.ridMap) > 0 {
+ recvRids := make([]string, 0, len(mediaSection.ridMap))
+
+ for rid := range mediaSection.ridMap {
+ media.WithValueAttribute("rid", rid+" recv")
+ recvRids = append(recvRids, rid)
+ }
+ // Simulcast
+ media.WithValueAttribute("simulcast", "recv "+strings.Join(recvRids, ";"))
+ }
+
+ for _, mt := range transceivers {
+ if mt.Sender() != nil && mt.Sender().Track() != nil {
+ track := mt.Sender().Track()
+ media = media.WithMediaSource(uint32(mt.Sender().ssrc), track.StreamID() /* cname */, track.StreamID() /* streamLabel */, track.ID())
+ if !isPlanB {
+ media = media.WithPropertyAttribute("msid:" + track.StreamID() + " " + track.ID())
+ break
+ }
+ }
+ }
+
+ media = media.WithPropertyAttribute(t.Direction().String())
+
+ for _, fingerprint := range dtlsFingerprints {
+ media = media.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value))
+ }
+
+ if shouldAddCandidates {
+ if err := addCandidatesToMediaDescriptions(candidates, media, iceGatheringState); err != nil {
+ return false, err
+ }
+ }
+
+ d.WithMedia(media)
+
+ return true, nil
+}
+
+type mediaSection struct {
+ id string
+ transceivers []*RTPTransceiver
+ data bool
+ ridMap map[string]string
+}
+
+// populateSDP serializes a PeerConnections state into an SDP
+func populateSDP(d *sdp.SessionDescription, isPlanB bool, dtlsFingerprints []DTLSFingerprint, mediaDescriptionFingerprint bool, isICELite bool, mediaEngine *MediaEngine, connectionRole sdp.ConnectionRole, candidates []ICECandidate, iceParams ICEParameters, mediaSections []mediaSection, iceGatheringState ICEGatheringState) (*sdp.SessionDescription, error) {
+ var err error
+ mediaDtlsFingerprints := []DTLSFingerprint{}
+
+ if mediaDescriptionFingerprint {
+ mediaDtlsFingerprints = dtlsFingerprints
+ }
+
+ bundleValue := "BUNDLE"
+ bundleCount := 0
+ appendBundle := func(midValue string) {
+ bundleValue += " " + midValue
+ bundleCount++
+ }
+
+ for i, m := range mediaSections {
+ if m.data && len(m.transceivers) != 0 {
+ return nil, errSDPMediaSectionMediaDataChanInvalid
+ } else if !isPlanB && len(m.transceivers) > 1 {
+ return nil, errSDPMediaSectionMultipleTrackInvalid
+ }
+
+ shouldAddID := true
+ shouldAddCanidates := i == 0
+ if m.data {
+ if err = addDataMediaSection(d, shouldAddCanidates, mediaDtlsFingerprints, m.id, iceParams, candidates, connectionRole, iceGatheringState); err != nil {
+ return nil, err
+ }
+ } else {
+ shouldAddID, err = addTransceiverSDP(d, isPlanB, shouldAddCanidates, mediaDtlsFingerprints, mediaEngine, m.id, iceParams, candidates, connectionRole, iceGatheringState, m)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if shouldAddID {
+ appendBundle(m.id)
+ }
+ }
+
+ if !mediaDescriptionFingerprint {
+ for _, fingerprint := range dtlsFingerprints {
+ d.WithFingerprint(fingerprint.Algorithm, strings.ToUpper(fingerprint.Value))
+ }
+ }
+
+ if isICELite {
+ // RFC 5245 S15.3
+ d = d.WithValueAttribute(sdp.AttrKeyICELite, sdp.AttrKeyICELite)
+ }
+
+ return d.WithValueAttribute(sdp.AttrKeyGroup, bundleValue), nil
+}
+
+func getMidValue(media *sdp.MediaDescription) string {
+ for _, attr := range media.Attributes {
+ if attr.Key == "mid" {
+ return attr.Value
+ }
+ }
+ return ""
+}
+
+func descriptionIsPlanB(desc *SessionDescription) bool {
+ if desc == nil || desc.parsed == nil {
+ return false
+ }
+
+ detectionRegex := regexp.MustCompile(`(?i)^(audio|video|data)$`)
+ for _, media := range desc.parsed.MediaDescriptions {
+ if len(detectionRegex.FindStringSubmatch(getMidValue(media))) == 2 {
+ return true
+ }
+ }
+ return false
+}
+
+func getPeerDirection(media *sdp.MediaDescription) RTPTransceiverDirection {
+ for _, a := range media.Attributes {
+ if direction := NewRTPTransceiverDirection(a.Key); direction != RTPTransceiverDirection(Unknown) {
+ return direction
+ }
+ }
+ return RTPTransceiverDirection(Unknown)
+}
+
+func extractFingerprint(desc *sdp.SessionDescription) (string, string, error) {
+ fingerprints := []string{}
+
+ if fingerprint, haveFingerprint := desc.Attribute("fingerprint"); haveFingerprint {
+ fingerprints = append(fingerprints, fingerprint)
+ }
+
+ for _, m := range desc.MediaDescriptions {
+ if fingerprint, haveFingerprint := m.Attribute("fingerprint"); haveFingerprint {
+ fingerprints = append(fingerprints, fingerprint)
+ }
+ }
+
+ if len(fingerprints) < 1 {
+ return "", "", ErrSessionDescriptionNoFingerprint
+ }
+
+ for _, m := range fingerprints {
+ if m != fingerprints[0] {
+ return "", "", ErrSessionDescriptionConflictingFingerprints
+ }
+ }
+
+ parts := strings.Split(fingerprints[0], " ")
+ if len(parts) != 2 {
+ return "", "", ErrSessionDescriptionInvalidFingerprint
+ }
+ return parts[1], parts[0], nil
+}
+
+func extractICEDetails(desc *sdp.SessionDescription) (string, string, []ICECandidate, error) {
+ candidates := []ICECandidate{}
+ remotePwds := []string{}
+ remoteUfrags := []string{}
+
+ if ufrag, haveUfrag := desc.Attribute("ice-ufrag"); haveUfrag {
+ remoteUfrags = append(remoteUfrags, ufrag)
+ }
+ if pwd, havePwd := desc.Attribute("ice-pwd"); havePwd {
+ remotePwds = append(remotePwds, pwd)
+ }
+
+ for _, m := range desc.MediaDescriptions {
+ if ufrag, haveUfrag := m.Attribute("ice-ufrag"); haveUfrag {
+ remoteUfrags = append(remoteUfrags, ufrag)
+ }
+ if pwd, havePwd := m.Attribute("ice-pwd"); havePwd {
+ remotePwds = append(remotePwds, pwd)
+ }
+
+ for _, a := range m.Attributes {
+ if a.IsICECandidate() {
+ c, err := ice.UnmarshalCandidate(a.Value)
+ if err != nil {
+ return "", "", nil, err
+ }
+
+ candidate, err := newICECandidateFromICE(c)
+ if err != nil {
+ return "", "", nil, err
+ }
+
+ candidates = append(candidates, candidate)
+ }
+ }
+ }
+
+ if len(remoteUfrags) == 0 {
+ return "", "", nil, ErrSessionDescriptionMissingIceUfrag
+ } else if len(remotePwds) == 0 {
+ return "", "", nil, ErrSessionDescriptionMissingIcePwd
+ }
+
+ for _, m := range remoteUfrags {
+ if m != remoteUfrags[0] {
+ return "", "", nil, ErrSessionDescriptionConflictingIceUfrag
+ }
+ }
+
+ for _, m := range remotePwds {
+ if m != remotePwds[0] {
+ return "", "", nil, ErrSessionDescriptionConflictingIcePwd
+ }
+ }
+
+ return remoteUfrags[0], remotePwds[0], candidates, nil
+}
+
+func haveApplicationMediaSection(desc *sdp.SessionDescription) bool {
+ for _, m := range desc.MediaDescriptions {
+ if m.MediaName.Media == mediaSectionApplication {
+ return true
+ }
+ }
+
+ return false
+}
+
+func getByMid(searchMid string, desc *SessionDescription) *sdp.MediaDescription {
+ for _, m := range desc.parsed.MediaDescriptions {
+ if mid, ok := m.Attribute(sdp.AttrKeyMID); ok && mid == searchMid {
+ return m
+ }
+ }
+ return nil
+}
+
+// haveDataChannel return MediaDescription with MediaName equal application
+func haveDataChannel(desc *SessionDescription) *sdp.MediaDescription {
+ for _, d := range desc.parsed.MediaDescriptions {
+ if d.MediaName.Media == mediaSectionApplication {
+ return d
+ }
+ }
+ return nil
+}
+
+func codecsFromMediaDescription(m *sdp.MediaDescription) (out []RTPCodecParameters, err error) {
+ s := &sdp.SessionDescription{
+ MediaDescriptions: []*sdp.MediaDescription{m},
+ }
+
+ for _, payloadStr := range m.MediaName.Formats {
+ payloadType, err := strconv.Atoi(payloadStr)
+ if err != nil {
+ return nil, err
+ }
+
+ codec, err := s.GetCodecForPayloadType(uint8(payloadType))
+ if err != nil {
+ if payloadType == 0 {
+ continue
+ }
+ return nil, err
+ }
+
+ channels := uint16(0)
+ val, err := strconv.Atoi(codec.EncodingParameters)
+ if err == nil {
+ channels = uint16(val)
+ }
+
+ feedback := []RTCPFeedback{}
+ for _, raw := range codec.RTCPFeedback {
+ split := strings.Split(raw, " ")
+ entry := RTCPFeedback{Type: split[0]}
+ if len(split) == 2 {
+ entry.Parameter = split[1]
+ }
+
+ feedback = append(feedback, entry)
+ }
+
+ out = append(out, RTPCodecParameters{
+ RTPCodecCapability: RTPCodecCapability{m.MediaName.Media + "/" + codec.Name, codec.ClockRate, channels, codec.Fmtp, feedback},
+ PayloadType: PayloadType(payloadType),
+ })
+ }
+
+ return out, nil
+}
+
+func rtpExtensionsFromMediaDescription(m *sdp.MediaDescription) (map[string]int, error) {
+ out := map[string]int{}
+
+ for _, a := range m.Attributes {
+ if a.Key == sdp.AttrKeyExtMap {
+ e := sdp.ExtMap{}
+ if err := e.Unmarshal(a.String()); err != nil {
+ return nil, err
+ }
+
+ out[e.URI.String()] = e.Value
+ }
+ }
+
+ return out, nil
+}