Skip to content

Commit

Permalink
Add sip-to-webrtc
Browse files Browse the repository at this point in the history
Example demonstrates how to connect to FreeSWITCH and save to disk

Co-authored-by: Sean DuBois <sean@siobud.com>
  • Loading branch information
Pascal Benoit and Sean-Der committed Dec 18, 2020
1 parent 8ce7885 commit 52d9a2d
Show file tree
Hide file tree
Showing 11 changed files with 649 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contribu
* [Cameron Elliott](https://github.com/cameronelliott) - *Small race bug fix*
* [Jamie Good](https://github.com/jamiegood) - *Bug fix in jsfiddle example*
* [PhVHoang](https://github.com/PhVHoang)
* [Pascal Benoit](https://github.com/pascal-ace)

### License
MIT License - see [LICENSE](LICENSE) for full text
27 changes: 27 additions & 0 deletions sip-to-webrtc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# sip-to-webrtc
sip-to-webrtc demonstrates how you can connect to a SIP over WebRTC endpoint. This example connects to an extension
and saves the audio to a ogg file.

## Instructions
### Setup FreeSWITCH (or SIP over WebSocket Server)
With a fresh install of FreeSWITCH all you need to do is

* Enable `ws-binding`
* Set a `default_password` to something you know

### Run `sip-to-webrtc`
Run `go run *.go -h` to see the arguments of the program. If everything is working
this is the output you will see.

```
$ go run *.go -host 172.17.0.2 -password Aelo1ievoh2oopooTh2paijaeNaidiek
Connection State has changed checking
Connection State has changed connected
Got Opus track, saving to disk as output.ogg
Connection State has changed disconnected
```

### Play the audio file
ffmpeg's in-tree Opus decoder isn't able to play the default audio file from FreeSWITCH. Use the following command to force libopus.

`ffplay -acodec libopus output.ogg`
122 changes: 122 additions & 0 deletions sip-to-webrtc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package main

import (
"flag"
"fmt"

"github.com/pion/example-webrtc-applications/sip-to-webrtc/softphone"
"github.com/pion/sdp/v2"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media/oggwriter"
)

var (
username = flag.String("username", "1000", "Extension you wish to register as")
password = flag.String("password", "", "Password for the extension you wish to register as")
extension = flag.String("extension", "9198", "Extension you wish to call")
host = flag.String("host", "", "Host that websocket is available on")
port = flag.String("port", "5066", "Port that websocket is available on")
)

func main() {
flag.Parse()

if *host == "" || *port == "" || *password == "" {
panic("-host -port and -password are required")
}

conn := softphone.NewSoftPhone(softphone.SIPInfoResponse{
Username: *username,
AuthorizationID: *username,
Password: *password,
Domain: *host,
Transport: "ws",
OutboundProxy: *host + ":" + *port,
})

pc, err := webrtc.NewPeerConnection(webrtc.Configuration{})
if err != nil {
panic(err)
}

pc.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed %s \n", connectionState.String())
})

oggFile, err := oggwriter.New("output.ogg", 48000, 2)
if err != nil {
panic(err)
}

pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
fmt.Println("Got Opus track, saving to disk as output.ogg")

for {
rtpPacket, _, readErr := track.ReadRTP()
if readErr != nil {
panic(readErr)
}
if readErr := oggFile.WriteRTP(rtpPacket); readErr != nil {
panic(readErr)
}
}
})

if _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio); err != nil {
panic(err)
}

offer, err := pc.CreateOffer(nil)
if err != nil {
panic(err)
}

if err := pc.SetLocalDescription(offer); err != nil {
panic(err)
}

gotAnswer := false

conn.OnOK(func(okBody string) {
if gotAnswer {
return
}
gotAnswer = true

okBody += "a=mid:0\r\n"
if err := pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: okBody}); err != nil {
panic(err)
}
})
conn.Invite(*extension, rewriteSDP(offer.SDP))

select {}
}

// Apply the following transformations for FreeSWITCH
// * Add fake srflx candidate to each media section
// * Add msid to each media section
// * Make bundle first attribute at session level.
func rewriteSDP(in string) string {
parsed := &sdp.SessionDescription{}
if err := parsed.Unmarshal([]byte(in)); err != nil {
panic(err)
}

// Reverse global attributes
for i, j := 0, len(parsed.Attributes)-1; i < j; i, j = i+1, j-1 {
parsed.Attributes[i], parsed.Attributes[j] = parsed.Attributes[j], parsed.Attributes[i]
}

parsed.MediaDescriptions[0].Attributes = append(parsed.MediaDescriptions[0].Attributes, sdp.Attribute{
Key: "candidate",
Value: "79019993 1 udp 1686052607 1.1.1.1 9 typ srflx",
})

out, err := parsed.Marshal()
if err != nil {
panic(err)
}

return string(out)
}
77 changes: 77 additions & 0 deletions sip-to-webrtc/softphone/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package softphone

