Skip to content

Commit bdf2c47

Browse files
Pascal BenoitSean-Der
andcommitted
Add sip-to-webrtc
Example demonstrates how to connect to FreeSWITCH and save to disk Co-authored-by: Sean DuBois <sean@siobud.com>
1 parent 8ce7885 commit bdf2c47

File tree

11 files changed

+670
-0
lines changed

11 files changed

+670
-0
lines changed

sip-to-webrtc/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# sip-to-webrtc
2+
sip-to-webrtc demonstrates how you can connect to a SIP over WebRTC endpoint. This example connects to an extension
3+
and saves the audio to a ogg file.
4+
5+
## Instructions
6+
### Setup FreeSWITCH (or SIP over WebSocket Server)
7+
With a fresh install of FreeSWITCH all you need to do is
8+
9+
* Enable `ws-binding`
10+
* Set a `default_password` to something you know
11+
12+
### Run `sip-to-webrtc`
13+
Run `go run *.go -h` to see the arguments of the program. If everything is working
14+
this is the output you will see.
15+
16+
```
17+
$ go run *.go -host 172.17.0.2 -password Aelo1ievoh2oopooTh2paijaeNaidiek
18+
Connection State has changed checking
19+
Connection State has changed connected
20+
Got Opus track, saving to disk as output.ogg
21+
Connection State has changed disconnected
22+
```
23+
24+
### Play the audio file
25+
ffmpeg's in-tree Opus decoder isn't able to play the default audio file from FreeSWITCH. Use the following command to force libopus.
26+
27+
`ffplay -acodec libopus output.ogg`

sip-to-webrtc/main.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
7+
"github.com/pion/example-webrtc-applications/sip-to-webrtc/softphone"
8+
"github.com/pion/sdp/v2"
9+
"github.com/pion/webrtc/v3"
10+
"github.com/pion/webrtc/v3/pkg/media/oggwriter"
11+
)
12+
13+
var (
14+
username = flag.String("username", "1000", "Extension you wish to register as")
15+
password = flag.String("password", "", "Password for the extension you wish to register as")
16+
extension = flag.String("extension", "9198", "Extension you wish to call")
17+
host = flag.String("host", "", "Host that websocket is available on")
18+
port = flag.String("port", "5066", "Port that websocket is available on")
19+
)
20+
21+
func main() {
22+
flag.Parse()
23+
24+
if *host == "" || *port == "" || *password == "" {
25+
panic("-host -port and -password are required")
26+
}
27+
28+
conn := softphone.NewSoftPhone(softphone.SIPInfoResponse{
29+
Username: *username,
30+
AuthorizationID: *username,
31+
Password: *password,
32+
Domain: *host,
33+
Transport: "ws",
34+
OutboundProxy: *host + ":" + *port,
35+
})
36+
37+
pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
38+
if err != nil {
39+
panic(err)
40+
}
41+
42+
pc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
43+
fmt.Printf("Connection State has changed %s \n", connectionState.String())
44+
})
45+
46+
oggFile, err := oggwriter.New("output.ogg", 48000, 2)
47+
if err != nil {
48+
panic(err)
49+
}
50+
51+
pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
52+
fmt.Println("Got Opus track, saving to disk as output.ogg")
53+
54+
for {
55+
rtpPacket, _, readErr := track.ReadRTP()
56+
if readErr != nil {
57+
panic(readErr)
58+
}
59+
if readErr := oggFile.WriteRTP(rtpPacket); readErr != nil {
60+
panic(readErr)
61+
}
62+
}
63+
})
64+
65+
if _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil {
66+
panic(err)
67+
}
68+
69+
offer, err := pc.CreateOffer(nil)
70+
if err != nil {
71+
panic(err)
72+
}
73+
74+
if err := pc.SetLocalDescription(offer); err != nil {
75+
panic(err)
76+
}
77+
78+
gotAnswer := false
79+
80+
conn.OnOK(func(okBody string) {
81+
if gotAnswer {
82+
return
83+
}
84+
gotAnswer = true
85+
86+
okBody += "a=mid:0\r\n"
87+
if err := pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: okBody}); err != nil {
88+
panic(err)
89+
}
90+
})
91+
conn.Invite(*extension, rewriteSDP(offer.SDP))
92+
93+
select {}
94+
}
95+
96+
// Apply the following transformations for FreeSWITCH
97+
// * Add fake srflx candidate to each media section
98+
// * Add msid to each media section
99+
// * Make bundle first attribute at session level.
100+
func rewriteSDP(in string) string {
101+
parsed := &sdp.SessionDescription{}
102+
if err := parsed.Unmarshal([]byte(in)); err != nil {
103+
panic(err)
104+
}
105+
106+
// Reverse global attributes
107+
for i, j := 0, len(parsed.Attributes)-1; i < j; i, j = i+1, j-1 {
108+
parsed.Attributes[i], parsed.Attributes[j] = parsed.Attributes[j], parsed.Attributes[i]
109+
}
110+
111+
parsed.MediaDescriptions[0].Attributes = append(parsed.MediaDescriptions[0].Attributes, sdp.Attribute{
112+
Key: "candidate",
113+
Value: "79019993 1 udp 1686052607 1.1.1.1 9 typ srflx",
114+
})
115+
116+
out, err := parsed.Marshal()
117+
if err != nil {
118+
panic(err)
119+
}
120+
121+
return string(out)
122+
}

