Create a RingCentral Softphone in GoLang

Tyler Liu
RingCentral Developers
7 min readApr 13, 2021

I have written several articles on this topic which include How to Programmatically Create a RingCentral Device, PJSIP and RingCentral (part 1, part 2, part 3, part 4) and Use SIP over TCP and RTP to create a RingCentral device. Today I am going to write one on creating a softphone, because is not an easy task and it deserves several good articles or tutorials. This time we will use a new programming language: GoLang. I’d like to divide this article into 3 sub topics:

  1. SIP Message Format
  2. Softphone Registration
  3. Pion WebRTC

SIP Message Format

It is very important to get the SIP message format correct. The SIP server will not respond if you send the message in the wrong format. First and foremost, line breaks in a SIP message is \r\n instead of \n or \r. It is a tricky pitfall. I wasted days troubleshooting weird issues and finally it turned out to be the line breaks issue.

A valid SIP message consists of 3 parts: the subject, the headers and the body. Let me show you a sample SIP message:

INVITE sip:3cea8c06-e425-424e-b775-d41d84893e7a@147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;transport=ws SIP/2.0
Via: SIP/2.0/WSS 199.255.120.223:8083;branch=z9hG4bK5Z5LTdovOUy
To: "WIRELESS CALLER" <sip:17206666666*11115@98.33.76.34>
From: "WIRELESS CALLER" <sip:+16506666666@199.255.120.223>;tag=10.13.123.201-5070-51f012c3-aa83-42a
Call-Id: 90370e2a-dfe7-4f5a-8a4d-c5bc3b65fc71
CSeq: 470226345 INVITE
Max-Forwards: 67
Contact: <sip:+16506666666@199.255.120.223:8083;transport=wss>
Content-Type: application/sdp
Call-Info: <358759047_52929943@10.14.116.50>;purpose=info
P-rc: <Msg><Hdr SID="35741438756848" Req="168655817232901423367701" Cmd="6" From="#1523016@sip.ringcentral.com:5060" To="17206666666*11115"/><Bdy SrvLvl="-149699523" SrvLvlExt="406" Phn="+16506666666" Nm="WIRELESS CALLER" ToPhn="+16502886382" ToNm="Tyler Liu" RecUrl=""/></Msg>
p-rc-api-call-info: callAttributes=reject,send-vm
p-rc-api-ids: party-id=p-0a7c8d86db8a434fa4a0d029a9909f19-2;session-id=s-0a7c8d86db8a434fa4a0d029a9909f19
User-Agent: RC_SIPWRP_123.201
Content-Length: 877
v=0
o=- 5880443394298904479 7252956430269573836 IN IP4 199.255.120.232
s=SmcSip
c=IN IP4 199.255.120.232
t=0 0
m=audio 21886 RTP/SAVPF 109 111 18 0 8 9 96 101
a=rtpmap:109 OPUS/16000
a=fmtp:109 useinbandfec=1
a=rtcp-fb:109 ccm tmmbr
a=rtpmap:111 OPUS/48000/2
a=fmtp:111 useinbandfec=1
a=rtcp-fb:111 ccm tmmbr
a=rtpmap:18 g729/8000
a=fmtp:18 annexb=no
a=rtpmap:0 pcmu/8000
a=rtpmap:8 pcma/8000
a=rtpmap:9 g722/8000
a=rtpmap:96 ilbc/8000
a=fmtp:96 mode=20
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
a=sendrecv
a=rtcp:21887
a=rtcp-mux
a=setup:actpass
a=fingerprint:sha-1 E0:B2:83:1B:E2:0A:19:8D:A2:8C:CE:B3:60:3A:BC:17:83:67:28:81
a=ice-ufrag:MQSXD3LG
a=ice-pwd:eLQMLNpXeCXEfoU38A6xzBCRLk
a=candidate:lVt5YyAxexOkz5l5 1 UDP 2130706431 199.255.120.232 21886 typ host
a=candidate:lVt5YyAxexOkz5l5 2 UDP 2130706430 199.255.120.232 21887 typ host

