Move SDP to an external package

Resolves #322
This commit is contained in:
Sean DuBois
2018-12-27 20:15:46 -08:00
parent 114de5e33f
commit 29d004ca75
16 changed files with 6 additions and 2373 deletions

1
go.mod
View File

@@ -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

2
go.sum
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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=<media> <port>/<number of ports> <proto> <fmt> ...
// https://tools.ietf.org/html/rfc4566#section-5.14
MediaName MediaName
// i=<session description>
// https://tools.ietf.org/html/rfc4566#section-5.4
MediaTitle *Information
// c=<nettype> <addrtype> <connection-address>
// https://tools.ietf.org/html/rfc4566#section-5.7
ConnectionInformation *ConnectionInformation
// b=<bwtype>:<bandwidth>
// https://tools.ietf.org/html/rfc4566#section-5.8
Bandwidth []Bandwidth
// k=<method>
// k=<method>:<encryption key>
// https://tools.ietf.org/html/rfc4566#section-5.12
EncryptionKey *EncryptionKey
// a=<attribute>
// a=<attribute>:<value>
// 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: <port>/<number of ports> 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
}

View File

@@ -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=<username> <sess-id> <sess-version> <nettype> <addrtype> <unicast-address>
// https://tools.ietf.org/html/rfc4566#section-5.2
Origin Origin
// s=<session name>
// https://tools.ietf.org/html/rfc4566#section-5.3
SessionName SessionName
// i=<session description>
// https://tools.ietf.org/html/rfc4566#section-5.4
SessionInformation *Information
// u=<uri>
// https://tools.ietf.org/html/rfc4566#section-5.5
URI *url.URL
// e=<email-address>
// https://tools.ietf.org/html/rfc4566#section-5.6
EmailAddress *EmailAddress
// p=<phone-number>
// https://tools.ietf.org/html/rfc4566#section-5.6
PhoneNumber *PhoneNumber
// c=<nettype> <addrtype> <connection-address>
// https://tools.ietf.org/html/rfc4566#section-5.7
ConnectionInformation *ConnectionInformation
// b=<bwtype>:<bandwidth>
// 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=<adjustment time> <offset> <adjustment time> <offset> ...
// https://tools.ietf.org/html/rfc4566#section-5.11
TimeZones []TimeZone
// k=<method>
// k=<method>:<encryption key>
// https://tools.ietf.org/html/rfc4566#section-5.12
EncryptionKey *EncryptionKey
// a=<attribute>
// a=<attribute>:<value>
// 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)
}

View File

@@ -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=<start-time> <stop-time>
// https://tools.ietf.org/html/rfc4566#section-5.9
Timing Timing
// r=<repeat interval> <active duration> <offsets from start-time>
// 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
}

View File

@@ -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=<adjustment time> <offset> <adjustment time> <offset> ....
// 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{}
// <media>
// 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]
// <port>
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
}
// <proto>
// 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)
}
// <fmt>...
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
}

View File

@@ -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)
}
}

View File

@@ -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:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
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:<format> <format specific parameters>
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 ""
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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.