mirror of
https://github.com/lkmio/gb-cms.git
synced 2025-09-26 19:51:22 +08:00
支持tcp拉流
This commit is contained in:
30
api.go
30
api.go
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gb-cms/sdp"
|
||||
"github.com/ghettovoice/gosip"
|
||||
"github.com/ghettovoice/gosip/sip"
|
||||
"github.com/gorilla/mux"
|
||||
@@ -121,23 +123,27 @@ func (api *ApiServer) OnPlay(streamId, protocol string, w http.ResponseWriter, r
|
||||
}()
|
||||
|
||||
ssrc := GetLiveSSRC()
|
||||
ip, port, err := CreateGBSource(streamId, "UDP", "recvonly", ssrc)
|
||||
query := r.URL.Query()
|
||||
setup := strings.ToLower(query.Get("setup"))
|
||||
ip, port, err := CreateGBSource(streamId, setup, ssrc)
|
||||
if err != nil {
|
||||
Sugar.Errorf("创建GBSource失败 err:%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
inviteRequest, err := device.DoLive(channelId, ip, port, "RTP/AVP", "recvonly", ssrc)
|
||||
inviteRequest, err := device.DoLive(channelId, ip, port, setup, ssrc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var bye sip.Request
|
||||
var answer string
|
||||
reqCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
SipUA.SendRequestWithContext(reqCtx, inviteRequest, gosip.WithResponseHandler(func(res sip.Response, request sip.Request) {
|
||||
if res.StatusCode() < 200 {
|
||||
|
||||
} else if res.StatusCode() == 200 {
|
||||
answer = res.Body()
|
||||
ackRequest := sip.NewAckRequest("", inviteRequest, res, "", nil)
|
||||
ackRequest.AppendHeader(globalContactAddress.AsContactHeader())
|
||||
//手动替换ack请求目标地址, answer的contact可能不对.
|
||||
@@ -170,6 +176,26 @@ func (api *ApiServer) OnPlay(streamId, protocol string, w http.ResponseWriter, r
|
||||
return
|
||||
}
|
||||
|
||||
if "active" == setup {
|
||||
parse, err := sdp.Parse(answer)
|
||||
if err != nil {
|
||||
inviteOk = false
|
||||
logger.Errorf("解析应答sdp失败 err:%s sdp:%s", err.Error(), answer)
|
||||
return
|
||||
}
|
||||
if parse.Video == nil || parse.Video.Port == 0 {
|
||||
inviteOk = false
|
||||
logger.Errorf("应答不没有视频连接地址 sdp:%s", answer)
|
||||
return
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", parse.Addr, parse.Video.Port)
|
||||
if err = ConnectGBSource(streamId, addr); err != nil {
|
||||
inviteOk = false
|
||||
logger.Errorf("设置GB28181连接地址失败 err:%s addr:%s", err.Error(), addr)
|
||||
}
|
||||
}
|
||||
|
||||
if stream.waitPublishStream() {
|
||||
stream.ByeRequest = bye
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
33
device.go
33
device.go
@@ -25,10 +25,22 @@ const (
|
||||
"s=Play\r\n" +
|
||||
"c=IN IP4 %s\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video %d %s 96\r\n" +
|
||||
"m=video %d RTP/AVP 96\r\n" +
|
||||
"a=%s\r\n" +
|
||||
"a=rtpmap:96 PS/90000\r\n" +
|
||||
"y=%s"
|
||||
|
||||
InviteTCPFormat = "v=0\r\n" +
|
||||
"o=%s 0 0 IN IP4 %s\r\n" +
|
||||
"s=Play\r\n" +
|
||||
"c=IN IP4 %s\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=video %d TCP/RTP/AVP 96\r\n" +
|
||||
"a=%s\r\n" +
|
||||
"a=rtpmap:96 PS/90000\r\n" +
|
||||
"a=setup:%s\r\n" +
|
||||
"a=connection:new\r\n" +
|
||||
"y=%s"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -128,10 +140,25 @@ func (d *DBDevice) NewRequestBuilder(method sip.RequestMethod, from, realm, to s
|
||||
return builder
|
||||
}
|
||||
|
||||
func (d *DBDevice) DoLive(channelId, ip string, port uint16, mediaProtocol string, setup string, ssrc uint32) (sip.Request, error) {
|
||||
func (d *DBDevice) DoLive(channelId, ip string, port uint16, setup string, ssrc uint32) (sip.Request, error) {
|
||||
builder := d.NewRequestBuilder(sip.INVITE, Config.SipId, Config.SipRealm, channelId)
|
||||
|
||||
sdp := fmt.Sprintf(InviteFormat, Config.SipId, ip, ip, port, mediaProtocol, setup, fmt.Sprintf("%0*d", 10, ssrc))
|
||||
tcp := true
|
||||
//var active bool
|
||||
var sdp string
|
||||
if "passive" == setup {
|
||||
} else if "active" == setup {
|
||||
// active = true
|
||||
} else {
|
||||
tcp = false
|
||||
}
|
||||
|
||||
if !tcp {
|
||||
sdp = fmt.Sprintf(InviteFormat, Config.SipId, ip, ip, port, "recvonly", fmt.Sprintf("%0*d", 10, ssrc))
|
||||
} else {
|
||||
sdp = fmt.Sprintf(InviteTCPFormat, Config.SipId, ip, ip, port, "recvonly", setup, fmt.Sprintf("%0*d", 10, ssrc))
|
||||
}
|
||||
|
||||
builder.SetContentType(&SDPMessageType)
|
||||
builder.SetContact(globalContactAddress)
|
||||
builder.SetBody(sdp)
|
||||
|
@@ -29,17 +29,15 @@ func Send(path string, body interface{}) (*http.Response, error) {
|
||||
return client.Do(request)
|
||||
}
|
||||
|
||||
func CreateGBSource(id, transport, setup string, ssrc uint32) (string, uint16, error) {
|
||||
func CreateGBSource(id, setup string, ssrc uint32) (string, uint16, error) {
|
||||
v := &struct {
|
||||
Source string `json:"source"`
|
||||
Transport string `json:"transport"`
|
||||
Setup string `json:"setup"`
|
||||
SSRC uint32 `json:"ssrc"`
|
||||
Source string `json:"source"`
|
||||
Setup string `json:"setup"`
|
||||
SSRC uint32 `json:"ssrc"`
|
||||
}{
|
||||
Source: id,
|
||||
Transport: transport,
|
||||
Setup: setup,
|
||||
SSRC: ssrc,
|
||||
Source: id,
|
||||
Setup: setup,
|
||||
SSRC: ssrc,
|
||||
}
|
||||
|
||||
response, err := Send("v1/gb28181/source/create", v)
|
||||
@@ -64,6 +62,19 @@ func CreateGBSource(id, transport, setup string, ssrc uint32) (string, uint16, e
|
||||
return connectInfo.Data.IP, connectInfo.Data.Port, nil
|
||||
}
|
||||
|
||||
func ConnectGBSource(id, addr string) error {
|
||||
v := &struct {
|
||||
Source string `json:"source"` //SourceId
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
}{
|
||||
Source: id,
|
||||
RemoteAddr: addr,
|
||||
}
|
||||
|
||||
_, err := Send("v1/gb28181/source/connect", v)
|
||||
return err
|
||||
}
|
||||
|
||||
func CloseGBSource(id string) error {
|
||||
v := &struct {
|
||||
Source string `json:"source"`
|
||||
|
52
sdp/codec.go
Normal file
52
sdp/codec.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sdp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Codec describes one of the codec lines in an SDP. This data will be
|
||||
// magically filled in if the rtpmap wasn't provided (assuming it's a well
|
||||
// known codec having a payload type less than 96.)
|
||||
type Codec struct {
|
||||
PT uint8 // 7-bit payload type we need to put in our RTP packets
|
||||
Name string // e.g. PCMU, G729, telephone-event, etc.
|
||||
Rate int // frequency in hertz. usually 8000
|
||||
Param string // sometimes used to specify number of channels
|
||||
Fmtp string // some extra info; i.e. dtmf might set as "0-16"
|
||||
}
|
||||
|
||||
func (codec *Codec) Append(b *bytes.Buffer) {
|
||||
b.WriteString("a=rtpmap:")
|
||||
b.WriteString(strconv.FormatInt(int64(codec.PT), 10))
|
||||
b.WriteString(" ")
|
||||
b.WriteString(codec.Name)
|
||||
b.WriteString("/")
|
||||
b.WriteString(strconv.Itoa(codec.Rate))
|
||||
if codec.Param != "" {
|
||||
b.WriteString("/")
|
||||
b.WriteString(codec.Param)
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
if codec.Fmtp != "" {
|
||||
b.WriteString("a=fmtp:")
|
||||
b.WriteString(strconv.FormatInt(int64(codec.PT), 10))
|
||||
b.WriteString(" ")
|
||||
b.WriteString(codec.Fmtp)
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
}
|
66
sdp/codecs.go
Normal file
66
sdp/codecs.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// IANA Assigned VoIP Codec Payload Types
|
||||
//
|
||||
// The following codecs have been standardized by IANA thereby
|
||||
// allowing their 'a=rtpmap' information to be omitted in SDP
|
||||
// messages. they've been hard coded to make your life easier.
|
||||
//
|
||||
// Many of these codecs (G711, G722, G726, GSM, LPC) have been
|
||||
// implemented by Steve Underwood in his excellent SpanDSP library.
|
||||
//
|
||||
// Newer codecs like silk, broadvoice, speex, etc. use a dynamic
|
||||
// payload type.
|
||||
//
|
||||
// Reference Material:
|
||||
//
|
||||
// - IANA Payload Types: http://www.iana.org/assignments/rtp-parameters
|
||||
// - Explains well-known ITU codecs: http://tools.ietf.org/html/rfc3551
|
||||
//
|
||||
|
||||
package sdp
|
||||
|
||||
var (
|
||||
ULAWCodec = Codec{PT: 0, Name: "PCMU", Rate: 8000}
|
||||
DTMFCodec = Codec{PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-16"}
|
||||
Opus = Codec{PT: 111, Name: "opus", Rate: 48000, Param: "2"}
|
||||
|
||||
StandardCodecs = map[uint8]Codec{
|
||||
0: ULAWCodec, // G.711 μ-Law is the de-facto codec (SpanDSP g711.h)
|
||||
3: {PT: 3, Name: "GSM", Rate: 8000}, // Uncool codec asterisk ppl like (SpanDSP gsm0610.h)
|
||||
4: {PT: 4, Name: "G723", Rate: 8000}, // Worthless.
|
||||
5: {PT: 5, Name: "DVI4", Rate: 8000}, // Adaptive pulse code modulation (SpanDSP ima_adpcm.h)
|
||||
6: {PT: 6, Name: "DVI4", Rate: 16000}, // Adaptive pulse code modulation 16khz (SpanDSP ima_adpcm.h)
|
||||
7: {PT: 7, Name: "LPC", Rate: 8000}, // Chat with your friends ww2 field marshall style (SpanDSP lpc10.h)
|
||||
8: {PT: 8, Name: "PCMA", Rate: 8000}, // G.711 variant of μ-Law used in yurop (SpanDSP g711.h)
|
||||
9: {PT: 9, Name: "G722", Rate: 8000}, // Used for Polycom HD Voice; Rate actually 16khz LOL (SpanDSP g722.h)
|
||||
10: {PT: 10, Name: "L16", Rate: 44100, Param: "2"}, // 16-bit signed PCM stereo/mono (mind your MTU; adjust ptime)
|
||||
11: {PT: 11, Name: "L16", Rate: 44100},
|
||||
12: {PT: 12, Name: "QCELP", Rate: 8000},
|
||||
13: {PT: 13, Name: "CN", Rate: 8000}, // RFC3389 comfort noise
|
||||
14: {PT: 14, Name: "MPA", Rate: 90000},
|
||||
15: {PT: 15, Name: "G728", Rate: 8000},
|
||||
16: {PT: 16, Name: "DVI4", Rate: 11025},
|
||||
17: {PT: 17, Name: "DVI4", Rate: 22050},
|
||||
18: {PT: 18, Name: "G729", Rate: 8000}, // Best telephone voice codec (if you got $10 bucks)
|
||||
25: {PT: 25, Name: "CelB", Rate: 90000},
|
||||
26: {PT: 26, Name: "JPEG", Rate: 90000},
|
||||
28: {PT: 28, Name: "nv", Rate: 90000},
|
||||
31: {PT: 31, Name: "H261", Rate: 90000}, // RFC4587 Video
|
||||
32: {PT: 32, Name: "MPV", Rate: 90000},
|
||||
33: {PT: 33, Name: "MP2T", Rate: 90000},
|
||||
34: {PT: 34, Name: "H263", Rate: 90000}, // $$$ video
|
||||
}
|
||||
)
|
49
sdp/media.go
Normal file
49
sdp/media.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sdp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Media is a high level representation of the c=/m=/a= lines for describing a
|
||||
// specific type of media. Only "audio" and "video" are supported at this time.
|
||||
type Media struct {
|
||||
Proto string // RTP, SRTP, UDP, UDPTL, TCP, TLS, etc.
|
||||
Port uint16 // Port number (0 - 2^16-1)
|
||||
Codecs []Codec // Collection of codecs of a specific type.
|
||||
}
|
||||
|
||||
func (media *Media) Append(type_ string, b *bytes.Buffer) {
|
||||
b.WriteString("m=")
|
||||
b.WriteString(type_)
|
||||
b.WriteString(" ")
|
||||
b.WriteString(strconv.FormatUint(uint64(media.Port), 10))
|
||||
b.WriteString(" ")
|
||||
if media.Proto == "" {
|
||||
b.WriteString("RTP/AVP")
|
||||
} else {
|
||||
b.WriteString(media.Proto)
|
||||
}
|
||||
for _, codec := range media.Codecs {
|
||||
b.WriteString(" ")
|
||||
b.WriteString(strconv.FormatInt(int64(codec.PT), 10))
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
for _, codec := range media.Codecs {
|
||||
codec.Append(b)
|
||||
}
|
||||
}
|
61
sdp/origin.go
Normal file
61
sdp/origin.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sdp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// Origin represents the session origin (o=) line of an SDP. Who knows what
|
||||
// this is supposed to do.
|
||||
type Origin struct {
|
||||
User string // First value in o= line
|
||||
ID string // Second value in o= line
|
||||
Version string // Third value in o= line
|
||||
Addr string // Tracks IP of original user-agent
|
||||
}
|
||||
|
||||
func (origin *Origin) Append(b *bytes.Buffer) {
|
||||
id := origin.ID
|
||||
if id == "" {
|
||||
id = GenerateOriginID()
|
||||
}
|
||||
b.WriteString("o=")
|
||||
if origin.User == "" {
|
||||
b.WriteString("-")
|
||||
} else {
|
||||
b.WriteString(origin.User)
|
||||
}
|
||||
b.WriteString(" ")
|
||||
b.WriteString(id)
|
||||
b.WriteString(" ")
|
||||
if origin.Version == "" {
|
||||
b.WriteString(id)
|
||||
} else {
|
||||
b.WriteString(origin.Version)
|
||||
}
|
||||
if IsIPv6(origin.Addr) {
|
||||
b.WriteString(" IN IP6 ")
|
||||
} else {
|
||||
b.WriteString(" IN IP4 ")
|
||||
}
|
||||
if origin.Addr == "" {
|
||||
// In case of bugs, keep calm and DDOS NASA.
|
||||
b.WriteString("69.28.157.198")
|
||||
} else {
|
||||
b.WriteString(origin.Addr)
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
}
|
507
sdp/sdp.go
Normal file
507
sdp/sdp.go
Normal file
@@ -0,0 +1,507 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// 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"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ContentType = "application/sdp"
|
||||
MaxLength = 1450
|
||||
)
|
||||
|
||||
// SDP represents a Session Description Protocol SIP payload.
|
||||
type SDP struct {
|
||||
Origin Origin // This must always be present
|
||||
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
|
||||
Session string // s= Session Name (default "-")
|
||||
Time string // t= Active Time (default "0 0")
|
||||
Ptime int // Transmit frame every N milliseconds (default 20)
|
||||
SendOnly bool // True if 'a=sendonly' was specified in SDP
|
||||
RecvOnly bool // True if 'a=recvonly' was specified in SDP
|
||||
Attrs [][2]string // a= lines we don't recognize
|
||||
Other [][2]string // Other description
|
||||
}
|
||||
|
||||
// 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 = GenerateOriginID()
|
||||
sdp.Origin.Version = sdp.Origin.ID
|
||||
sdp.Origin.Addr = sdp.Addr
|
||||
sdp.Audio = new(Media)
|
||||
sdp.Audio.Proto = "RTP/AVP"
|
||||
sdp.Audio.Port = uint16(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, ""}
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
||||
// Other unknown fields will be saved here
|
||||
if n := strings.Index(line, "="); n >= 0 {
|
||||
|
||||
if n == 0 {
|
||||
log.Println("Evil SDP field:", line)
|
||||
} else {
|
||||
sdp.Other = append(sdp.Other, [2]string{line[0:n], line[n+1:]})
|
||||
}
|
||||
} else {
|
||||
sdp.Other = append(sdp.Other, [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.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.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) ContentType() string {
|
||||
return ContentType
|
||||
}
|
||||
|
||||
func (sdp *SDP) Data() []byte {
|
||||
if sdp == nil {
|
||||
return nil
|
||||
}
|
||||
var b bytes.Buffer
|
||||
sdp.Append(&b)
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func (sdp *SDP) String() string {
|
||||
if sdp == nil {
|
||||
return ""
|
||||
}
|
||||
var b bytes.Buffer
|
||||
sdp.Append(&b)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (sdp *SDP) Append(b *bytes.Buffer) {
|
||||
b.WriteString("v=0\r\n")
|
||||
sdp.Origin.Append(b)
|
||||
b.WriteString("s=")
|
||||
if sdp.Session == "" {
|
||||
b.WriteString("my people call themselves dark angels")
|
||||
} else {
|
||||
b.WriteString(sdp.Session)
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
if IsIPv6(sdp.Addr) {
|
||||
b.WriteString("c=IN IP6 ")
|
||||
} else {
|
||||
b.WriteString("c=IN IP4 ")
|
||||
}
|
||||
if sdp.Addr == "" {
|
||||
// In case of bugs, keep calm and DDOS NASA.
|
||||
b.WriteString("69.28.157.198")
|
||||
} else {
|
||||
b.WriteString(sdp.Addr)
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
b.WriteString("t=")
|
||||
if sdp.Time == "" {
|
||||
b.WriteString("0 0")
|
||||
} else {
|
||||
b.WriteString(sdp.Time)
|
||||
}
|
||||
b.WriteString("\r\n")
|
||||
if sdp.Audio != nil {
|
||||
sdp.Audio.Append("audio", b)
|
||||
}
|
||||
if sdp.Video != nil {
|
||||
sdp.Video.Append("video", b)
|
||||
}
|
||||
for _, attr := range sdp.Attrs {
|
||||
if attr[1] == "" {
|
||||
b.WriteString("a=")
|
||||
b.WriteString(attr[0])
|
||||
b.WriteString("\r\n")
|
||||
} else {
|
||||
b.WriteString("a=")
|
||||
b.WriteString(attr[0])
|
||||
b.WriteString(":")
|
||||
b.WriteString(attr[1])
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
if sdp.Ptime > 0 {
|
||||
b.WriteString("a=ptime:")
|
||||
b.WriteString(strconv.Itoa(sdp.Ptime))
|
||||
b.WriteString("\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")
|
||||
}
|
||||
|
||||
// save unknown field
|
||||
if sdp.Other != nil {
|
||||
for _, v := range sdp.Other {
|
||||
b.WriteString(v[0])
|
||||
b.WriteString("=")
|
||||
b.WriteString(v[1])
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 uint16, 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.
|
||||
portU, err := strconv.ParseUint(portS, 10, 16)
|
||||
if err != nil || !(0 <= port && port <= 65535) {
|
||||
return 0, "", nil, errors.New("invalid m= port")
|
||||
}
|
||||
port = uint16(portU)
|
||||
|
||||
// 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
|
||||
}
|
475
sdp/sdp_test.go
Normal file
475
sdp/sdp_test.go
Normal file
@@ -0,0 +1,475 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sdp_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gb-cms/sdp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type sdpTest struct {
|
||||
name string // arbitrary name for test
|
||||
s string // raw sdp input to parse
|
||||
s2 string // non-blank if sdp looks different when we format it
|
||||
sdp *sdp.SDP // memory structure of 's' after parsing
|
||||
err error
|
||||
}
|
||||
|
||||
var sdpTests = []sdpTest{
|
||||
|
||||
{
|
||||
name: "Asterisk PCMU+DTMF",
|
||||
s: ("v=0\r\n" +
|
||||
"o=root 31589 31589 IN IP4 10.0.0.38\r\n" +
|
||||
"s=session\r\n" +
|
||||
"c=IN IP4 10.0.0.38\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 30126 RTP/AVP 0 101\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:101 telephone-event/8000\r\n" +
|
||||
"a=fmtp:101 0-16\r\n" +
|
||||
"a=silenceSupp:off - - - -\r\n" +
|
||||
"a=ptime:20\r\n" +
|
||||
"a=sendrecv\r\n"),
|
||||
sdp: &sdp.SDP{
|
||||
Origin: sdp.Origin{
|
||||
User: "root",
|
||||
ID: "31589",
|
||||
Version: "31589",
|
||||
Addr: "10.0.0.38",
|
||||
},
|
||||
Session: "session",
|
||||
Time: "0 0",
|
||||
Addr: "10.0.0.38",
|
||||
Audio: &sdp.Media{
|
||||
Proto: "RTP/AVP",
|
||||
Port: 30126,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 0, Name: "PCMU", Rate: 8000},
|
||||
{PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-16"},
|
||||
},
|
||||
},
|
||||
Attrs: [][2]string{
|
||||
{"silenceSupp", "off - - - -"},
|
||||
},
|
||||
Ptime: 20,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Audio+Video+Implicit+Fmtp",
|
||||
s: "v=0\r\n" +
|
||||
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
|
||||
"c=IN IP4 1.2.3.4\r\n" +
|
||||
"m=audio 32898 RTP/AVP 18\r\n" +
|
||||
"m=video 32900 RTP/AVP 34\r\n" +
|
||||
"a=fmtp:18 annexb=yes",
|
||||
s2: "v=0\r\n" +
|
||||
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
|
||||
"s=pokémon\r\n" +
|
||||
"c=IN IP4 1.2.3.4\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 32898 RTP/AVP 18\r\n" +
|
||||
"a=rtpmap:18 G729/8000\r\n" +
|
||||
"a=fmtp:18 annexb=yes\r\n" +
|
||||
"m=video 32900 RTP/AVP 34\r\n" +
|
||||
"a=rtpmap:34 H263/90000\r\n" +
|
||||
"a=sendrecv\r\n",
|
||||
sdp: &sdp.SDP{
|
||||
Origin: sdp.Origin{
|
||||
User: "-",
|
||||
ID: "3366701332",
|
||||
Version: "3366701332",
|
||||
Addr: "1.2.3.4",
|
||||
},
|
||||
Addr: "1.2.3.4",
|
||||
Session: "pokémon",
|
||||
Time: "0 0",
|
||||
Audio: &sdp.Media{
|
||||
Proto: "RTP/AVP",
|
||||
Port: 32898,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 18, Name: "G729", Rate: 8000, Fmtp: "annexb=yes"},
|
||||
},
|
||||
},
|
||||
Video: &sdp.Media{
|
||||
Proto: "RTP/AVP",
|
||||
Port: 32900,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 34, Name: "H263", Rate: 90000},
|
||||
},
|
||||
},
|
||||
Attrs: [][2]string{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "Implicit Codecs",
|
||||
s: "v=0\r\n" +
|
||||
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
|
||||
"s=-\r\n" +
|
||||
"c=IN IP4 1.2.3.4\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
|
||||
"a=rtpmap:101 telephone-event/8000\r\n" +
|
||||
"a=ptime:20\r\n",
|
||||
s2: "v=0\r\n" +
|
||||
"o=- 3366701332 3366701332 IN IP4 1.2.3.4\r\n" +
|
||||
"s=-\r\n" +
|
||||
"c=IN IP4 1.2.3.4\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:18 G729/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:101 telephone-event/8000\r\n" +
|
||||
"a=ptime:20\r\n" +
|
||||
"a=sendrecv\r\n",
|
||||
sdp: &sdp.SDP{
|
||||
Origin: sdp.Origin{
|
||||
User: "-",
|
||||
ID: "3366701332",
|
||||
Version: "3366701332",
|
||||
Addr: "1.2.3.4",
|
||||
},
|
||||
Session: "-",
|
||||
Time: "0 0",
|
||||
Addr: "1.2.3.4",
|
||||
Audio: &sdp.Media{
|
||||
Proto: "RTP/AVP",
|
||||
Port: 32898,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 9, Name: "G722", Rate: 8000},
|
||||
{PT: 18, Name: "G729", Rate: 8000},
|
||||
{PT: 0, Name: "PCMU", Rate: 8000},
|
||||
{PT: 101, Name: "telephone-event", Rate: 8000},
|
||||
},
|
||||
},
|
||||
Ptime: 20,
|
||||
Attrs: [][2]string{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "IPv6",
|
||||
s: "v=0\r\n" +
|
||||
"o=- 3366701332 3366701332 IN IP6 dead:beef::666\r\n" +
|
||||
"s=-\r\n" +
|
||||
"c=IN IP6 dead:beef::666\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
|
||||
"a=rtpmap:101 telephone-event/8000\r\n" +
|
||||
"a=ptime:20\r\n",
|
||||
s2: "v=0\r\n" +
|
||||
"o=- 3366701332 3366701332 IN IP6 dead:beef::666\r\n" +
|
||||
"s=-\r\n" +
|
||||
"c=IN IP6 dead:beef::666\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 32898 RTP/AVP 9 18 0 101\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:18 G729/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:101 telephone-event/8000\r\n" +
|
||||
"a=ptime:20\r\n" +
|
||||
"a=sendrecv\r\n",
|
||||
sdp: &sdp.SDP{
|
||||
Origin: sdp.Origin{
|
||||
User: "-",
|
||||
ID: "3366701332",
|
||||
Version: "3366701332",
|
||||
Addr: "dead:beef::666",
|
||||
},
|
||||
Session: "-",
|
||||
Time: "0 0",
|
||||
Addr: "dead:beef::666",
|
||||
Audio: &sdp.Media{
|
||||
Proto: "RTP/AVP",
|
||||
Port: 32898,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 9, Name: "G722", Rate: 8000},
|
||||
{PT: 18, Name: "G729", Rate: 8000},
|
||||
{PT: 0, Name: "PCMU", Rate: 8000},
|
||||
{PT: 101, Name: "telephone-event", Rate: 8000},
|
||||
},
|
||||
},
|
||||
Ptime: 20,
|
||||
Attrs: [][2]string{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "pjmedia long sdp is long",
|
||||
s: ("v=0\r\n" +
|
||||
"o=- 3457169218 3457169218 IN IP4 10.11.34.37\r\n" +
|
||||
"s=pjmedia\r\n" +
|
||||
"c=IN IP4 10.11.34.37\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 4000 RTP/AVP 103 102 104 113 3 0 8 9 101\r\n" +
|
||||
"a=rtpmap:103 speex/16000\r\n" +
|
||||
"a=rtpmap:102 speex/8000\r\n" +
|
||||
"a=rtpmap:104 speex/32000\r\n" +
|
||||
"a=rtpmap:113 iLBC/8000\r\n" +
|
||||
"a=fmtp:113 mode=30\r\n" +
|
||||
"a=rtpmap:3 GSM/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:101 telephone-event/8000\r\n" +
|
||||
"a=fmtp:101 0-15\r\n" +
|
||||
"a=rtcp:4001 IN IP4 10.11.34.37\r\n" +
|
||||
"a=X-nat:0\r\n" +
|
||||
"a=ptime:20\r\n" +
|
||||
"a=sendrecv\r\n"),
|
||||
sdp: &sdp.SDP{
|
||||
Origin: sdp.Origin{
|
||||
User: "-",
|
||||
ID: "3457169218",
|
||||
Version: "3457169218",
|
||||
Addr: "10.11.34.37",
|
||||
},
|
||||
Session: "pjmedia",
|
||||
Time: "0 0",
|
||||
Addr: "10.11.34.37",
|
||||
Audio: &sdp.Media{
|
||||
Proto: "RTP/AVP",
|
||||
Port: 4000,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 103, Name: "speex", Rate: 16000},
|
||||
{PT: 102, Name: "speex", Rate: 8000},
|
||||
{PT: 104, Name: "speex", Rate: 32000},
|
||||
{PT: 113, Name: "iLBC", Rate: 8000, Fmtp: "mode=30"},
|
||||
{PT: 3, Name: "GSM", Rate: 8000},
|
||||
{PT: 0, Name: "PCMU", Rate: 8000},
|
||||
{PT: 8, Name: "PCMA", Rate: 8000},
|
||||
{PT: 9, Name: "G722", Rate: 8000},
|
||||
{PT: 101, Name: "telephone-event", Rate: 8000, Fmtp: "0-15"},
|
||||
},
|
||||
},
|
||||
Ptime: 20,
|
||||
Attrs: [][2]string{
|
||||
{"rtcp", "4001 IN IP4 10.11.34.37"},
|
||||
{"X-nat", "0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "mp3 tcp",
|
||||
s: ("v=0\r\n" +
|
||||
"o=- 3366701332 3366701334 IN IP4 10.11.34.37\r\n" +
|
||||
"s=squigglies\r\n" +
|
||||
"c=IN IP6 dead:beef::666\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 80 TCP/IP 111\r\n" +
|
||||
"a=rtpmap:111 MP3/44100/2\r\n" +
|
||||
"a=sendonly\r\n"),
|
||||
sdp: &sdp.SDP{
|
||||
Origin: sdp.Origin{
|
||||
User: "-",
|
||||
ID: "3366701332",
|
||||
Version: "3366701334",
|
||||
Addr: "10.11.34.37",
|
||||
},
|
||||
Session: "squigglies",
|
||||
Time: "0 0",
|
||||
Addr: "dead:beef::666",
|
||||
SendOnly: true,
|
||||
Audio: &sdp.Media{
|
||||
Proto: "TCP/IP",
|
||||
Port: 80,
|
||||
Codecs: []sdp.Codec{
|
||||
{PT: 111, Name: "MP3", Rate: 44100, Param: "2"},
|
||||
},
|
||||
},
|
||||
Attrs: [][2]string{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func sdpCompareCodec(t *testing.T, name string, correct, codec *sdp.Codec) {
|
||||
if correct != nil && codec == nil {
|
||||
t.Error(name, "not found")
|
||||
}
|
||||
if correct == nil && codec != nil {
|
||||
t.Error(name, "DO NOT WANT", codec)
|
||||
}
|
||||
if codec == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if correct.PT != codec.PT {
|
||||
t.Error(name, "PT", correct.PT, "!=", codec.PT)
|
||||
}
|
||||
if correct.Name != codec.Name {
|
||||
t.Error(name, "Name", correct.Name, "!=", codec.Name)
|
||||
}
|
||||
if correct.Rate != codec.Rate {
|
||||
t.Error(name, "Rate", correct.Rate, "!=", codec.Rate)
|
||||
}
|
||||
if correct.Param != codec.Param {
|
||||
t.Error(name, "Param", correct.Param, "!=", codec.Param)
|
||||
}
|
||||
if correct.Fmtp != codec.Fmtp {
|
||||
t.Error(name, "Fmtp", correct.Fmtp, "!=", codec.Fmtp)
|
||||
}
|
||||
}
|
||||
|
||||
func sdpCompareCodecs(t *testing.T, name string, corrects, codecs []sdp.Codec) {
|
||||
if corrects != nil && codecs == nil {
|
||||
t.Error(name, "codecs not found")
|
||||
}
|
||||
if corrects == nil && codecs != nil {
|
||||
t.Error(name, "codecs WUT", codecs)
|
||||
}
|
||||
if corrects == nil || codecs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(corrects) != len(codecs) {
|
||||
t.Error(name, "len(Codecs)", len(corrects), "!=", len(codecs))
|
||||
} else {
|
||||
for i := range corrects {
|
||||
c1, c2 := &corrects[i], &codecs[i]
|
||||
if c1 == nil || c2 == nil {
|
||||
t.Error(name, "where my codecs at?")
|
||||
} else {
|
||||
sdpCompareCodec(t, name, c1, c2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sdpCompareMedia(t *testing.T, name string, correct, media *sdp.Media) {
|
||||
if correct != nil && media == nil {
|
||||
t.Error(name, "not found")
|
||||
}
|
||||
if correct == nil && media != nil {
|
||||
t.Error(name, "DO NOT WANT", media)
|
||||
}
|
||||
if correct == nil || media == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if correct.Proto != media.Proto {
|
||||
t.Error(name, "Proto", correct.Proto, "!=", media.Proto)
|
||||
}
|
||||
if correct.Port != media.Port {
|
||||
t.Error(name, "Port", correct.Port, "!=", media.Port)
|
||||
}
|
||||
if media.Codecs == nil || len(media.Codecs) < 1 {
|
||||
t.Error(name, "Must have at least one codec")
|
||||
}
|
||||
|
||||
sdpCompareCodecs(t, name, correct.Codecs, media.Codecs)
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
|
||||
sdpStr :=
|
||||
"v=0\r\n" +
|
||||
"o=34020099991320000015 2950 2950 IN IP4 192.168.1.64\r\n" +
|
||||
"s=Play\r\n" +
|
||||
"c=IN IP4 192.168.1.64\r\n" +
|
||||
"t=0 0\r\n" +
|
||||
"m=audio 15066 RTP/AVP 8 96\r\n" +
|
||||
"a=recvonly\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:96 PS/90000\r\n" +
|
||||
"y=0200000017\r\n" +
|
||||
"f=v/////a/1/8/1\r\n"
|
||||
for _, test := range sdpTests {
|
||||
//sdp, err := sdp.Parse(test.s)
|
||||
sdp, err := sdp.Parse(sdpStr)
|
||||
if err != nil {
|
||||
if test.err == nil {
|
||||
t.Errorf("%v", err)
|
||||
continue
|
||||
} else { // test was supposed to fail
|
||||
panic("todo")
|
||||
}
|
||||
}
|
||||
|
||||
if test.sdp.Addr != sdp.Addr {
|
||||
t.Error(test.name, "Addr", test.sdp.Addr, "!=", sdp.Addr)
|
||||
}
|
||||
if test.sdp.Origin.User != sdp.Origin.User {
|
||||
t.Error(test.name, "Origin.User", test.sdp.Origin.User, "!=",
|
||||
sdp.Origin.User)
|
||||
}
|
||||
if test.sdp.Origin.ID != sdp.Origin.ID {
|
||||
t.Error(test.name, "Origin.ID doesn't match")
|
||||
}
|
||||
if test.sdp.Origin.Version != sdp.Origin.Version {
|
||||
t.Error(test.name, "Origin.Version doesn't match")
|
||||
}
|
||||
if test.sdp.Origin.Addr != sdp.Origin.Addr {
|
||||
t.Error(test.name, "Origin.Addr doesn't match")
|
||||
}
|
||||
if test.sdp.Session != sdp.Session {
|
||||
t.Error(test.name, "Session", test.sdp.Session, "!=", sdp.Session)
|
||||
}
|
||||
if test.sdp.Time != sdp.Time {
|
||||
t.Error(test.name, "Time", test.sdp.Time, "!=", sdp.Time)
|
||||
}
|
||||
if test.sdp.Ptime != sdp.Ptime {
|
||||
t.Error(test.name, "Ptime", test.sdp.Ptime, "!=", sdp.Ptime)
|
||||
}
|
||||
if test.sdp.RecvOnly != sdp.RecvOnly {
|
||||
t.Error(test.name, "RecvOnly doesn't match")
|
||||
}
|
||||
if test.sdp.SendOnly != sdp.SendOnly {
|
||||
t.Error(test.name, "SendOnly doesn't match")
|
||||
}
|
||||
|
||||
if test.sdp.Attrs != nil {
|
||||
if sdp.Attrs == nil {
|
||||
t.Error(test.name, "Attrs weren't extracted")
|
||||
} else if len(sdp.Attrs) != len(test.sdp.Attrs) {
|
||||
t.Error(test.name, "Attrs length not same")
|
||||
} else {
|
||||
for i := range sdp.Attrs {
|
||||
p1, p2 := test.sdp.Attrs[i], sdp.Attrs[i]
|
||||
if p1[0] != p2[0] || p1[1] != p2[1] {
|
||||
t.Error(test.name, "attr", p1, "!=", p2)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if sdp.Attrs != nil {
|
||||
t.Error(test.name, "unexpected attrs", sdp.Attrs)
|
||||
}
|
||||
}
|
||||
|
||||
sdpCompareMedia(t, "Audio", test.sdp.Audio, sdp.Audio)
|
||||
sdpCompareMedia(t, "Video", test.sdp.Video, sdp.Video)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSDP(t *testing.T) {
|
||||
for _, test := range sdpTests {
|
||||
sdp := test.sdp.String()
|
||||
s := test.s
|
||||
if test.s2 != "" {
|
||||
s = test.s2
|
||||
}
|
||||
if s != sdp {
|
||||
t.Error("\n" + test.name + "\n\n" + s + "\nIS NOT\n\n" + sdp)
|
||||
fmt.Printf("%s", sdp)
|
||||
fmt.Printf("pokémon")
|
||||
}
|
||||
}
|
||||
}
|
98
sdp/util.go
Normal file
98
sdp/util.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2020 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package sdp
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Return true if error is ICMP connection refused.
|
||||
func IsRefused(err error) bool {
|
||||
operr, ok := err.(*net.OpError)
|
||||
return ok && operr.Err == syscall.ECONNREFUSED
|
||||
}
|
||||
|
||||
// Return true if error was caused by reading from a closed socket.
|
||||
func IsUseOfClosed(err error) bool {
|
||||
return strings.Contains(err.Error(), "use of closed network connection")
|
||||
}
|
||||
|
||||
// Return true if IP contains a colon.
|
||||
func IsIPv6(ip string) bool {
|
||||
return strings.Index(ip, ":") >= 0
|
||||
}
|
||||
|
||||
// Generate a secure random number between 0 and 50,000.
|
||||
func GenerateCSeq() int {
|
||||
return rand.Int() % 50000
|
||||
}
|
||||
|
||||
// Generate a 48-bit secure random string like: 27c97271d363.
|
||||
func GenerateTag() string {
|
||||
return hex.EncodeToString(randomBytes(6))
|
||||
}
|
||||
|
||||
// Generate a SIP 2.0 Via branch ID. This is probably not suitable for use by
|
||||
// stateless proxies.
|
||||
func GenerateBranch() string {
|
||||
return "z9hG4bK-" + GenerateTag()
|
||||
}
|
||||
|
||||
// Generate a secure UUID4, e.g.f47ac10b-58cc-4372-a567-0e02b2c3d479
|
||||
func GenerateCallID() string {
|
||||
lol := randomBytes(15)
|
||||
digs := hex.EncodeToString(lol)
|
||||
uuid4 := digs[0:8] +
|
||||
"-" + digs[8:12] +
|
||||
"-4" + digs[12:15] +
|
||||
"-a" + digs[15:18] +
|
||||
"-" + digs[18:]
|
||||
return uuid4
|
||||
}
|
||||
|
||||
// Generate a random ID for an SDP.
|
||||
func GenerateOriginID() string {
|
||||
return strconv.FormatUint(uint64(rand.Uint32()), 10)
|
||||
}
|
||||
|
||||
func randomBytes(l int) (b []byte) {
|
||||
b = make([]byte, l)
|
||||
for i := 0; i < l; i++ {
|
||||
b[i] = byte(rand.Int())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*func append(buf []byte, s string) []byte {
|
||||
lenb, lens := len(buf), len(s)
|
||||
if lenb+lens <= cap(buf) {
|
||||
buf = buf[0 : lenb+lens]
|
||||
} else {
|
||||
panic("mtu exceeded D:")
|
||||
}
|
||||
for i := 0; i < lens; i++ {
|
||||
buf[lenb+i] = byte(s[i])
|
||||
}
|
||||
return buf
|
||||
}*/
|
||||
|
||||
func Portstr(port uint16) string {
|
||||
return strconv.FormatInt(int64(port), 10)
|
||||
}
|
Reference in New Issue
Block a user