The subject is the first line: INVITE sip:3cea8c06-e425–424e-b775-d41d84893e7a@147b2c77–4f94–4572–85a1-fbdb2efb6aff.invalid;transport=ws SIP/2.0 The very first message in a sequence usually starts with a verb, such as INVITE and REGISTER. Messages replying to verb messages don’t start with a verb, but contain a status code and a status message, such as SIP/2.0 100 Trying , SIP/2.0 401 Unauthorized and SIP/2.0 200 OK

The headers consist of several key value pairs:

Via: SIP/2.0/WSS 199.255.120.223:8083;branch=z9hG4bK5Z5LTdovOUy
To: "WIRELESS CALLER" <sip:17206666666*11115@98.33.76.34>
From: "WIRELESS CALLER" <sip:+16506666666@199.255.120.223>;tag=10.13.123.201-5070-51f012c3-aa83-42a
Call-Id: 90370e2a-dfe7-4f5a-8a4d-c5bc3b65fc71
CSeq: 470226345 INVITE
Max-Forwards: 67
Contact: <sip:+16506666666@199.255.120.223:8083;transport=wss>
Content-Type: application/sdp
Call-Info: <358759047_52929943@10.14.116.50>;purpose=info
P-rc: <Msg><Hdr SID="35741438756848" Req="168655817232901423367701" Cmd="6" From="#1523016@sip.ringcentral.com:5060" To="17206666666*11115"/><Bdy SrvLvl="-149699523" SrvLvlExt="406" Phn="+16506666666" Nm="WIRELESS CALLER" ToPhn="+16502886382" ToNm="Tyler Liu" RecUrl=""/></Msg>
p-rc-api-call-info: callAttributes=reject,send-vm
p-rc-api-ids: party-id=p-0a7c8d86db8a434fa4a0d029a9909f19-2;session-id=s-0a7c8d86db8a434fa4a0d029a9909f19
User-Agent: RC_SIPWRP_123.201
Content-Length: 877

Headers from the SIP server contains quite a lot of magic custom information, such as P-rc ,p-rc-api-ids and p-rc-api-call-info. We don’t need them in this article, and to be frank I don’t know what they are useful for, potentially some RingCentral apps may need them to function properly. Some of the headers information could be deduced from the body, such as Content-Type and Content-Length. When you build a SIP message from scratch, don’t forget those body deduced header entries.

The body is separated from the headers by one blank line:

v=0
o=- 5880443394298904479 7252956430269573836 IN IP4 199.255.120.232
s=SmcSip
c=IN IP4 199.255.120.232
t=0 0
m=audio 21886 RTP/SAVPF 109 111 18 0 8 9 96 101
a=rtpmap:109 OPUS/16000
a=fmtp:109 useinbandfec=1
a=rtcp-fb:109 ccm tmmbr
a=rtpmap:111 OPUS/48000/2
a=fmtp:111 useinbandfec=1
a=rtcp-fb:111 ccm tmmbr
a=rtpmap:18 g729/8000
a=fmtp:18 annexb=no
a=rtpmap:0 pcmu/8000
a=rtpmap:8 pcma/8000
a=rtpmap:9 g722/8000
a=rtpmap:96 ilbc/8000
a=fmtp:96 mode=20
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-15
a=sendrecv
a=rtcp:21887
a=rtcp-mux
a=setup:actpass
a=fingerprint:sha-1 E0:B2:83:1B:E2:0A:19:8D:A2:8C:CE:B3:60:3A:BC:17:83:67:28:81
a=ice-ufrag:MQSXD3LG
a=ice-pwd:eLQMLNpXeCXEfoU38A6xzBCRLk
a=candidate:lVt5YyAxexOkz5l5 1 UDP 2130706431 199.255.120.232 21886 typ host
a=candidate:lVt5YyAxexOkz5l5 2 UDP 2130706430 199.255.120.232 21887 typ host