var responseCodes = map[int]string{
100: "Trying",
180: "Ringing",
181: "Call is Being Forwarded",
182: "Queued",
183: "Session Progress",
199: "Early Dialog Terminated",
200: "OK",
202: "Accepted",
204: "No Notification",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
305: "Use Proxy",
380: "Alternative Service",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Conditional Request Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Unsupported URI Scheme",
417: "Unknown Resource-Priority",
420: "Bad Extension",
421: "Extension Required",
422: "Session Interval Too Small",
423: "Interval Too Brief",
424: "Bad Location Information",
428: "Use Identity Header",
429: "Provide Referrer Identity",
433: "Anonymity Disallowed",
436: "Bad Identity-Info",
437: "Unsupported Certificate",
438: "Invalid Identity Header",
439: "First Hop Lacks Outbound Support",
440: "Max-Breadth Exceeded",
469: "Bad Info Package",
470: "Consent Needed",
480: "Temporarily Unavailable",
481: "Call/Transaction Does Not Exist",
482: "Loop Detected",
483: "Too Many Hops",
484: "Address Incomplete",
485: "Ambiguous",
486: "Busy Here",
487: "Request Terminated",
488: "Not Acceptable Here",
489: "Bad Event",
491: "Request Pending",
493: "Undecipherable",
494: "Security Agreement Required",
500: "Server Internal Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Server Time-out",
505: "Version Not Supported",
513: "Message Too Large",
580: "Precondition Failure",
600: "Busy Everywhere",
603: "Decline",
604: "Does Not Exist Anywhere",
606: "Not Acceptable",
607: "Unwanted",
}
54 changes: 54 additions & 0 deletions sip-to-webrtc/softphone/inboundcall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package softphone

import (
"encoding/xml"
"fmt"
"log"
"strings"
)

// OpenToInvite adds a handler that responds to any incoming invites.
func (softphone *Softphone) OpenToInvite() {
softphone.inviteKey = softphone.addMessageListener(func(message string) {
if strings.HasPrefix(message, "INVITE sip:") {
inviteMessage := SIPMessage{}.FromString(message)

dict := map[string]string{"Contact": fmt.Sprintf(`<sip:%s;transport=ws>`, softphone.fakeDomain)}
responseMsg := inviteMessage.Response(*softphone, 180, dict, "")
softphone.response(responseMsg)

var msg Msg
if err := xml.Unmarshal([]byte(inviteMessage.headers["P-rc"]), &msg); err != nil {
log.Panic(err)
}
sipMessage := SIPMessage{}
sipMessage.method = "MESSAGE"
sipMessage.address = msg.Hdr.From
sipMessage.headers = make(map[string]string)
sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WSS %s;branch=%s", softphone.fakeDomain, branch())
sipMessage.headers["From"] = fmt.Sprintf("<sip:%s@%s>;tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag)
sipMessage.headers["To"] = fmt.Sprintf("<sip:%s>", msg.Hdr.From)
sipMessage.headers["Content-Type"] = "x-rc/agent"
sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent()
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)
softphone.request(sipMessage, nil)

softphone.OnInvite(inviteMessage)
}
})
}

// CloseToInvite removes the previously set invite listener.
func (softphone *Softphone) CloseToInvite() {
softphone.removeMessageListener(softphone.inviteKey)
}

// OnOK adds a handler that responds to any incoming ok events.
func (softphone *Softphone) OnOK(hdlr func(string)) {
softphone.addMessageListener(func(message string) {
if strings.HasPrefix(message, "SIP/2.0 200 OK") {
parsed := SIPMessage{}.FromString(message)
hdlr(parsed.Body)
}
})
}
37 changes: 37 additions & 0 deletions sip-to-webrtc/softphone/invite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package softphone

import (
"fmt"
"regexp"
)

// Invite ...
func (softphone *Softphone) Invite(extension, offer string) {
sipMessage := SIPMessage{headers: map[string]string{}}

sipMessage.method = "INVITE"
sipMessage.address = softphone.sipInfo.Domain

sipMessage.headers["Contact"] = fmt.Sprintf("<sip:%s;transport=ws>;expires=200", softphone.FakeEmail)
sipMessage.headers["To"] = fmt.Sprintf("<sip:%s@%s>", extension, softphone.sipInfo.Domain)
sipMessage.headers["Via"] = fmt.Sprintf("SIP/2.0/WS %s;branch=%s", softphone.fakeDomain, branch())
sipMessage.headers["From"] = fmt.Sprintf("<sip:%s@%s>;tag=%s", softphone.sipInfo.Username, softphone.sipInfo.Domain, softphone.fromTag)
sipMessage.headers["Supported"] = "replaces, outbound,ice"
sipMessage.addCseq(softphone).addCallID(*softphone).addUserAgent()

sipMessage.headers["Content-Type"] = "application/sdp"
sipMessage.Body = offer

softphone.request(sipMessage, func(message string) bool {
authenticateHeader := SIPMessage{}.FromString(message).headers["Proxy-Authenticate"]
regex := regexp.MustCompile(`, nonce="(.+?)"`)
nonce := regex.FindStringSubmatch(authenticateHeader)[1]

sipMessage.addProxyAuthorization(*softphone, nonce, extension, "INVITE").addCseq(softphone).newViaBranch()
softphone.request(sipMessage, func(msg string) bool {
return false
})

return true
})
}
32 changes: 32 additions & 0 deletions sip-to-webrtc/softphone/rcmessage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package softphone

import "encoding/xml"

// Msg ...
type Msg struct {
XMLName xml.Name `xml:"Msg"`
Hdr Hdr `xml:"Hdr"`
Bdy Bdy `xml:"Bdy"`
}

// Hdr ...
type Hdr struct {
XMLName xml.Name `xml:"Hdr"`
SID string `xml:"SID,attr"`
Req string `xml:"Req,attr"`
From string `xml:"From,attr"`
To string `xml:"To,attr"`
Cmd string `xml:"Cmd,attr"`
}

// Bdy ...
type Bdy struct {
XMLName xml.Name `xml:"Bdy"`
SrvLvl string `xml:"SrvLvl,attr"`
SrvLvlExt string `xml:"SrvLvlExt,attr"`
Phn string `xml:"Phn,attr"`
Nm string `xml:"Nm,attr"`
ToPhn string `xml:"ToPhn,attr"`
ToNm string `xml:"ToNm,attr"`
RecURL string `xml:"RecUrl,attr"`
}

0 comments on commit 52d9a2d

Please sign in to comment.