mirror of
https://github.com/pion/webrtc.git
synced 2025-10-29 01:42:59 +08:00
1
go.mod
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user