Please note that, the body could be an empty string. And even when it is an empty string, it still needs to be separated from the headers by a blank line, thus resulting in two blanks lines at the end of the message. Otherwise it simply won’t work. Some SIP servers might be more tolerant, but RingCentral SIP doesn’t tolerate it.

Softphone Registration

This part is similar to what we did in How to Programmatically Create a RingCentral Device. Make sure to read that article for a refresher — here I will just show you a sample message flow and some key source code in GoLang.

REGISTER sip:sip.ringcentral.com SIP/2.0
CSeq: 8082 REGISTER
Call-ID: 2f8d335a-037b-429f-ab91-3927bc98b2a2
Contact: <sip:3cea8c06-e425-424e-b775-d41d84893e7a@147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;transport=ws>;expires=600
Via: SIP/2.0/WSS 147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;branch=z9hG4bKe328d513-9671-415d-add2-f2d3d760d4b0
From: <sip:17206666666*11115@sip.ringcentral.com>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
To: <sip:17206666666*11115@sip.ringcentral.com>
Content-Length: 0
User-Agent: github.com/ringcentral/ringcentral-softphone-go
2021/02/23 09:20:43 ↓↓↓
SIP/2.0 100 Trying
Via: SIP/2.0/WSS 147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;branch=z9hG4bKe328d513-9671-415d-add2-f2d3d760d4b0;received=98.33.76.34
To: <sip:17206666666*11115@sip.ringcentral.com>
From: <sip:17206666666*11115@sip.ringcentral.com>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
Call-ID: 2f8d335a-037b-429f-ab91-3927bc98b2a2
CSeq: 8082 REGISTER
Content-Length: 0
2021/02/23 09:20:43 ↓↓↓
SIP/2.0 401 Unauthorized
Via: SIP/2.0/WSS 147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;branch=z9hG4bKe328d513-9671-415d-add2-f2d3d760d4b0;received=98.33.76.34
To: <sip:17206666666*11115@sip.ringcentral.com>;tag=78ea291e5127de07f0581c77565a331b-46b0
From: <sip:17206666666*11115@sip.ringcentral.com>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
Call-ID: 2f8d335a-037b-429f-ab91-3927bc98b2a2
CSeq: 8082 REGISTER
WWW-Authenticate: Digest realm="sip.ringcentral.com", nonce="YDU6l2A1OWtVYiAg/aSv5gkAVmQxjhN5"
Content-Length: 0
2021/02/23 09:20:43 ↑↑↑
REGISTER sip:sip.ringcentral.com SIP/2.0
Call-ID: 2f8d335a-037b-429f-ab91-3927bc98b2a2
Via: SIP/2.0/TCP 147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;branch=z9hG4bK373f2f6c-220b-43dc-9e04-4921336965e5
From: <sip:17206666666*11115@sip.ringcentral.com>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
Content-Length: 0
Authorization: Digest algorithm=MD5, username="802398804016", realm="sip.ringcentral.com", nonce="YDU6l2A1OWtVYiAg/aSv5gkAVmQxjhN5", uri="sip:sip.ringcentral.com", response="ddc9fa7d6f41b3865dc0be334800aa1c"
CSeq: 8083 REGISTER
Contact: <sip:3cea8c06-e425-424e-b775-d41d84893e7a@147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;transport=ws>;expires=600
To: <sip:17206666666*11115@sip.ringcentral.com>
User-Agent: github.com/ringcentral/ringcentral-softphone-go
2021/02/23 09:20:43 ↓↓↓
SIP/2.0 100 Trying
Via: SIP/2.0/TCP 147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;branch=z9hG4bK373f2f6c-220b-43dc-9e04-4921336965e5;received=98.33.76.34
To: <sip:17206666666*11115@sip.ringcentral.com>
From: <sip:17206666666*11115@sip.ringcentral.com>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
Call-ID: 2f8d335a-037b-429f-ab91-3927bc98b2a2
CSeq: 8083 REGISTER
Content-Length: 0
2021/02/23 09:20:43 ↓↓↓
SIP/2.0 200 OK
Via: SIP/2.0/TCP 147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;branch=z9hG4bK373f2f6c-220b-43dc-9e04-4921336965e5;received=98.33.76.34
To: <sip:17206666666*11115@sip.ringcentral.com>;tag=2a77cbf960ac867417503d7f78a9c521-05f6
From: <sip:17206666666*11115@sip.ringcentral.com>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
Call-ID: 2f8d335a-037b-429f-ab91-3927bc98b2a2
CSeq: 8083 REGISTER
Contact: <sip:3cea8c06-e425-424e-b775-d41d84893e7a@147b2c77-4f94-4572-85a1-fbdb2efb6aff.invalid;transport=ws>;expires=52
Content-Length: 0

