You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

456 lines
12 KiB

// Session Description Protocol Library
//
// This is the stuff people embed in SIP packets that tells you how to
// establish audio and/or video sessions.
//
// Here's a typical SDP for a phone call sent by Asterisk:
//
// v=0
// o=root 31589 31589 IN IP4 10.0.0.38
// s=session
// c=IN IP4 10.0.0.38 <-- ip we should connect to
// t=0 0
// m=audio 30126 RTP/AVP 0 101 <-- audio port number and codecs
// a=rtpmap:0 PCMU/8000 <-- use μ-Law codec at 8000 hz
// a=rtpmap:101 telephone-event/8000 <-- they support rfc2833 dtmf tones
// a=fmtp:101 0-16
// a=silenceSupp:off - - - - <-- they'll freak out if you use VAD
// a=ptime:20 <-- send packet every 20 milliseconds
// a=sendrecv <-- they wanna send and receive audio
//
// Here's an SDP response from MetaSwitch, meaning the exact same
// thing as above, but omitting fields we're smart enough to assume:
//
// v=0
// o=- 3366701332 3366701332 IN IP4 1.2.3.4
// s=-
// c=IN IP4 1.2.3.4
// t=0 0
// m=audio 32898 RTP/AVP 0 101
// a=rtpmap:101 telephone-event/8000
// a=ptime:20
//
// If you wanted to go where no woman or man has gone before in the
// voip world, like stream 44.1khz stereo MP3 audio over a IPv6 TCP
// socket for a Flash player to connect to, you could do something
// like:
//
// v=0
// o=- 3366701332 3366701332 IN IP6 dead:beef::666
// s=-
// c=IN IP6 dead:beef::666
// t=0 0
// m=audio 80 TCP/IP 111
// a=rtpmap:111 MP3/44100/2
// a=sendonly
//
// Reference Material:
//
// - SDP RFC: http://tools.ietf.org/html/rfc4566
// - SIP/SDP Handshake RFC: http://tools.ietf.org/html/rfc3264
//
package sdp
import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"log"
"math/rand"
"net"
"strconv"
"strings"
)
const (
MaxLength = 1450
)
// SDP represents a Session Description Protocol SIP payload.
type SDP struct {
SendOnly bool // true if 'a=sendonly' was specified in sdp
RecvOnly bool // true if 'a=recvonly' was specified in sdp
Ptime int // transmit every X millisecs (0 means not set)
Addr string // connect to this ip; never blank (from c=)
Audio *Media // non nil if we can establish audio
Video *Media // non nil if we can establish video
Origin Origin // this must always be present
Session string // s= Session Name (defaults to "-")
Time string // t= Active Time (defaults to "0 0")
Attrs [][2]string // a= lines we don't recognize (never nil)
}
// Easy way to create a basic, everyday SDP for VoIP.
func New(addr *net.UDPAddr, codecs ...*Codec) *SDP {
sdp := new(SDP)
sdp.Addr = addr.IP.String()
sdp.Origin.ID = strconv.FormatInt(int64(rand.Uint32()), 10)
sdp.Origin.Version = sdp.Origin.ID
sdp.Origin.Addr = sdp.Addr
sdp.Audio = new(Media)
sdp.Audio.Type = "audio"
sdp.Audio.Proto = "RTP/AVP"
sdp.Audio.Port = addr.Port
sdp.Audio.Codecs = make([]Codec, len(codecs))
for i := 0; i < len(codecs); i++ {
sdp.Audio.Codecs[i] = *codecs[i]
}
sdp.Attrs = make([][2]string, 0, 8)
return sdp
}
// parses sdp message text into a happy data structure
func Parse(s string) (sdp *SDP, err error) {
sdp = new(SDP)
sdp.Session = "pokémon"
sdp.Time = "0 0"
// Eat version.
if !strings.HasPrefix(s, "v=0\r\n") {
return nil, errors.New("sdp must start with v=0\\r\\n")
}
s = s[5:]
// Turn into lines.
lines := strings.Split(s, "\r\n")
if lines == nil || len(lines) < 2 {
return nil, errors.New("too few lines in sdp")
}
// We abstract the structure of the media lines so we need a place to store
// them before assembling the audio/video data structures.
var audioinfo, videoinfo string
rtpmaps := make([]string, len(lines))
rtpmapcnt := 0
fmtps := make([]string, len(lines))
fmtpcnt := 0
sdp.Attrs = make([][2]string, 0, len(lines))
// Extract information from SDP.
var okOrigin, okConn bool
for _, line := range lines {
switch {
case line == "":
continue
case len(line) < 3 || line[1] != '=': // empty or invalid line
log.Println("Bad line in SDP:", line)
continue
case line[0] == 'm': // media line
line = line[2:]
if strings.HasPrefix(line, "audio ") {
audioinfo = line[6:]
} else if strings.HasPrefix(line, "video ") {
videoinfo = line[6:]
} else {
log.Println("Unsupported SDP media line:", line)
}
case line[0] == 's': // session line
sdp.Session = line[2:]
case line[0] == 't': // active time
sdp.Time = line[2:]
case line[0] == 'c': // connect to this ip address
if okConn {
log.Println("Dropping extra c= line in sdp:", line)
continue
}
sdp.Addr, err = parseConnLine(line)
if err != nil {
return nil, err
}
okConn = true
case line[0] == 'o': // origin line
err = parseOriginLine(&sdp.Origin, line)
if err != nil {
return nil, err
}
okOrigin = true
case line[0] == 'a': // attribute lines
line = line[2:]
switch {
case strings.HasPrefix(line, "rtpmap:"):
rtpmaps[rtpmapcnt] = line[7:]
rtpmapcnt++
case strings.HasPrefix(line, "fmtp:"):
fmtps[fmtpcnt] = line[5:]
fmtpcnt++
case strings.HasPrefix(line, "ptime:"):
ptimeS := line[6:]
if ptime, err := strconv.Atoi(ptimeS); err == nil && ptime > 0 {
sdp.Ptime = ptime
} else {
log.Println("Invalid SDP Ptime value", ptimeS)
}
case line == "sendrecv":
case line == "sendonly":
sdp.SendOnly = true
case line == "recvonly":
sdp.RecvOnly = true
default:
if n := strings.Index(line, ":"); n >= 0 {
if n == 0 {
log.Println("Evil SDP attribute:", line)
} else {
l := len(sdp.Attrs)
sdp.Attrs = sdp.Attrs[0 : l+1]
sdp.Attrs[l] = [2]string{line[0:n], line[n+1:]}
}
} else {
l := len(sdp.Attrs)
sdp.Attrs = sdp.Attrs[0 : l+1]
sdp.Attrs[l] = [2]string{line, ""}
}
}
}
}
rtpmaps = rtpmaps[0:rtpmapcnt]
fmtps = fmtps[0:fmtpcnt]
if !okConn || !okOrigin {
return nil, errors.New("sdp missing mandatory information")
}
// Assemble audio/video information.
var pts []uint8
if audioinfo != "" {
sdp.Audio = new(Media)
sdp.Audio.Type = "audio"
sdp.Audio.Port, sdp.Audio.Proto, pts, err = parseMediaInfo(audioinfo)
if err != nil {
return nil, err
}
err = populateCodecs(sdp.Audio, pts, rtpmaps, fmtps)
if err != nil {
return nil, err
}
} else {
sdp.Video = nil
}
if videoinfo != "" {
sdp.Video = new(Media)
sdp.Video.Type = "video"
sdp.Video.Port, sdp.Video.Proto, pts, err = parseMediaInfo(videoinfo)
if err != nil {
return nil, err
}
err = populateCodecs(sdp.Video, pts, rtpmaps, fmtps)
if err != nil {
return nil, err
}
} else {
sdp.Video = nil
}
if sdp.Audio == nil && sdp.Video == nil {
return nil, errors.New("sdp has no audio or video information")
}
return sdp, nil
}
func (sdp *SDP) String() string {
if sdp == nil {
return ""
}
var b bytes.Buffer
if err := sdp.Append(&b); err != nil {
log.Println("Bad SDP!", err)
return ""
}
return b.String()
}
func (sdp *SDP) Append(b *bytes.Buffer) error {
if sdp.Audio == nil && sdp.Video == nil {
return errors.New("sdp lonely no media :[")
}
if sdp.Session == "" {
sdp.Session = "my people call themselves dark angels"
}
if sdp.Time == "" {
sdp.Time = "0 0"
}
b.WriteString("v=0\r\n")
if err := sdp.Origin.Append(b); err != nil {
return err
}
b.WriteString("s=" + sdp.Session + "\r\n")
if util.IsIPv6(sdp.Addr) {
b.WriteString("c=IN IP6 " + sdp.Addr + "\r\n")
} else {
b.WriteString("c=IN IP4 " + sdp.Addr + "\r\n")
}
b.WriteString("t=" + sdp.Time + "\r\n")
if sdp.Audio != nil {
if err := sdp.Audio.Append(b); err != nil {
return err
}
}
if sdp.Video != nil {
if err := sdp.Video.Append(b); err != nil {
return err
}
}
if sdp.Attrs != nil {
for _, attr := range sdp.Attrs {
if attr[0] == "" {
return errors.New("SDP.Attrs key empty!")
}
if attr[1] == "" {
b.WriteString("a=" + attr[0] + "\r\n")
} else {
b.WriteString("a=" + attr[0] + ":" + attr[1] + "\r\n")
}
}
}
if sdp.Ptime > 0 {
b.WriteString("a=ptime:" + strconv.Itoa(sdp.Ptime) + "\r\n")
}
if sdp.SendOnly {
b.WriteString("a=sendonly\r\n")
} else if sdp.RecvOnly {
b.WriteString("a=recvonly\r\n")
} else {
b.WriteString("a=sendrecv\r\n")
}
return nil
}
// Here we take the list of payload types from the m= line (e.g. 9 18 0 101)
// and turn them into a list of codecs.
//
// If we couldn't find a matching rtpmap, iana standardized will be filled in
// like magic.
func populateCodecs(media *Media, pts []uint8, rtpmaps, fmtps []string) (err error) {
media.Codecs = make([]Codec, len(pts))
for n, pt := range pts {
codec := &media.Codecs[n]
codec.PT = pt
prefix := strconv.FormatInt(int64(pt), 10) + " "
for _, rtpmap := range rtpmaps {
if strings.HasPrefix(rtpmap, prefix) {
err = parseRtpmapInfo(codec, rtpmap[len(prefix):])
if err != nil {
return err
}
break
}
}
if codec.Name == "" {
if isDynamicPT(pt) {
return errors.New("dynamic codec missing rtpmap")
} else {
if v, ok := StandardCodecs[pt]; ok {
*codec = *v
} else {
return errors.New("unknown iana codec id: " +
strconv.Itoa(int(pt)))
}
}
}
for _, fmtp := range fmtps {
if strings.HasPrefix(fmtp, prefix) {
codec.Fmtp = fmtp[len(prefix):]
break
}
}
}
return nil
}
// Returns true if IANA says this payload type is dynamic.
func isDynamicPT(pt uint8) bool {
return (pt >= 96)
}
// Give me the part of the a=rtpmap line that looks like: "PCMU/8000" or
// "L16/16000/2".
func parseRtpmapInfo(codec *Codec, s string) (err error) {
toks := strings.Split(s, "/")
if toks != nil && len(toks) >= 2 {
codec.Name = toks[0]
codec.Rate, err = strconv.Atoi(toks[1])
if err != nil {
return errors.New("invalid rtpmap rate")
}
if len(toks) >= 3 {
codec.Param = toks[2]
}
} else {
return errors.New("invalid rtpmap")
}
return nil
}
// Give me the part of an "m=" line that looks like: "30126 RTP/AVP 0 101".
func parseMediaInfo(s string) (port int, proto string, pts []uint8, err error) {
toks := strings.Split(s, " ")
if toks == nil || len(toks) < 3 {
return 0, "", nil, errors.New("invalid m= line")
}
// We don't care if they say like "666/2" which is a weird way of saying hey!
// send ME rtcp too (I think).
portS := toks[0]
if n := strings.Index(portS, "/"); n > 0 {
portS = portS[0:n]
}
// Convert port to int and check range.
port, err = strconv.Atoi(portS)
if err != nil || !(0 <= port && port <= 65535) {
return 0, "", nil, errors.New("invalid m= port")
}
// Is it rtp? srtp? udp? tcp? etc. (must be 3+ chars)
proto = toks[1]
// The rest of these tokens are payload types sorted by preference.
pts = make([]uint8, len(toks)-2)
for n, pt := range toks[2:] {
pt, err := strconv.ParseUint(pt, 10, 8)
if err != nil {
return 0, "", nil, errors.New("invalid pt in m= line")
}
pts[n] = byte(pt)
}
return port, proto, pts, nil
}
// I want a string that looks like "c=IN IP4 10.0.0.38".
func parseConnLine(line string) (addr string, err error) {
toks := strings.Split(line[2:], " ")
if toks == nil || len(toks) != 3 {
return "", errors.New("invalid conn line")
}
if toks[0] != "IN" || (toks[1] != "IP4" && toks[1] != "IP6") {
return "", errors.New("unsupported conn net type")
}
addr = toks[2]
if n := strings.Index(addr, "/"); n >= 0 {
return "", errors.New("multicast address in c= line D:")
}
return addr, nil
}
// I want a string that looks like "o=root 31589 31589 IN IP4 10.0.0.38".
func parseOriginLine(origin *Origin, line string) error {
toks := strings.Split(line[2:], " ")
if toks == nil || len(toks) != 6 {
return errors.New("invalid origin line")
}
if toks[3] != "IN" || (toks[4] != "IP4" && toks[4] != "IP6") {
return errors.New("unsupported origin net type")
}
origin.User = toks[0]
origin.ID = toks[1]
origin.Version = toks[2]
origin.Addr = toks[5]
if n := strings.Index(origin.Addr, "/"); n >= 0 {
return errors.New("multicast address in o= line D:")
}
return nil
}