Я написал несколько статей на эту тему, в том числе Как программно создать устройство RingCentral, PJSIP и RingCentral (часть 1, часть 2, часть 3, часть 4) и Использование SIP поверх TCP. и RTP для создания устройства RingCentral . Сегодня я напишу один о создании софтфона, потому что это непростая задача, и она заслуживает нескольких хороших статей или руководств. На этот раз мы будем использовать новый язык программирования: GoLang. Я хотел бы разделить эту статью на 3 подтемы:

  1. Формат сообщения SIP
  2. Регистрация софтфона
  3. Пион WebRTC

Формат сообщения SIP

Очень важно правильно настроить формат сообщения SIP. SIP-сервер не ответит, если вы отправите сообщение в неправильном формате. Прежде всего, разрывы строк в сообщении SIP - это \r\n вместо \n или \r. Это хитрая ловушка. Я потратил дни на устранение странных проблем, и, наконец, это оказалось проблемой разрывов строк.

Правильное сообщение SIP состоит из 3 частей: темы, заголовков и тела. Позвольте мне показать вам образец сообщения SIP:

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*[email protected]>
From: "WIRELESS CALLER" <sip:[email protected]>;tag=10.13.123.201-5070-51f012c3-aa83-42a
Call-Id: 90370e2a-dfe7-4f5a-8a4d-c5bc3b65fc71
CSeq: 470226345 INVITE
Max-Forwards: 67
Contact: <sip:[email protected]:8083;transport=wss>
Content-Type: application/sdp
Call-Info: <[email protected]>;purpose=info
P-rc: <Msg><Hdr SID="35741438756848" Req="168655817232901423367701" Cmd="6" From="#[email protected]: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

Тема - это первая строка: INVITE sip:3cea8c06-e425–424e-b775-d41d84893e7a@147b2c77–4f94–4572–85a1-fbdb2efb6aff.invalid;transport=ws SIP/2.0 Самое первое сообщение в последовательности обычно начинается с глагола, например INVITE и REGISTER. Сообщения, отвечающие на глагольные сообщения, не начинаются с глагола, но содержат код статуса и сообщение статуса, например SIP/2.0 100 Trying, SIP/2.0 401 Unauthorized и SIP/2.0 200 OK

Заголовки состоят из нескольких пар ключ-значение:

Via: SIP/2.0/WSS 199.255.120.223:8083;branch=z9hG4bK5Z5LTdovOUy
To: "WIRELESS CALLER" <sip:17206666666*[email protected]>
From: "WIRELESS CALLER" <sip:[email protected]>;tag=10.13.123.201-5070-51f012c3-aa83-42a
Call-Id: 90370e2a-dfe7-4f5a-8a4d-c5bc3b65fc71
CSeq: 470226345 INVITE
Max-Forwards: 67
Contact: <sip:[email protected]:8083;transport=wss>
Content-Type: application/sdp
Call-Info: <[email protected]>;purpose=info
P-rc: <Msg><Hdr SID="35741438756848" Req="168655817232901423367701" Cmd="6" From="#[email protected]: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

Заголовки с SIP-сервера содержат довольно много волшебной пользовательской информации, такой как P-rc, p-rc-api-ids и p-rc-api-call-info. В этой статье они нам не нужны, и, честно говоря, я не знаю, для чего они полезны. Возможно, некоторым приложениям RingCentral они могут понадобиться для правильной работы. Некоторая информация заголовков может быть выведена из тела, например Content-Type и Content-Length. Создавая SIP-сообщение с нуля, не забывайте, что эти заголовки выводятся из тела.

Тело отделяется от заголовков одной пустой строкой:

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

Обратите внимание, что тело может быть пустой строкой. И даже если это пустая строка, ее все равно нужно отделить от заголовков пустой строкой, что приведет к появлению двух пустых строк в конце сообщения. Иначе просто не получится. Некоторые SIP-серверы могут быть более терпимыми, но RingCentral SIP этого не терпит.

Регистрация софтфона

Эта часть аналогична тому, что мы делали в Как программно создать устройство RingCentral. Обязательно прочитайте эту статью, чтобы освежить в памяти изменения - здесь я просто покажу вам пример потока сообщений и некоторый ключевой исходный код на 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*[email protected]>;tag=b8731b49-0e4a-435b-abd0-273967eb27e2
To: <sip:17206666666*[email protected]>
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*[email protected]>
From: <sip:17206666666*[email protected]>;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*[email protected]>;tag=78ea291e5127de07f0581c77565a331b-46b0
From: <sip:17206666666*[email protected]>;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*[email protected]>;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*[email protected]>
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*[email protected]>
From: <sip:17206666666*[email protected]>;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*[email protected]>;tag=2a77cbf960ac867417503d7f78a9c521-05f6
From: <sip:17206666666*[email protected]>;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 для создания заголовка Authorization на основе значения nonce:

// 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),
 )
}

Пион WebRTC

Pion WebRTC - это чистая Go реализация API WebRTC. Я покажу вам код, чтобы заставить Pion WebRTC работать с RingCentral. Первый шаг - создать peerConnection с mediaEngine:

Затем нам нужно немного изменить тело SDP:

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")

Вышеупомянутое изменение предназначено для обхода проблемы в Pion WebRTC. Затем нам нужно установить предложение и ответ для SIP:

Обратите внимание на функцию GatheringCompletePromise:

GatheringCompletePromise - это специальная вспомогательная функция для Pion, которая возвращает канал, который закрывается после завершения сбора. Эта функция может быть полезна в тех случаях, когда вы не можете получить своих ICE-кандидатов. Лучше не использовать эту функцию, а вместо этого подбирать кандидатов. Если вы воспользуетесь этой функцией, вы увидите более длительное время запуска соединения. Однако при подключении вызова вы не увидите никакого воздействия.

Наконец, нам нужно обработать входящую звуковую дорожку:

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

Как обращаться со звуковой дорожкой, зависит от требований вашего бизнеса. Например, вы можете сохранить аудио в виде файла:

Исходный код

Для получения работоспособного и протестированного исходного кода, пожалуйста, обратитесь к RingCentral Softphone SDK для GoLang. И обычно вам не нужно создавать программный телефон с нуля, вы просто используете наш SDK, чтобы быстро создать программный телефон:

Резюме

В сегодняшней статье мы рассмотрели технические детали создания программного телефона RingCentral в GoLang. Мы начали с введения в структуру сообщения SIP, за которым последовало краткое объяснение процесса регистрации SIP. И, наконец, мы поделились некоторыми техническими подробностями о Pion WebRTC. Спасибо за чтение.

Пожалуйста, дайте нам знать, что вы думаете, оставив свои вопросы и комментарии ниже. Чтобы узнать больше о других функциях, обязательно посетите наш сайт для разработчиков, а если вы когда-нибудь столкнетесь с затруднениями, обязательно посетите наш форум разработчиков.

Хотите быть в курсе и узнавать о новых API и функциях? Присоединяйтесь к нашей программе Game Changer и получайте отличные награды за развитие своих навыков и изучение RingCentral больше!