GoLang Source code to generate Authorization header based on nonce value:

// GenerateResponse generate response field in the authorization header
func GenerateResponse(username string, password string, realm string, method string, uri string, nonce string) string {
ha1 := md5.Sum([]byte(fmt.Sprintf("%s:%s:%s", username, realm, password)))
ha2 := md5.Sum([]byte(fmt.Sprintf("%s:%s", method, uri)))
response := md5.Sum([]byte(fmt.Sprintf("%x:%s:%x", ha1, nonce, ha2)))
return fmt.Sprintf("%x", response)
}
// GenerateAuthorization generate the authorization header
func GenerateAuthorization(sipInfo ringcentral.SIPInfoResponse, method string, nonce string) string {
return fmt.Sprintf(
`Digest algorithm=MD5, username="%s", realm="%s", nonce="%s", uri="sip:%s", response="%s"`,
sipInfo.AuthorizationId, sipInfo.Domain, nonce, sipInfo.Domain,
GenerateResponse(sipInfo.AuthorizationId, sipInfo.Password, sipInfo.Domain, method, "sip:"+sipInfo.Domain, nonce),
)
}

Pion WebRTC

Pion WebRTC is a pure Go implementation of the WebRTC API. I will show you some code to make Pion WebRTC work with RingCentral. The first step is to create the peerConnection with mediaEngine:

Then we need to change the SDP body a little bit:

var re = regexp.MustCompile(`\r\na=rtpmap:111 OPUS/48000/2\r\n`)
sdp := re.ReplaceAllString(inviteMessage.Body, "\r\na=rtpmap:111 OPUS/48000/2\r\na=mid:0\r\n")

The change above is to workaround an issue in the Pion WebRTC. Then we need to set the offer and answer for the SIP:

Pay attention to the GatheringCompletePromise function:

GatheringCompletePromise is a Pion specific helper function that returns a channel that is closed when gathering is complete.This function may be helpful in cases where you are unable to trickle your ICE Candidates. It is better to not use this function, and instead trickle candidates. If you use this function you will see longer connection startup times. When the call is connected you will see no impact however.

Lastly, we need to handle the incoming audio track:

peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
log.Println("OnTrack")
if softphone.OnTrack != nil {
softphone.OnTrack(track)
}
})

How to deal with the audio track depends on your business requirements. For example, you can save the audio as a file:

Source Code

For runnable and tested source code, please refer to the RingCentral Softphone SDK for GoLang. And normally you don’t have to implement a softphone from scratch, you just use our SDK to create a softphone quickly:

Summary

In today’s article, we went through the technical details to create a RingCentral softphone in GoLang. We started with an introduction to the SIP message structure, followed by a brief explanation of SIP registration process. And at last we shared some technical details about Pion WebRTC. Thank you for reading.

Please let us know what you think by leaving your questions and comments below. To learn even more about other features we have make sure to visit our developer site and if you’re ever stuck make sure to go to our developer forum.

Want to stay up to date and in the know about new APIs and features? Join our Game Changer Program and earn great rewards for building your skills and learning more about RingCentral!

--

--