-
Notifications
You must be signed in to change notification settings - Fork 244
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Example demonstrates how to connect to FreeSWITCH and save to disk Co-authored-by: Sean DuBois <sean@siobud.com>
- Loading branch information
Showing
11 changed files
with
649 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
Oops, something went wrong.