sip-to-webrtc/output.ogg

22.7 KB
Binary file not shown.

sip-to-webrtc/softphone/constants.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package softphone
2+
3+
var responseCodes = map[int]string{
4+
100: "Trying",
5+
180: "Ringing",
6+
181: "Call is Being Forwarded",
7+
182: "Queued",
8+
183: "Session Progress",
9+
199: "Early Dialog Terminated",
10+
200: "OK",
11+
202: "Accepted",
12+
204: "No Notification",
13+
300: "Multiple Choices",
14+
301: "Moved Permanently",
15+
302: "Moved Temporarily",
16+
305: "Use Proxy",
17+
380: "Alternative Service",
18+
400: "Bad Request",
19+
401: "Unauthorized",
20+
402: "Payment Required",
21+
403: "Forbidden",
22+
404: "Not Found",
23+
405: "Method Not Allowed",
24+
406: "Not Acceptable",
25+
407: "Proxy Authentication Required",
26+
408: "Request Timeout",
27+
409: "Conflict",
28+
410: "Gone",
29+
411: "Length Required",
30+
412: "Conditional Request Failed",
31+
413: "Request Entity Too Large",
32+
414: "Request-URI Too Long",
33+
415: "Unsupported Media Type",
34+
416: "Unsupported URI Scheme",
35+
417: "Unknown Resource-Priority",
36+
420: "Bad Extension",
37+
421: "Extension Required",
38+
422: "Session Interval Too Small",
39+
423: "Interval Too Brief",
40+
424: "Bad Location Information",
41+
428: "Use Identity Header",
42+
429: "Provide Referrer Identity",
43+
433: "Anonymity Disallowed",
44+
436: "Bad Identity-Info",
45+
437: "Unsupported Certificate",
46+
438: "Invalid Identity Header",
47+
439: "First Hop Lacks Outbound Support",
48+
440: "Max-Breadth Exceeded",
49+
469: "Bad Info Package",
50+
470: "Consent Needed",
51+
480: "Temporarily Unavailable",
52+
481: "Call/Transaction Does Not Exist",
53+
482: "Loop Detected",
54+
483: "Too Many Hops",
55+
484: "Address Incomplete",
56+
485: "Ambiguous",
57+
486: "Busy Here",
58+
487: "Request Terminated",
59+
488: "Not Acceptable Here",
60+
489: "Bad Event",
61+
491: "Request Pending",
62+
493: "Undecipherable",
63+
494: "Security Agreement Required",
64+
500: "Server Internal Error",
65+
501: "Not Implemented",
66+
502: "Bad Gateway",
67+
503: "Service Unavailable",
68+
504: "Server Time-out",
69+
505: "Version Not Supported",
70+
513: "Message Too Large",
71+
580: "Precondition Failure",
72+
600: "Busy Everywhere",
73+
603: "Decline",
74+
604: "Does Not Exist Anywhere",
75+
606: "Not Acceptable",
76+
607: "Unwanted",
77+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package softphone
2+
3+
import (
4+
"encoding/xml"
5+
"fmt"
6+
"log"
7+
"strings"
8+
)
9+
10+
// OpenToInvite adds a handler that responds to any incoming invites.
11+
func (softphone *Softphone) OpenToInvite() {
12+
softphone.inviteKey = softphone.addMessageListener(func(message string) {
13+
if strings.HasPrefix(message, "INVITE sip:") {
14+
inviteMessage := SIPMessage{}.FromString(message)
15+
16+
dict := map[string]string{"Contact": fmt.Sprintf(`<sip:%s;transport=ws>`, softphone.fakeDomain)}
17+
responseMsg := inviteMessage.Response(*softphone, 180, dict, "")
18+
softphone.response(responseMsg)
19+
20+
var msg Msg
21+
if err := xml.Unmarshal([]byte(inviteMessage.headers["P-rc"]), &msg); err != nil {
22+
log.Panic(err)
23+
}
24+
sipMessage := SIPMessage{}
25+
sipMessage.method = "MESSAGE"
26+
sipMessage.address = msg.Hdr.From
27+
sipMessage.headers = make(map[string]string)
28+
sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WSS %s;branch=%s", softphone.fakeDomain, branch())
29+
sipMessage.headers["From"] = fmt.Sprintf("<sip:%s@%s>;tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag)
30+
sipMessage.headers["To"] = fmt.Sprintf("<sip:%s>", msg.Hdr.From)
31+
sipMessage.headers["Content-Type"] = "x-rc/agent"
32+
sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent()
33+
sipMessage.Body = fmt.Sprintf(`<Msg><Hdr SID="%s" Req="%s" From="%s" To="%s" Cmd="17"/><Bdy Cln="%s"/></Msg>`, msg.Hdr.SID, msg.Hdr.Req, msg.Hdr.To, msg.Hdr.From, softphone.sipInfo.AuthorizationID)
34+
softphone.request(sipMessage, nil)
35+
36+
softphone.OnInvite(inviteMessage)
37+
}
38+
})
39+
}
40+
41+
// CloseToInvite removes the previously set invite listener.
42+
func (softphone *Softphone) CloseToInvite() {
43+
softphone.removeMessageListener(softphone.inviteKey)
44+
}
45+
46+
// OnOK adds a handler that responds to any incoming ok events.
47+
func (softphone *Softphone) OnOK(hdlr func(string)) {
48+
softphone.addMessageListener(func(message string) {
49+
if strings.HasPrefix(message, "SIP/2.0 200 OK") {
50+
parsed := SIPMessage{}.FromString(message)
51+
hdlr(parsed.Body)
52+
}
53+
})
54+
}

sip-to-webrtc/softphone/invite.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package softphone
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
)
7+
8+
// Invite ...
9+
func (softphone *Softphone) Invite(extension, offer string) {
10+
sipMessage := SIPMessage{headers: map[string]string{}}
11+
12+
sipMessage.method = "INVITE"
13+
sipMessage.address = softphone.sipInfo.Domain
14+
15+
sipMessage.headers["Contact"] = fmt.Sprintf("<sip:%s;transport=ws>;expires=200", softphone.FakeEmail)
16+
sipMessage.headers["To"] = fmt.Sprintf("<sip:%s@%s>", extension, softphone.sipInfo.Domain)
17+
sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WS %s;branch=%s", softphone.fakeDomain, branch())
18+
sipMessage.headers["From"] = fmt.Sprintf("<sip:%s@%s>;tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag)
19+
sipMessage.headers["Supported"] = "replaces, outbound,ice"
20+
sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent()
21+
22+
sipMessage.headers["Content-Type"] = "application/sdp"
23+
sipMessage.Body = offer
24+
25+
softphone.request(sipMessage, func(message string) bool {
26+
authenticateHeader := SIPMessage{}.FromString(message).headers["Proxy-Authenticate"]
27+
regex := regexp.MustCompile(`, nonce="(.+?)"`)
28+
nonce := regex.FindStringSubmatch(authenticateHeader)[1]
29+
30+
sipMessage.addProxyAuthorization(*softphone, nonce, extension, "INVITE").addCseq(softphone).newViaBranch()
31+
softphone.request(sipMessage, func(msg string) bool {
32+
return false
33+
})
34+
35+
return true
36+
})
37+
}

sip-to-webrtc/softphone/rcmessage.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package softphone
2+
3+
import "encoding/xml"
4+
5+
// Msg ...
6+
type Msg struct {
7+
XMLName xml.Name `xml:"Msg"`
8+
Hdr Hdr `xml:"Hdr"`
9+
Bdy Bdy `xml:"Bdy"`
10+
}
11+
12+
// Hdr ...
13+
type Hdr struct {
14+
XMLName xml.Name `xml:"Hdr"`
15+
SID string `xml:"SID,attr"`
16+
Req string `xml:"Req,attr"`
17+
From string `xml:"From,attr"`
18+
To string `xml:"To,attr"`
19+
Cmd string `xml:"Cmd,attr"`
20+
}
21+
22+
// Bdy ...
23+
type Bdy struct {
24+
XMLName xml.Name `xml:"Bdy"`
25+
SrvLvl string `xml:"SrvLvl,attr"`
26+
SrvLvlExt string `xml:"SrvLvlExt,attr"`
27+
Phn string `xml:"Phn,attr"`
28+
Nm string `xml:"Nm,attr"`
29+
ToPhn string `xml:"ToPhn,attr"`
30+
ToNm string `xml:"ToNm,attr"`
31+
RecURL string `xml:"RecUrl,attr"`
32+
}

0 commit comments

Comments
 (0)