diff --git a/go.mod b/go.mod index f7b62bc9..46c236b2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ require ( github.com/pions/dtls v1.0.2 github.com/pions/pkg v0.0.0-20181115215726-b60cd756f712 github.com/pions/sctp v1.0.0 + github.com/pions/sdp v1.0.0 github.com/pions/transport v0.0.0-20181219213214-cd29ef7d0726 github.com/pkg/errors v0.8.0 github.com/stretchr/testify v1.2.2 diff --git a/go.sum b/go.sum index 37fd357d..f40a3666 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/pions/pkg v0.0.0-20181115215726-b60cd756f712 h1:ciXO7F7PusyAzW/EZJt01 github.com/pions/pkg v0.0.0-20181115215726-b60cd756f712/go.mod h1:r9wKZs+Xxv2acLspex4CHQiIhFjGK1zGP+nUm/8klXA= github.com/pions/sctp v1.0.0 h1:ScxSzY867GGr0peNGYqOdw6bPBymM0V9UcbKrsTfeDA= github.com/pions/sctp v1.0.0/go.mod h1:lamdubZNMS+X5BjlMmsjWrg78Aa87tjYswD7p+AIZhc= +github.com/pions/sdp v1.0.0 h1:OxCczxFOKzudhJRMkntSX+Q0fZK1MQJugGh6n9hq3nw= +github.com/pions/sdp v1.0.0/go.mod h1:moNMmnVSlx8rBBb39U9t0Rdr7xvMlqiJjHlMESRad5k= github.com/pions/transport v0.0.0-20181219213214-cd29ef7d0726 h1:kQFdB0RkBeK7TavRC09+5OAN73/NCSdz3AMxZS8v6H8= github.com/pions/transport v0.0.0-20181219213214-cd29ef7d0726/go.mod h1:HLhzI7I0k8TyiQ99hfRZNRf84lG76eaFnZHnVy/wFnM= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= diff --git a/internal/sdp/common_description.go b/internal/sdp/common_description.go deleted file mode 100644 index 4b46eb93..00000000 --- a/internal/sdp/common_description.go +++ /dev/null @@ -1,111 +0,0 @@ -package sdp - -import ( - "fmt" - "net" - "strconv" - "strings" -) - -// Information describes the "i=" field which provides textual information -// about the session. -type Information string - -func (i *Information) String() *string { - output := string(*i) - return &output -} - -// ConnectionInformation defines the representation for the "c=" field -// containing connection data. -type ConnectionInformation struct { - NetworkType string - AddressType string - Address *Address -} - -func (c *ConnectionInformation) String() *string { - output := fmt.Sprintf( - "%v %v %v", - c.NetworkType, - c.AddressType, - c.Address.String(), - ) - return &output -} - -// Address desribes a structured address token from within the "c=" field. -type Address struct { - IP net.IP - TTL *int - Range *int -} - -func (c *Address) String() string { - var parts []string - parts = append(parts, c.IP.String()) - if c.TTL != nil { - parts = append(parts, strconv.Itoa(*c.TTL)) - } - - if c.Range != nil { - parts = append(parts, strconv.Itoa(*c.Range)) - } - - return strings.Join(parts, "/") -} - -// Bandwidth describes an optional field which denotes the proposed bandwidth -// to be used by the session or media. -type Bandwidth struct { - Experimental bool - Type string - Bandwidth uint64 -} - -func (b *Bandwidth) String() *string { - var output string - if b.Experimental { - output += "X-" - } - output += b.Type + ":" + strconv.FormatUint(b.Bandwidth, 10) - return &output -} - -// EncryptionKey describes the "k=" which conveys encryption key information. -type EncryptionKey string - -func (s *EncryptionKey) String() *string { - output := string(*s) - return &output -} - -// Attribute describes the "a=" field which represents the primary means for -// extending SDP. -type Attribute struct { - Key string - Value string -} - -// NewPropertyAttribute constructs a new attribute -func NewPropertyAttribute(key string) Attribute { - return Attribute{ - Key: key, - } -} - -// NewAttribute constructs a new attribute -func NewAttribute(key, value string) Attribute { - return Attribute{ - Key: key, - Value: value, - } -} - -func (a *Attribute) String() *string { - output := a.Key - if len(a.Value) > 0 { - output += ":" + a.Value - } - return &output -} diff --git a/internal/sdp/ice.go b/internal/sdp/ice.go deleted file mode 100644 index 891b2440..00000000 --- a/internal/sdp/ice.go +++ /dev/null @@ -1,78 +0,0 @@ -package sdp - -import ( - "fmt" - "net" - "strconv" - "strings" - - "github.com/pions/webrtc/pkg/ice" -) - -// ICECandidateUnmarshal takes a candidate strings and returns a ice.Candidate or nil if it fails to parse -// TODO: return error if parsing fails -func ICECandidateUnmarshal(raw string) (*ice.Candidate, error) { - split := strings.Fields(raw) - if len(split) < 8 { - return nil, fmt.Errorf("attribute not long enough to be ICE candidate (%d) %s", len(split), raw) - } - - getValue := func(key string) string { - rtrnNext := false - for _, i := range split { - if rtrnNext { - return i - } else if i == key { - rtrnNext = true - } - } - return "" - } - - port, err := strconv.Atoi(split[5]) - if err != nil { - return nil, err - } - - transport := split[2] - - // TODO verify valid address - ip := net.ParseIP(split[4]) - if ip == nil { - return nil, err - } - - switch getValue("typ") { - case "host": - return ice.NewCandidateHost(transport, ip, port) - case "srflx": - return ice.NewCandidateServerReflexive(transport, ip, port, "", 0) // TODO: parse related address - default: - return nil, fmt.Errorf("Unhandled candidate typ %s", getValue("typ")) - } -} - -func iceCandidateString(c *ice.Candidate, component int) string { - // TODO: calculate foundation - switch c.Type { - case ice.CandidateTypeHost: - return fmt.Sprintf("foundation %d %s %d %s %d typ host generation 0", - component, c.NetworkShort(), c.Priority(c.Type.Preference(), uint16(component)), c.IP, c.Port) - - case ice.CandidateTypeServerReflexive: - return fmt.Sprintf("foundation %d %s %d %s %d typ srflx raddr %s rport %d generation 0", - component, c.NetworkShort(), c.Priority(c.Type.Preference(), uint16(component)), c.IP, c.Port, - c.RelatedAddress.Address, c.RelatedAddress.Port) - } - return "" -} - -// ICECandidateMarshal takes a candidate and returns a string representation -func ICECandidateMarshal(c *ice.Candidate) []string { - out := make([]string, 0) - - out = append(out, iceCandidateString(c, 1)) - out = append(out, iceCandidateString(c, 2)) - - return out -} diff --git a/internal/sdp/jsep.go b/internal/sdp/jsep.go deleted file mode 100644 index 0b1fe41b..00000000 --- a/internal/sdp/jsep.go +++ /dev/null @@ -1,159 +0,0 @@ -package sdp - -import ( - "fmt" - "net" - "strconv" - "time" -) - -// Constants for SDP attributes used in JSEP -const ( - AttrKeyIdentity = "identity" - AttrKeyGroup = "group" - AttrKeySsrc = "ssrc" - AttrKeySsrcGroup = "ssrc-group" - AttrKeyMsidSemantic = "msid-semantic" - AttrKeyConnectionSetup = "setup" - AttrKeyMID = "mid" - AttrKeyICELite = "ice-lite" - AttrKeyRtcpMux = "rtcp-mux" - AttrKeyRtcpRsize = "rtcp-rsize" -) - -// Constants for semantic tokens used in JSEP -const ( - SemanticTokenLipSynchronization = "LS" - SemanticTokenFlowIdentification = "FID" - SemanticTokenForwardErrorCorrection = "FEC" - SemanticTokenWebRTCMediaStreams = "WMS" -) - -// API to match draft-ietf-rtcweb-jsep -// Move to webrtc or its own package? - -// NewJSEPSessionDescription creates a new SessionDescription with -// some settings that are required by the JSEP spec. -func NewJSEPSessionDescription(identity bool) *SessionDescription { - d := &SessionDescription{ - Version: 0, - Origin: Origin{ - Username: "-", - SessionID: newSessionID(), - SessionVersion: uint64(time.Now().Unix()), - NetworkType: "IN", - AddressType: "IP4", - UnicastAddress: "0.0.0.0", - }, - SessionName: "-", - TimeDescriptions: []TimeDescription{ - { - Timing: Timing{ - StartTime: 0, - StopTime: 0, - }, - RepeatTimes: nil, - }, - }, - Attributes: []Attribute{ - // "Attribute(ice-options:trickle)", // TODO: implement trickle ICE - }, - } - - if identity { - d.WithPropertyAttribute(AttrKeyIdentity) - } - - return d -} - -// WithPropertyAttribute adds a property attribute 'a=key' to the session description -func (s *SessionDescription) WithPropertyAttribute(key string) *SessionDescription { - s.Attributes = append(s.Attributes, NewPropertyAttribute(key)) - return s -} - -// WithValueAttribute adds a value attribute 'a=key:value' to the session description -func (s *SessionDescription) WithValueAttribute(key, value string) *SessionDescription { - s.Attributes = append(s.Attributes, NewAttribute(key, value)) - return s -} - -// WithFingerprint adds a fingerprint to the session description -func (s *SessionDescription) WithFingerprint(algorithm, value string) *SessionDescription { - return s.WithValueAttribute("fingerprint", algorithm+" "+value) -} - -// WithMedia adds a media description to the session description -func (s *SessionDescription) WithMedia(md *MediaDescription) *SessionDescription { - s.MediaDescriptions = append(s.MediaDescriptions, md) - return s -} - -// NewJSEPMediaDescription creates a new MediaName with -// some settings that are required by the JSEP spec. -func NewJSEPMediaDescription(codecType string, codecPrefs []string) *MediaDescription { - // TODO: handle codecPrefs - d := &MediaDescription{ - MediaName: MediaName{ - Media: codecType, - Port: RangedPort{Value: 9}, - Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, - }, - ConnectionInformation: &ConnectionInformation{ - NetworkType: "IN", - AddressType: "IP4", - Address: &Address{ - IP: net.ParseIP("0.0.0.0"), - }, - }, - } - return d -} - -// WithPropertyAttribute adds a property attribute 'a=key' to the media description -func (d *MediaDescription) WithPropertyAttribute(key string) *MediaDescription { - d.Attributes = append(d.Attributes, NewPropertyAttribute(key)) - return d -} - -// WithValueAttribute adds a value attribute 'a=key:value' to the media description -func (d *MediaDescription) WithValueAttribute(key, value string) *MediaDescription { - d.Attributes = append(d.Attributes, NewAttribute(key, value)) - return d -} - -// WithICECredentials adds ICE credentials to the media description -func (d *MediaDescription) WithICECredentials(username, password string) *MediaDescription { - return d. - WithValueAttribute("ice-ufrag", username). - WithValueAttribute("ice-pwd", password) -} - -// WithCodec adds codec information to the media description -func (d *MediaDescription) WithCodec(payloadType uint8, name string, clockrate uint32, channels uint16, fmtp string) *MediaDescription { - d.MediaName.Formats = append(d.MediaName.Formats, strconv.Itoa(int(payloadType))) - rtpmap := fmt.Sprintf("%d %s/%d", payloadType, name, clockrate) - if channels > 0 { - rtpmap = rtpmap + fmt.Sprintf("/%d", channels) - } - d.WithValueAttribute("rtpmap", rtpmap) - if fmtp != "" { - d.WithValueAttribute("fmtp", fmt.Sprintf("%d %s", payloadType, fmtp)) - } - return d -} - -// WithMediaSource adds media source information to the media description -func (d *MediaDescription) WithMediaSource(ssrc uint32, cname, streamLabel, label string) *MediaDescription { - return d. - WithValueAttribute("ssrc", fmt.Sprintf("%d cname:%s", ssrc, cname)). // Deprecated but not phased out? - WithValueAttribute("ssrc", fmt.Sprintf("%d msid:%s %s", ssrc, streamLabel, label)). - WithValueAttribute("ssrc", fmt.Sprintf("%d mslabel:%s", ssrc, streamLabel)). // Deprecated but not phased out? - WithValueAttribute("ssrc", fmt.Sprintf("%d label:%s", ssrc, label)) // Deprecated but not phased out? -} - -// WithCandidate adds an ICE candidate to the media description -func (d *MediaDescription) WithCandidate(value string) *MediaDescription { - return d.WithValueAttribute("candidate", value) -} diff --git a/internal/sdp/marshal.go b/internal/sdp/marshal.go deleted file mode 100644 index 4b3a66a1..00000000 --- a/internal/sdp/marshal.go +++ /dev/null @@ -1,118 +0,0 @@ -package sdp - -import ( - "strings" -) - -// Marshal takes a SDP struct to text -// https://tools.ietf.org/html/rfc4566#section-5 -// Session description -// v= (protocol version) -// o= (originator and session identifier) -// s= (session name) -// i=* (session information) -// u=* (URI of description) -// e=* (email address) -// p=* (phone number) -// c=* (connection information -- not required if included in -// all media) -// b=* (zero or more bandwidth information lines) -// One or more time descriptions ("t=" and "r=" lines; see below) -// z=* (time zone adjustments) -// k=* (encryption key) -// a=* (zero or more session attribute lines) -// Zero or more media descriptions -// -// Time description -// t= (time the session is active) -// r=* (zero or more repeat times) -// -// Media description, if present -// m= (media name and transport address) -// i=* (media title) -// c=* (connection information -- optional if included at -// session level) -// b=* (zero or more bandwidth information lines) -// k=* (encryption key) -// a=* (zero or more media attribute lines) -func (s *SessionDescription) Marshal() (raw string) { - raw += keyValueBuild("v=", s.Version.String()) - raw += keyValueBuild("o=", s.Origin.String()) - raw += keyValueBuild("s=", s.SessionName.String()) - - if s.SessionInformation != nil { - raw += keyValueBuild("i=", s.SessionInformation.String()) - } - - if s.URI != nil { - uri := s.URI.String() - raw += keyValueBuild("u=", &uri) - } - - if s.EmailAddress != nil { - raw += keyValueBuild("e=", s.EmailAddress.String()) - } - - if s.PhoneNumber != nil { - raw += keyValueBuild("p=", s.PhoneNumber.String()) - } - - if s.ConnectionInformation != nil { - raw += keyValueBuild("c=", s.ConnectionInformation.String()) - } - - for _, b := range s.Bandwidth { - raw += keyValueBuild("b=", b.String()) - } - - for _, td := range s.TimeDescriptions { - raw += keyValueBuild("t=", td.Timing.String()) - for _, r := range td.RepeatTimes { - raw += keyValueBuild("r=", r.String()) - } - } - - rawTimeZones := make([]string, 0) - for _, z := range s.TimeZones { - rawTimeZones = append(rawTimeZones, z.String()) - } - - if len(rawTimeZones) > 0 { - timeZones := strings.Join(rawTimeZones, " ") - raw += keyValueBuild("z=", &timeZones) - } - - if s.EncryptionKey != nil { - raw += keyValueBuild("k=", s.EncryptionKey.String()) - } - - for _, a := range s.Attributes { - raw += keyValueBuild("a=", a.String()) - } - - for _, md := range s.MediaDescriptions { - raw += keyValueBuild("m=", md.MediaName.String()) - - if md.MediaTitle != nil { - raw += keyValueBuild("i=", md.MediaTitle.String()) - } - - if md.ConnectionInformation != nil { - raw += keyValueBuild("c=", md.ConnectionInformation.String()) - } - - for _, b := range md.Bandwidth { - raw += keyValueBuild("b=", b.String()) - } - - if md.EncryptionKey != nil { - raw += keyValueBuild("k=", md.EncryptionKey.String()) - } - - for _, a := range md.Attributes { - raw += keyValueBuild("a=", a.String()) - } - } - - return raw -} diff --git a/internal/sdp/marshal_test.go b/internal/sdp/marshal_test.go deleted file mode 100644 index 3cb342a3..00000000 --- a/internal/sdp/marshal_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package sdp - -import ( - "net" - "net/url" - "testing" -) - -const ( - CanonicalMarshalSDP = "v=0\r\n" + - "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + - "s=SDP Seminar\r\n" + - "i=A Seminar on the session description protocol\r\n" + - "u=http://www.example.com/seminars/sdp.pdf\r\n" + - "e=j.doe@example.com (Jane Doe)\r\n" + - "p=+1 617 555-6011\r\n" + - "c=IN IP4 224.2.17.12/127\r\n" + - "b=X-YZ:128\r\n" + - "b=AS:12345\r\n" + - "t=2873397496 2873404696\r\n" + - "t=3034423619 3042462419\r\n" + - "r=604800 3600 0 90000\r\n" + - "z=2882844526 -3600 2898848070 0\r\n" + - "k=prompt\r\n" + - "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + - "a=recvonly\r\n" + - "m=audio 49170 RTP/AVP 0\r\n" + - "i=Vivamus a posuere nisl\r\n" + - "c=IN IP4 203.0.113.1\r\n" + - "b=X-YZ:128\r\n" + - "k=prompt\r\n" + - "a=sendrecv\r\n" + - "m=video 51372 RTP/AVP 99\r\n" + - "a=rtpmap:99 h263-1998/90000\r\n" -) - -func TestMarshalCanonical(t *testing.T) { - sd := &SessionDescription{ - Version: 0, - Origin: Origin{ - Username: "jdoe", - SessionID: uint64(2890844526), - SessionVersion: uint64(2890842807), - NetworkType: "IN", - AddressType: "IP4", - UnicastAddress: "10.47.16.5", - }, - SessionName: "SDP Seminar", - SessionInformation: &(&struct{ x Information }{"A Seminar on the session description protocol"}).x, - URI: func() *url.URL { - uri, err := url.Parse("http://www.example.com/seminars/sdp.pdf") - if err != nil { - return nil - } - return uri - }(), - EmailAddress: &(&struct{ x EmailAddress }{"j.doe@example.com (Jane Doe)"}).x, - PhoneNumber: &(&struct{ x PhoneNumber }{"+1 617 555-6011"}).x, - ConnectionInformation: &ConnectionInformation{ - NetworkType: "IN", - AddressType: "IP4", - Address: &Address{ - IP: net.ParseIP("224.2.17.12"), - TTL: &(&struct{ x int }{127}).x, - }, - }, - Bandwidth: []Bandwidth{ - { - Experimental: true, - Type: "YZ", - Bandwidth: 128, - }, - { - Type: "AS", - Bandwidth: 12345, - }, - }, - TimeDescriptions: []TimeDescription{ - { - Timing: Timing{ - StartTime: 2873397496, - StopTime: 2873404696, - }, - RepeatTimes: nil, - }, - { - Timing: Timing{ - StartTime: 3034423619, - StopTime: 3042462419, - }, - RepeatTimes: []RepeatTime{ - { - Interval: 604800, - Duration: 3600, - Offsets: []int64{0, 90000}, - }, - }, - }, - }, - TimeZones: []TimeZone{ - { - AdjustmentTime: 2882844526, - Offset: -3600, - }, - { - AdjustmentTime: 2898848070, - Offset: 0, - }, - }, - EncryptionKey: &(&struct{ x EncryptionKey }{"prompt"}).x, - Attributes: []Attribute{ - NewAttribute("candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host", ""), - NewAttribute("recvonly", ""), - }, - MediaDescriptions: []*MediaDescription{ - { - MediaName: MediaName{ - Media: "audio", - Port: RangedPort{ - Value: 49170, - }, - Protos: []string{"RTP", "AVP"}, - Formats: []string{"0"}, - }, - MediaTitle: &(&struct{ x Information }{"Vivamus a posuere nisl"}).x, - ConnectionInformation: &ConnectionInformation{ - NetworkType: "IN", - AddressType: "IP4", - Address: &Address{ - IP: net.ParseIP("203.0.113.1"), - }, - }, - Bandwidth: []Bandwidth{ - { - Experimental: true, - Type: "YZ", - Bandwidth: 128, - }, - }, - EncryptionKey: &(&struct{ x EncryptionKey }{"prompt"}).x, - Attributes: []Attribute{ - NewAttribute("sendrecv", ""), - }, - }, - { - MediaName: MediaName{ - Media: "video", - Port: RangedPort{ - Value: 51372, - }, - Protos: []string{"RTP", "AVP"}, - Formats: []string{"99"}, - }, - Attributes: []Attribute{ - NewAttribute("rtpmap:99 h263-1998/90000", ""), - }, - }, - }, - } - - actual := sd.Marshal() - if actual != CanonicalMarshalSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", CanonicalMarshalSDP, actual) - } -} diff --git a/internal/sdp/media_description.go b/internal/sdp/media_description.go deleted file mode 100644 index c2d3e144..00000000 --- a/internal/sdp/media_description.go +++ /dev/null @@ -1,81 +0,0 @@ -package sdp - -import ( - "strconv" - "strings" -) - -// MediaDescription represents a media type. -// https://tools.ietf.org/html/rfc4566#section-5.14 -type MediaDescription struct { - // m= / ... - // https://tools.ietf.org/html/rfc4566#section-5.14 - MediaName MediaName - - // i= - // https://tools.ietf.org/html/rfc4566#section-5.4 - MediaTitle *Information - - // c= - // https://tools.ietf.org/html/rfc4566#section-5.7 - ConnectionInformation *ConnectionInformation - - // b=: - // https://tools.ietf.org/html/rfc4566#section-5.8 - Bandwidth []Bandwidth - - // k= - // k=: - // https://tools.ietf.org/html/rfc4566#section-5.12 - EncryptionKey *EncryptionKey - - // a= - // a=: - // https://tools.ietf.org/html/rfc4566#section-5.13 - Attributes []Attribute -} - -// Attribute returns the value of an attribute and if it exists -func (s *MediaDescription) Attribute(key string) (string, bool) { - for _, a := range s.Attributes { - if a.Key == key { - return a.Value, true - } - } - return "", false -} - -// RangedPort supports special format for the media field "m=" port value. If -// it may be necessary to specify multiple transport ports, the protocol allows -// to write it as: / where number of ports is a an -// offsetting range. -type RangedPort struct { - Value int - Range *int -} - -func (p *RangedPort) String() string { - output := strconv.Itoa(p.Value) - if p.Range != nil { - output += "/" + strconv.Itoa(*p.Range) - } - return output -} - -// MediaName describes the "m=" field storage structure. -type MediaName struct { - Media string - Port RangedPort - Protos []string - Formats []string -} - -func (m *MediaName) String() *string { - output := strings.Join([]string{ - m.Media, - m.Port.String(), - strings.Join(m.Protos, "/"), - strings.Join(m.Formats, " "), - }, " ") - return &output -} diff --git a/internal/sdp/session_description.go b/internal/sdp/session_description.go deleted file mode 100644 index 0d8d696e..00000000 --- a/internal/sdp/session_description.go +++ /dev/null @@ -1,151 +0,0 @@ -package sdp - -import ( - "fmt" - "net/url" - "strconv" -) - -// SessionDescription is a a well-defined format for conveying sufficient -// information to discover and participate in a multimedia session. -type SessionDescription struct { - // v=0 - // https://tools.ietf.org/html/rfc4566#section-5.1 - Version Version - - // o= - // https://tools.ietf.org/html/rfc4566#section-5.2 - Origin Origin - - // s= - // https://tools.ietf.org/html/rfc4566#section-5.3 - SessionName SessionName - - // i= - // https://tools.ietf.org/html/rfc4566#section-5.4 - SessionInformation *Information - - // u= - // https://tools.ietf.org/html/rfc4566#section-5.5 - URI *url.URL - - // e= - // https://tools.ietf.org/html/rfc4566#section-5.6 - EmailAddress *EmailAddress - - // p= - // https://tools.ietf.org/html/rfc4566#section-5.6 - PhoneNumber *PhoneNumber - - // c= - // https://tools.ietf.org/html/rfc4566#section-5.7 - ConnectionInformation *ConnectionInformation - - // b=: - // https://tools.ietf.org/html/rfc4566#section-5.8 - Bandwidth []Bandwidth - - // https://tools.ietf.org/html/rfc4566#section-5.9 - // https://tools.ietf.org/html/rfc4566#section-5.10 - TimeDescriptions []TimeDescription - - // z= ... - // https://tools.ietf.org/html/rfc4566#section-5.11 - TimeZones []TimeZone - - // k= - // k=: - // https://tools.ietf.org/html/rfc4566#section-5.12 - EncryptionKey *EncryptionKey - - // a= - // a=: - // https://tools.ietf.org/html/rfc4566#section-5.13 - Attributes []Attribute - - // https://tools.ietf.org/html/rfc4566#section-5.14 - MediaDescriptions []*MediaDescription -} - -// Attribute returns the value of an attribute and if it exists -func (s *SessionDescription) Attribute(key string) (string, bool) { - for _, a := range s.Attributes { - if a.Key == key { - return a.Value, true - } - } - return "", false -} - -// Version describes the value provided by the "v=" field which gives -// the version of the Session Description Protocol. -type Version int - -func (v *Version) String() *string { - output := strconv.Itoa(int(*v)) - return &output -} - -// Origin defines the structure for the "o=" field which provides the -// originator of the session plus a session identifier and version number. -type Origin struct { - Username string - SessionID uint64 - SessionVersion uint64 - NetworkType string - AddressType string - UnicastAddress string -} - -func (o *Origin) String() *string { - output := fmt.Sprintf( - "%v %d %d %v %v %v", - o.Username, - o.SessionID, - o.SessionVersion, - o.NetworkType, - o.AddressType, - o.UnicastAddress, - ) - return &output -} - -// SessionName describes a structured representations for the "s=" field -// and is the textual session name. -type SessionName string - -func (s *SessionName) String() *string { - output := string(*s) - return &output -} - -// EmailAddress describes a structured representations for the "e=" line -// which specifies email contact information for the person responsible for -// the conference. -type EmailAddress string - -func (e *EmailAddress) String() *string { - output := string(*e) - return &output -} - -// PhoneNumber describes a structured representations for the "p=" line -// specify phone contact information for the person responsible for the -// conference. -type PhoneNumber string - -func (p *PhoneNumber) String() *string { - output := string(*p) - return &output -} - -// TimeZone defines the structured object for "z=" line which describes -// repeated sessions scheduling. -type TimeZone struct { - AdjustmentTime uint64 - Offset int64 -} - -func (z *TimeZone) String() string { - return strconv.FormatUint(z.AdjustmentTime, 10) + " " + strconv.FormatInt(z.Offset, 10) -} diff --git a/internal/sdp/time_description.go b/internal/sdp/time_description.go deleted file mode 100644 index 0edfb1c1..00000000 --- a/internal/sdp/time_description.go +++ /dev/null @@ -1,52 +0,0 @@ -package sdp - -import ( - "strconv" - "strings" -) - -// TimeDescription describes "t=", "r=" fields of the session description -// which are used to specify the start and stop times for a session as well as -// repeat intervals and durations for the scheduled session. -type TimeDescription struct { - // t= - // https://tools.ietf.org/html/rfc4566#section-5.9 - Timing Timing - - // r= - // https://tools.ietf.org/html/rfc4566#section-5.10 - RepeatTimes []RepeatTime -} - -// Timing defines the "t=" field's structured representation for the start and -// stop times. -type Timing struct { - StartTime uint64 - StopTime uint64 -} - -func (t *Timing) String() *string { - output := strconv.FormatUint(t.StartTime, 10) - output += " " + strconv.FormatUint(t.StopTime, 10) - return &output -} - -// RepeatTime describes the "r=" fields of the session description which -// represents the intervals and durations for repeated scheduled sessions. -type RepeatTime struct { - Interval int64 - Duration int64 - Offsets []int64 -} - -func (r *RepeatTime) String() *string { - fields := make([]string, 0) - fields = append(fields, strconv.FormatInt(r.Interval, 10)) - fields = append(fields, strconv.FormatInt(r.Duration, 10)) - for _, value := range r.Offsets { - fields = append(fields, strconv.FormatInt(value, 10)) - } - - output := strings.Join(fields, " ") - return &output -} diff --git a/internal/sdp/unmarshal.go b/internal/sdp/unmarshal.go deleted file mode 100644 index 96ad786a..00000000 --- a/internal/sdp/unmarshal.go +++ /dev/null @@ -1,972 +0,0 @@ -package sdp - -import ( - "bufio" - "strings" - - "io" - "net" - "net/url" - "strconv" - - "github.com/pkg/errors" -) - -// Unmarshal is the primary function that deserializes the session description -// message and stores it inside of a structured SessionDescription object. -// -// The States Sransition Table describes the computation flow between functions -// (namely s1, s2, s3, ...) for a parsing procedure that complies with the -// specifications laid out by the rfc4566#section-5 as well as by JavaScript -// Session Establishment Protocol draft. Links: -// https://tools.ietf.org/html/rfc4566#section-5 -// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24 -// -// https://tools.ietf.org/html/rfc4566#section-5 -// Session description -// v= (protocol version) -// o= (originator and session identifier) -// s= (session name) -// i=* (session information) -// u=* (URI of description) -// e=* (email address) -// p=* (phone number) -// c=* (connection information -- not required if included in -// all media) -// b=* (zero or more bandwidth information lines) -// One or more time descriptions ("t=" and "r=" lines; see below) -// z=* (time zone adjustments) -// k=* (encryption key) -// a=* (zero or more session attribute lines) -// Zero or more media descriptions -// -// Time description -// t= (time the session is active) -// r=* (zero or more repeat times) -// -// Media description, if present -// m= (media name and transport address) -// i=* (media title) -// c=* (connection information -- optional if included at -// session level) -// b=* (zero or more bandwidth information lines) -// k=* (encryption key) -// a=* (zero or more media attribute lines) -// -// In order to generate the following state table and draw subsequent -// deterministic finite-state automota ("DFA") the following regex was used to -// derive the DFA: -// vosi?u?e?p?c?b*(tr*)+z?k?a*(mi?c?b*k?a*)* -// -// Please pay close attention to the `k`, and `a` parsing states. In the table -// below in order to distinguish between the states belonging to the media -// description as opposed to the session description, the states are marked -// with an asterisk ("a*", "k*"). -// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ -// | STATES | a* | a*,k* | a | a,k | b | b,c | e | i | m | o | p | r,t | s | t | u | v | z | -// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ -// | s1 | | | | | | | | | | | | | | | | 2 | | -// | s2 | | | | | | | | | | 3 | | | | | | | | -// | s3 | | | | | | | | | | | | | 4 | | | | | -// | s4 | | | | | | 5 | 6 | 7 | | | 8 | | | 9 | 10 | | | -// | s5 | | | | | 5 | | | | | | | | | 9 | | | | -// | s6 | | | | | | 5 | | | | | 8 | | | 9 | | | | -// | s7 | | | | | | 5 | 6 | | | | 8 | | | 9 | 10 | | | -// | s8 | | | | | | 5 | | | | | | | | 9 | | | | -// | s9 | | | | 11 | | | | | 12 | | | 9 | | | | | 13 | -// | s10 | | | | | | 5 | 6 | | | | 8 | | | 9 | | | | -// | s11 | | | 11 | | | | | | 12 | | | | | | | | | -// | s12 | | 14 | | | | 15 | | 16 | 12 | | | | | | | | | -// | s13 | | | | 11 | | | | | 12 | | | | | | | | | -// | s14 | 14 | | | | | | | | 12 | | | | | | | | | -// | s15 | | 14 | | | 15 | | | | 12 | | | | | | | | | -// | s16 | | 14 | | | | 15 | | | 12 | | | | | | | | | -// +--------+----+-------+----+-----+----+-----+---+----+----+---+---+-----+---+---+----+---+----+ -func (s *SessionDescription) Unmarshal(value string) error { - l := &lexer{ - desc: s, - input: bufio.NewReader(strings.NewReader(value)), - } - for state := s1; state != nil; { - var err error - state, err = state(l) - if err != nil { - return err - } - } - return nil - -} - -func s1(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - if key == "v=" { - return unmarshalProtocolVersion, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s2(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - if key == "o=" { - return unmarshalOrigin, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s3(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, err - } - - if key == "s=" { - return unmarshalSessionName, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s4(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - switch key { - case "i=": - return unmarshalSessionInformation, nil - case "u=": - return unmarshalURI, nil - case "e=": - return unmarshalEmail, nil - case "p=": - return unmarshalPhone, nil - case "c=": - return unmarshalSessionConnectionInformation, nil - case "b=": - return unmarshalSessionBandwidth, nil - case "t=": - return unmarshalTiming, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s5(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, err - } - - switch key { - case "b=": - return unmarshalSessionBandwidth, nil - case "t=": - return unmarshalTiming, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s6(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - switch key { - case "p=": - return unmarshalPhone, nil - case "c=": - return unmarshalSessionConnectionInformation, nil - case "b=": - return unmarshalSessionBandwidth, nil - case "t=": - return unmarshalTiming, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s7(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - switch key { - case "u=": - return unmarshalURI, nil - case "e=": - return unmarshalEmail, nil - case "p=": - return unmarshalPhone, nil - case "c=": - return unmarshalSessionConnectionInformation, nil - case "b=": - return unmarshalSessionBandwidth, nil - case "t=": - return unmarshalTiming, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s8(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - switch key { - case "c=": - return unmarshalSessionConnectionInformation, nil - case "b=": - return unmarshalSessionBandwidth, nil - case "t=": - return unmarshalTiming, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s9(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "z=": - return unmarshalTimeZones, nil - case "k=": - return unmarshalSessionEncryptionKey, nil - case "a=": - return unmarshalSessionAttribute, nil - case "r=": - return unmarshalRepeatTimes, nil - case "t=": - return unmarshalTiming, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s10(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - switch key { - case "e=": - return unmarshalEmail, nil - case "p=": - return unmarshalPhone, nil - case "c=": - return unmarshalSessionConnectionInformation, nil - case "b=": - return unmarshalSessionBandwidth, nil - case "t=": - return unmarshalTiming, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s11(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "a=": - return unmarshalSessionAttribute, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s12(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "a=": - return unmarshalMediaAttribute, nil - case "k=": - return unmarshalMediaEncryptionKey, nil - case "b=": - return unmarshalMediaBandwidth, nil - case "c=": - return unmarshalMediaConnectionInformation, nil - case "i=": - return unmarshalMediaTitle, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s13(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "a=": - return unmarshalSessionAttribute, nil - case "k=": - return unmarshalSessionEncryptionKey, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s14(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "a=": - return unmarshalMediaAttribute, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s15(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "a=": - return unmarshalMediaAttribute, nil - case "k=": - return unmarshalMediaEncryptionKey, nil - case "b=": - return unmarshalMediaBandwidth, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func s16(l *lexer) (stateFn, error) { - key, err := readType(l.input) - if err != nil { - if err == io.EOF && key == "" { - return nil, nil - } - return nil, err - } - - switch key { - case "a=": - return unmarshalMediaAttribute, nil - case "k=": - return unmarshalMediaEncryptionKey, nil - case "c=": - return unmarshalMediaConnectionInformation, nil - case "b=": - return unmarshalMediaBandwidth, nil - case "m=": - return unmarshalMediaDescription, nil - } - - return nil, errors.Errorf("sdp: invalid syntax `%v`", key) -} - -func unmarshalProtocolVersion(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - version, err := strconv.ParseInt(value, 10, 32) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", version) - } - - // As off the latest draft of the rfc this value is required to be 0. - // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-24#section-5.8.1 - if version != 0 { - return nil, errors.Errorf("sdp: invalid value `%v`", version) - } - - return s2, nil -} - -func unmarshalOrigin(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - fields := strings.Fields(value) - if len(fields) != 6 { - return nil, errors.Errorf("sdp: invalid syntax `o=%v`", fields) - } - - sessionID, err := strconv.ParseUint(fields[1], 10, 64) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", fields[1]) - } - - sessionVersion, err := strconv.ParseUint(fields[2], 10, 64) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", fields[2]) - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.6 - if i := indexOf(fields[3], []string{"IN"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[3]) - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.7 - if i := indexOf(fields[4], []string{"IP4", "IP6"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[4]) - } - - // TODO validated UnicastAddress - - l.desc.Origin = Origin{ - Username: fields[0], - SessionID: sessionID, - SessionVersion: sessionVersion, - NetworkType: fields[3], - AddressType: fields[4], - UnicastAddress: fields[5], - } - - return s3, nil -} - -func unmarshalSessionName(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - l.desc.SessionName = SessionName(value) - return s4, nil -} - -func unmarshalSessionInformation(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - sessionInformation := Information(value) - l.desc.SessionInformation = &sessionInformation - return s7, nil -} - -func unmarshalURI(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - l.desc.URI, err = url.Parse(value) - if err != nil { - return nil, err - } - - return s10, nil -} - -func unmarshalEmail(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - emailAddress := EmailAddress(value) - l.desc.EmailAddress = &emailAddress - return s6, nil -} - -func unmarshalPhone(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - phoneNumber := PhoneNumber(value) - l.desc.PhoneNumber = &phoneNumber - return s8, nil -} - -func unmarshalSessionConnectionInformation(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - l.desc.ConnectionInformation, err = unmarshalConnectionInformation(value) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `c=%v`", value) - } - return s5, nil -} - -func unmarshalConnectionInformation(value string) (*ConnectionInformation, error) { - fields := strings.Fields(value) - if len(fields) < 2 { - return nil, errors.Errorf("sdp: invalid syntax `c=%v`", fields) - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.6 - if i := indexOf(fields[0], []string{"IN"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[0]) - } - - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-8.2.7 - if i := indexOf(fields[1], []string{"IP4", "IP6"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[1]) - } - - var connAddr *Address - if len(fields) > 2 { - connAddr = &Address{} - - parts := strings.Split(fields[2], "/") - connAddr.IP = net.ParseIP(parts[0]) - if connAddr.IP == nil { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[2]) - } - - isIP6 := connAddr.IP.To4() == nil - if len(parts) > 1 { - val, err := strconv.ParseInt(parts[1], 10, 32) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", fields[2]) - } - - if isIP6 { - multi := int(val) - connAddr.Range = &multi - } else { - ttl := int(val) - connAddr.TTL = &ttl - } - } - - if len(parts) > 2 { - val, err := strconv.ParseInt(parts[2], 10, 32) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", fields[2]) - } - - multi := int(val) - connAddr.Range = &multi - } - - } - - return &ConnectionInformation{ - NetworkType: fields[0], - AddressType: fields[1], - Address: connAddr, - }, nil -} - -func unmarshalSessionBandwidth(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - bandwidth, err := unmarshalBandwidth(value) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `b=%v`", value) - } - l.desc.Bandwidth = append(l.desc.Bandwidth, *bandwidth) - - return s5, nil -} - -func unmarshalBandwidth(value string) (*Bandwidth, error) { - parts := strings.Split(value, ":") - if len(parts) != 2 { - return nil, errors.Errorf("sdp: invalid syntax `b=%v`", parts) - } - - experimental := strings.HasPrefix(parts[0], "X-") - if experimental { - parts[0] = strings.TrimPrefix(parts[0], "X-") - } else { - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.8 - if i := indexOf(parts[0], []string{"CT", "AS"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", parts[0]) - } - } - - bandwidth, err := strconv.ParseUint(parts[1], 10, 64) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", parts[1]) - } - - return &Bandwidth{ - Experimental: experimental, - Type: parts[0], - Bandwidth: bandwidth, - }, nil -} - -func unmarshalTiming(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - fields := strings.Fields(value) - if len(fields) < 2 { - return nil, errors.Errorf("sdp: invalid syntax `t=%v`", fields) - } - - td := TimeDescription{} - - td.Timing.StartTime, err = strconv.ParseUint(fields[0], 10, 64) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", fields[1]) - } - - td.Timing.StopTime, err = strconv.ParseUint(fields[1], 10, 64) - if err != nil { - return nil, errors.Errorf("sdp: invalid numeric value `%v`", fields[1]) - } - - l.desc.TimeDescriptions = append(l.desc.TimeDescriptions, td) - - return s9, nil -} - -func unmarshalRepeatTimes(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - fields := strings.Fields(value) - if len(fields) < 3 { - return nil, errors.Errorf("sdp: invalid syntax `r=%v`", fields) - } - - latestTimeDesc := &l.desc.TimeDescriptions[len(l.desc.TimeDescriptions)-1] - - newRepeatTime := RepeatTime{} - newRepeatTime.Interval, err = parseTimeUnits(fields[0]) - if err != nil { - return nil, errors.Errorf("sdp: invalid value `%v`", fields) - } - - newRepeatTime.Duration, err = parseTimeUnits(fields[1]) - if err != nil { - return nil, errors.Errorf("sdp: invalid value `%v`", fields) - } - - for i := 2; i < len(fields); i++ { - offset, err := parseTimeUnits(fields[i]) - if err != nil { - return nil, errors.Errorf("sdp: invalid value `%v`", fields) - } - newRepeatTime.Offsets = append(newRepeatTime.Offsets, offset) - } - latestTimeDesc.RepeatTimes = append(latestTimeDesc.RepeatTimes, newRepeatTime) - - return s9, nil -} - -func unmarshalTimeZones(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - // These fields are transimitted in pairs - // z= .... - // so we are making sure that there are actually multiple of 2 total. - fields := strings.Fields(value) - if len(fields)%2 != 0 { - return nil, errors.Errorf("sdp: invalid syntax `t=%v`", fields) - } - - for i := 0; i < len(fields); i += 2 { - var timeZone TimeZone - - timeZone.AdjustmentTime, err = strconv.ParseUint(fields[i], 10, 64) - if err != nil { - return nil, errors.Errorf("sdp: invalid value `%v`", fields) - } - - timeZone.Offset, err = parseTimeUnits(fields[i+1]) - if err != nil { - return nil, err - } - - l.desc.TimeZones = append(l.desc.TimeZones, timeZone) - } - - return s13, nil -} - -func unmarshalSessionEncryptionKey(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - encryptionKey := EncryptionKey(value) - l.desc.EncryptionKey = &encryptionKey - return s11, nil -} - -func unmarshalSessionAttribute(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - i := strings.IndexRune(value, ':') - var a Attribute - if i > 0 { - a = NewAttribute(value[:i], value[i+1:]) - } else { - a = NewPropertyAttribute(value) - } - - l.desc.Attributes = append(l.desc.Attributes, a) - return s11, nil -} - -func unmarshalMediaDescription(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - fields := strings.Fields(value) - if len(fields) < 4 { - return nil, errors.Errorf("sdp: invalid syntax `m=%v`", fields) - } - - newMediaDesc := &MediaDescription{} - - // - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.14 - if i := indexOf(fields[0], []string{"audio", "video", "text", "application", "message"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[0]) - } - newMediaDesc.MediaName.Media = fields[0] - - // - parts := strings.Split(fields[1], "/") - newMediaDesc.MediaName.Port.Value, err = parsePort(parts[0]) - if err != nil { - return nil, errors.Errorf("sdp: invalid port value `%v`", parts[0]) - } - - if len(parts) > 1 { - portRange, err := strconv.Atoi(parts[1]) - if err != nil { - return nil, errors.Errorf("sdp: invalid value `%v`", parts) - } - newMediaDesc.MediaName.Port.Range = &portRange - } - - // - // Set according to currently registered with IANA - // https://tools.ietf.org/html/rfc4566#section-5.14 - for _, proto := range strings.Split(fields[2], "/") { - if i := indexOf(proto, []string{"UDP", "RTP", "AVP", "SAVP", "SAVPF", "TLS", "DTLS", "SCTP"}); i == -1 { - return nil, errors.Errorf("sdp: invalid value `%v`", fields[2]) - } - newMediaDesc.MediaName.Protos = append(newMediaDesc.MediaName.Protos, proto) - } - - // ... - for i := 3; i < len(fields); i++ { - newMediaDesc.MediaName.Formats = append(newMediaDesc.MediaName.Formats, fields[i]) - } - - l.desc.MediaDescriptions = append(l.desc.MediaDescriptions, newMediaDesc) - - return s12, nil -} - -func unmarshalMediaTitle(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] - mediaTitle := Information(value) - latestMediaDesc.MediaTitle = &mediaTitle - return s16, nil -} - -func unmarshalMediaConnectionInformation(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] - latestMediaDesc.ConnectionInformation, err = unmarshalConnectionInformation(value) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `c=%v`", value) - } - return s15, nil -} - -func unmarshalMediaBandwidth(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] - bandwidth, err := unmarshalBandwidth(value) - if err != nil { - return nil, errors.Errorf("sdp: invalid syntax `b=%v`", value) - } - latestMediaDesc.Bandwidth = append(latestMediaDesc.Bandwidth, *bandwidth) - return s15, nil -} - -func unmarshalMediaEncryptionKey(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] - encryptionKey := EncryptionKey(value) - latestMediaDesc.EncryptionKey = &encryptionKey - return s14, nil -} - -func unmarshalMediaAttribute(l *lexer) (stateFn, error) { - value, err := readValue(l.input) - if err != nil { - return nil, err - } - - i := strings.IndexRune(value, ':') - var a Attribute - if i > 0 { - a = NewAttribute(value[:i], value[i+1:]) - } else { - a = NewPropertyAttribute(value) - } - - latestMediaDesc := l.desc.MediaDescriptions[len(l.desc.MediaDescriptions)-1] - latestMediaDesc.Attributes = append(latestMediaDesc.Attributes, a) - return s14, nil -} - -func parseTimeUnits(value string) (int64, error) { - // Some time offsets in the protocol can be provided with a shorthand - // notation. This code ensures to convert it to NTP timestamp format. - // d - days (86400 seconds) - // h - hours (3600 seconds) - // m - minutes (60 seconds) - // s - seconds (allowed for completeness) - switch value[len(value)-1:] { - case "d": - num, err := strconv.ParseInt(value[:len(value)-1], 10, 64) - if err != nil { - return 0, errors.Errorf("sdp: invalid value `%v`", value) - } - return num * 86400, nil - case "h": - num, err := strconv.ParseInt(value[:len(value)-1], 10, 64) - if err != nil { - return 0, errors.Errorf("sdp: invalid value `%v`", value) - } - return num * 3600, nil - case "m": - num, err := strconv.ParseInt(value[:len(value)-1], 10, 64) - if err != nil { - return 0, errors.Errorf("sdp: invalid value `%v`", value) - } - return num * 60, nil - } - - num, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return 0, errors.Errorf("sdp: invalid value `%v`", value) - } - - return num, nil -} - -func parsePort(value string) (int, error) { - port, err := strconv.Atoi(value) - if err != nil { - return 0, errors.Errorf("sdp: invalid port value `%v`", port) - } - - if port < 0 || port > 65536 { - return 0, errors.Errorf("sdp: invalid port value -- out of range `%v`", port) - } - - return port, nil -} diff --git a/internal/sdp/unmarshal_test.go b/internal/sdp/unmarshal_test.go deleted file mode 100644 index 1aed92a6..00000000 --- a/internal/sdp/unmarshal_test.go +++ /dev/null @@ -1,315 +0,0 @@ -package sdp - -import ( - "testing" -) - -const ( - BaseSDP = "v=0\r\n" + - "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + - "s=SDP Seminar\r\n" - - SessionInformationSDP = BaseSDP + - "i=A Seminar on the session description protocol\r\n" + - "t=3034423619 3042462419\r\n" - - URISDP = BaseSDP + - "u=http://www.example.com/seminars/sdp.pdf\r\n" + - "t=3034423619 3042462419\r\n" - - EmailAddressSDP = BaseSDP + - "e=j.doe@example.com (Jane Doe)\r\n" + - "t=3034423619 3042462419\r\n" - - PhoneNumberSDP = BaseSDP + - "p=+1 617 555-6011\r\n" + - "t=3034423619 3042462419\r\n" - - SessionConnectionInformationSDP = BaseSDP + - "c=IN IP4 224.2.17.12/127\r\n" + - "t=3034423619 3042462419\r\n" - - SessionBandwidthSDP = BaseSDP + - "b=X-YZ:128\r\n" + - "b=AS:12345\r\n" + - "t=3034423619 3042462419\r\n" - - TimingSDP = BaseSDP + - "t=2873397496 2873404696\r\n" - - // Short hand time notation is converted into NTP timestamp format in - // seconds. Because of that unittest comparisons will fail as the same time - // will be expressed in different units. - RepeatTimesSDP = TimingSDP + - "r=604800 3600 0 90000\r\n" + - "r=3d 2h 0 21h\r\n" - - RepeatTimesSDPExpected = TimingSDP + - "r=604800 3600 0 90000\r\n" + - "r=259200 7200 0 75600\r\n" - - // The expected value looks a bit different for the same reason as mentioned - // above regarding RepeatTimes. - TimeZonesSDP = TimingSDP + - "r=2882844526 -1h 2898848070 0\r\n" - - TimeZonesSDPExpected = TimingSDP + - "r=2882844526 -3600 2898848070 0\r\n" - - SessionEncryptionKeySDP = TimingSDP + - "k=prompt\r\n" - - SessionAttributesSDP = TimingSDP + - "a=rtpmap:96 opus/48000\r\n" - - MediaNameSDP = TimingSDP + - "m=video 51372 RTP/AVP 99\r\n" + - "m=audio 54400 RTP/SAVPF 0 96\r\n" - - MediaTitleSDP = MediaNameSDP + - "i=Vivamus a posuere nisl\r\n" - - MediaConnectionInformationSDP = MediaNameSDP + - "c=IN IP4 203.0.113.1\r\n" - - MediaBandwidthSDP = MediaNameSDP + - "b=X-YZ:128\r\n" + - "b=AS:12345\r\n" - - MediaEncryptionKeySDP = MediaNameSDP + - "k=prompt\r\n" - - MediaAttributesSDP = MediaNameSDP + - "a=rtpmap:99 h263-1998/90000\r\n" + - "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" - - CanonicalUnmarshalSDP = "v=0\r\n" + - "o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r\n" + - "s=SDP Seminar\r\n" + - "i=A Seminar on the session description protocol\r\n" + - "u=http://www.example.com/seminars/sdp.pdf\r\n" + - "e=j.doe@example.com (Jane Doe)\r\n" + - "p=+1 617 555-6011\r\n" + - "c=IN IP4 224.2.17.12/127\r\n" + - "b=X-YZ:128\r\n" + - "b=AS:12345\r\n" + - "t=2873397496 2873404696\r\n" + - "t=3034423619 3042462419\r\n" + - "r=604800 3600 0 90000\r\n" + - "z=2882844526 -3600 2898848070 0\r\n" + - "k=prompt\r\n" + - "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host\r\n" + - "a=recvonly\r\n" + - "m=audio 49170 RTP/AVP 0\r\n" + - "i=Vivamus a posuere nisl\r\n" + - "c=IN IP4 203.0.113.1\r\n" + - "b=X-YZ:128\r\n" + - "k=prompt\r\n" + - "a=sendrecv\r\n" + - "m=video 51372 RTP/AVP 99\r\n" + - "a=rtpmap:99 h263-1998/90000\r\n" -) - -func TestUnmarshalSessionInformation(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(SessionInformationSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != SessionInformationSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", SessionInformationSDP, actual) - } -} - -func TestUnmarshalURI(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(URISDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != URISDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", URISDP, actual) - } -} - -func TestUnmarshalEmailAddress(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(EmailAddressSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != EmailAddressSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", EmailAddressSDP, actual) - } -} - -func TestUnmarshalPhoneNumber(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(PhoneNumberSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != PhoneNumberSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", PhoneNumberSDP, actual) - } -} - -func TestUnmarshalSessionConnectionInformation(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(SessionConnectionInformationSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != SessionConnectionInformationSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", SessionConnectionInformationSDP, actual) - } -} - -func TestUnmarshalSessionBandwidth(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(SessionBandwidthSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != SessionBandwidthSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", SessionBandwidthSDP, actual) - } -} - -func TestUnmarshalRepeatTimes(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(RepeatTimesSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != RepeatTimesSDPExpected { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", RepeatTimesSDPExpected, actual) - } -} - -func TestUnmarshalTimeZones(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(TimeZonesSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != TimeZonesSDPExpected { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", TimeZonesSDPExpected, actual) - } -} - -func TestUnmarshalSessionEncryptionKey(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(SessionEncryptionKeySDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != SessionEncryptionKeySDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", SessionEncryptionKeySDP, actual) - } -} - -func TestUnmarshalSessionAttributes(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(SessionAttributesSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != SessionAttributesSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", SessionAttributesSDP, actual) - } -} - -func TestUnmarshalMediaName(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(MediaNameSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != MediaNameSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", MediaNameSDP, actual) - } -} - -func TestUnmarshalMediaTitle(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(MediaTitleSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != MediaTitleSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", MediaTitleSDP, actual) - } -} - -func TestUnmarshalMediaConnectionInformation(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(MediaConnectionInformationSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != MediaConnectionInformationSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", MediaConnectionInformationSDP, actual) - } -} - -func TestUnmarshalMediaBandwidth(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(MediaBandwidthSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != MediaBandwidthSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", MediaBandwidthSDP, actual) - } -} - -func TestUnmarshalMediaEncryptionKey(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(MediaEncryptionKeySDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != MediaEncryptionKeySDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", MediaEncryptionKeySDP, actual) - } -} - -func TestUnmarshalMediaAttributes(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(MediaAttributesSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != MediaAttributesSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", MediaAttributesSDP, actual) - } -} - -func TestUnmarshalCanonical(t *testing.T) { - sd := &SessionDescription{} - if err := sd.Unmarshal(CanonicalUnmarshalSDP); err != nil { - t.Errorf("error: %v", err) - } - - actual := sd.Marshal() - if actual != CanonicalUnmarshalSDP { - t.Errorf("error:\n\nEXPECTED:\n%v\nACTUAL:\n%v", CanonicalUnmarshalSDP, actual) - } -} diff --git a/internal/sdp/util.go b/internal/sdp/util.go deleted file mode 100644 index 87a73c0d..00000000 --- a/internal/sdp/util.go +++ /dev/null @@ -1,168 +0,0 @@ -package sdp - -import ( - "bufio" - "fmt" - "io" - "math/rand" - "strconv" - "strings" - - "time" - - "github.com/pkg/errors" -) - -// ConnectionRole indicates which of the end points should initiate the connection establishment -type ConnectionRole int - -const ( - // ConnectionRoleActive indicates the endpoint will initiate an outgoing connection. - ConnectionRoleActive ConnectionRole = iota + 1 - - // ConnectionRolePassive indicates the endpoint will accept an incoming connection. - ConnectionRolePassive - - // ConnectionRoleActpass indicates the endpoint is willing to accept an incoming connection or to initiate an outgoing connection. - ConnectionRoleActpass - - // ConnectionRoleHoldconn indicates the endpoint does not want the connection to be established for the time being. - ConnectionRoleHoldconn -) - -func (t ConnectionRole) String() string { - switch t { - case ConnectionRoleActive: - return "active" - case ConnectionRolePassive: - return "passive" - case ConnectionRoleActpass: - return "actpass" - case ConnectionRoleHoldconn: - return "holdconn" - default: - return "Unknown" - } -} - -func newSessionID() uint64 { - r := rand.New(rand.NewSource(time.Now().UnixNano())) - return uint64(r.Uint32()*2) >> 2 -} - -// Codec represents a codec -type Codec struct { - PayloadType uint8 - Name string - ClockRate uint32 - EncodingParameters string - Fmtp string -} - -func (c Codec) String() string { - return fmt.Sprintf("%d %s/%d/%s", c.PayloadType, c.Name, c.ClockRate, c.EncodingParameters) -} - -// GetCodecForPayloadType scans the SessionDescription for the given payloadType and returns the codec -func (s *SessionDescription) GetCodecForPayloadType(payloadType uint8) (Codec, error) { - codec := Codec{ - PayloadType: payloadType, - } - - found := false - payloadTypeString := strconv.Itoa(int(payloadType)) - rtpmapPrefix := "rtpmap:" + payloadTypeString - fmtpPrefix := "fmtp:" + payloadTypeString - - for _, m := range s.MediaDescriptions { - for _, a := range m.Attributes { - if strings.HasPrefix(*a.String(), rtpmapPrefix) { - found = true - // a=rtpmap: / [/] - split := strings.Split(*a.String(), " ") - if len(split) == 2 { - split = strings.Split(split[1], "/") - codec.Name = split[0] - parts := len(split) - if parts > 1 { - rate, err := strconv.Atoi(split[1]) - if err != nil { - return codec, err - } - codec.ClockRate = uint32(rate) - } - if parts > 2 { - codec.EncodingParameters = split[2] - } - } - } else if strings.HasPrefix(*a.String(), fmtpPrefix) { - // a=fmtp: - split := strings.Split(*a.String(), " ") - if len(split) == 2 { - codec.Fmtp = split[1] - } - } - } - if found { - return codec, nil - } - } - return codec, errors.New("payload type not found") -} - -type lexer struct { - desc *SessionDescription - input *bufio.Reader -} - -type stateFn func(*lexer) (stateFn, error) - -func readType(input *bufio.Reader) (string, error) { - key, err := input.ReadString('=') - if err != nil { - return key, err - } - - if len(key) != 2 { - return key, errors.Errorf("sdp: invalid syntax `%v`", key) - } - - return key, nil -} - -func readValue(input *bufio.Reader) (string, error) { - line, err := input.ReadString('\n') - if err != nil && err != io.EOF { - return line, err - } - - if len(line) == 0 { - return line, nil - } - - if line[len(line)-1] == '\n' { - drop := 1 - if len(line) > 1 && line[len(line)-2] == '\r' { - drop = 2 - } - line = line[:len(line)-drop] - } - - return line, nil -} - -func indexOf(element string, data []string) int { - for k, v := range data { - if element == v { - return k - } - } - return -1 -} - -func keyValueBuild(key string, value *string) string { - if value != nil { - return key + *value + "\r\n" - } - return "" -} diff --git a/mediaengine.go b/mediaengine.go index a5d7636c..fd967dfb 100644 --- a/mediaengine.go +++ b/mediaengine.go @@ -3,7 +3,7 @@ package webrtc import ( "strconv" - "github.com/pions/webrtc/internal/sdp" + "github.com/pions/sdp" "github.com/pions/webrtc/pkg/rtp" "github.com/pions/webrtc/pkg/rtp/codecs" "github.com/pkg/errors" diff --git a/rtcpeerconnection.go b/rtcpeerconnection.go index cc1df1af..09b6f438 100644 --- a/rtcpeerconnection.go +++ b/rtcpeerconnection.go @@ -14,8 +14,8 @@ import ( "encoding/binary" "github.com/pions/datachannel" + "github.com/pions/sdp" "github.com/pions/webrtc/internal/network" - "github.com/pions/webrtc/internal/sdp" "github.com/pions/webrtc/pkg/ice" "github.com/pions/webrtc/pkg/media" "github.com/pions/webrtc/pkg/rtcerr" diff --git a/rtcsessiondescription.go b/rtcsessiondescription.go index 5c5cd5fb..170ac31b 100644 --- a/rtcsessiondescription.go +++ b/rtcsessiondescription.go @@ -1,7 +1,7 @@ package webrtc import ( - "github.com/pions/webrtc/internal/sdp" + "github.com/pions/sdp" ) // RTCSessionDescription is used to expose local and remote session descriptions.