- PVSM.RU - https://www.pvsm.ru -

Друзья, сегодня я хочу поговорить с вами про звонки. Для кого-то это совсем новая тема. Для других — чистой воды фан на уровне «а не зафигачить ли мне свой скайп?». Для третьих — внезапно возникшая жизненная необходимость. Последний вариант — наш вариант.
В этой статье я покажу вам маленький, но очень жизнеспособный пример реализации, который позволит вам буквально на коленке из нескольких десятков строк java-скрипта сделать собственную WEB-звонилку и позвонить другу прямо из браузера.

На дворе 2019 год, и к нашей радости уже есть готовый инструмент для реализации Real-Time Communication (RTC) для веба, а именно WebRTC [1]. Еще несколько лет назад он был в активной разработке. API до сих пор дорабатывается, но технология де-факто стала стандартом и поддерживается в большинстве популярных браузеров. В этой статье на самой технологии мы останавливаться не будем, можно подробнее почитать на сайте разработчиков или поискать статьи на хабре. Например, вот тут [2].
Но прежде, чем мы начнём, хочу прояснить пару моментов.
Эти штуки оставляем за рамками статьи. Будем считать, что вам повезло так же, как нам, и в распоряжении уже имеется настроенная VoIP телефония.
Ну что, самая длинная часть статьи позади, давайте кодить!

Для начала нам понадобится страничка, с которой мы будем звонить, поля для ввода логина, пароля, номера телефона и пара кнопок. В простейшем варианте выглядеть будет примерно так:
<div class="container">
<div class="input-group mb-6">
<div class="input-group-prepend">
<span class="input-group-text">Login</span>
</div>
<input id="loginText" type="text" class="form-control">
<div class="input-group-prepend">
<span class="input-group-text">Password</span>
</div>
<input id="passwordText" type="password" class="form-control">
<button id="loginButton" type="button" class="btn btn-primary" onclick="login()">Login</button>
<button id="logOutButton" type="button" class="btn btn-primary d-none" onclick="logout()">LogOut</button>
</div>
<div class="input-group mb-6 d-none" id="callPanel">
<input id="callNumberText" type="text" class="form-control" placeholder="Call number">
<button id="callNumberButton" type="button" class="btn btn-success" onclick="call()">Call</button>
<button id="hangUpButton" type="button" class="btn btn-danger d-none" onclick="hangUp()">Hang Up</button>
</div>
<audio id="localAudio" autoPlay muted></audio>
<audio id="remoteAudio" autoPlay></audio>
<audio id="sounds" autoPlay></audio>
</div>
Аудиоэлементы будут «отправлять» и «принимать» звук, ну и для красоты через sounds проигрывать звуки дозвона.
UI готов, к UX не придерешься, давайте заставим его работать.

Воспользуемся библиотекой, в которой уже реализовано всё, что требуется, — JSSIP [4]. Можно посмотреть документацию: там всё довольно подробно описано и есть даже готовый пример реализации. То есть нам практически ничего не нужно делать — лишь всё максимально упростить и разобраться, что к чему.
После ввода логина/пароля (должны быть зарегистрированы на вашем сервере телефонии) нужно залогиниться на сервере. Делаем так:
socket = new JsSIP.WebSocketInterface("wss://webrtcserver:port/ws");
_ua = new JsSIP.UA(
{
uri: "sip:" + this.loginText.val() + "@webrtcserver",
password: this.passwordText.val(),
display_name: this.loginText.val(),
sockets: [socket]
});
Попутно можно подписаться на события connecting и connected и сделать там что-то полезное. Но перейдём сразу к событию регистрации:
his._ua.on('registered', () => {
console.log("UA registered");
this.loginButton.addClass('d-none');
this.logOutButton.removeClass('d-none');
this.loginText.prop('disabled', true);
this.passwordText.prop('disabled', true);
$("#callPanel").removeClass('d-none');
});
Тут нам просто нужно поменять статусы кнопочек: показать нужное, скрыть ненужное. И если вдруг что-то пошло не так с логином, плюнем ошибку в лог:
this._ua.on('registrationFailed', (data) => {
console.error("UA registrationFailed", data.cause);
});
Этого достаточно для логина. Осталось завести шарманку с помощью
this._ua.start();
Если сервер указан правильно и ваш логин/пароль им приняты — появится поле для ввода телефона и кнопка Call.
Для разлогина нужно вызвать this._ua.stop(), всё просто.
Теперь — самое главное: нужно позвонить на введённый номер.
this.session = this._ua.call(number, {
pcConfig:
{
hackStripTcp: true, // Важно для хрома, чтоб он не тупил при звонке
rtcpMuxPolicy: 'negotiate', // Важно для хрома, чтоб работал multiplexing. Эту штуку обязательно нужно включить на астере.
iceServers: []
},
mediaConstraints:
{
audio: true, // Поддерживаем только аудио
video: false
},
rtcOfferConstraints:
{
offerToReceiveAudio: 1, // Принимаем только аудио
offerToReceiveVideo: 0
}
});
Обратите внимание: мы явно включаем мультиплексирование, эту настройку нужно включить и на вашем сервере. В случае астериска это rtcp_mux=yes в настройках sip.conf.
Дальнейшее взаимодействие строится на колбэках, в которых мы должны обеспечить направление аудио-видео потока в соответствующий элемент страницы и послать нужные сообщения в правильном порядке на сервер.
// В процессе дозвона
this.session.on('progress', () => {
console.log("UA session progress");
playSound("ringback.ogg", true);
});
// Астер нас соединил с абонентом
this.session.on('connecting', () => {
console.log("UA session connecting");
playSound("ringback.ogg", true);
// Тут мы подключаемся к микрофону и цепляем к нему поток, который пойдёт в астер
let peerconnection = this.session.connection;
let localStream = peerconnection.getLocalStreams()[0];
// Handle local stream
if (localStream) {
// Clone local stream
this._localClonedStream = localStream.clone();
console.log('UA set local stream');
let localAudioControl = document.getElementById("localAudio");
localAudioControl.srcObject = this._localClonedStream;
}
// Как только астер отдаст нам поток абонента, мы его засунем к себе в наушники
peerconnection.addEventListener('addstream', (event) => {
console.log("UA session addstream");
let remoteAudioControl = document.getElementById("remoteAudio");
remoteAudioControl.srcObject = event.stream;
});
});
// Дозвон завершился неудачно, например, абонент сбросил звонок
this.session.on('failed', (data) => {
console.log("UA session failed");
stopSound("ringback.ogg");
playSound("rejected.mp3", false);
this.callButton.removeClass('d-none');
this.hangUpButton.addClass('d-none');
});
// Поговорили, разбежались
this.session.on('ended', () => {
console.log("UA session ended");
playSound("rejected.mp3", false);
JsSIP.Utils.closeMediaStream(this._localClonedStream);
this.callButton.removeClass('d-none');
this.hangUpButton.addClass('d-none');
});
// Звонок принят, можно начинать говорить
this.session.on('accepted', () => {
console.log("UA session accepted");
stopSound("ringback.ogg");
playSound("answered.mp3", false);
});
В общем, всё довольно логично. Пока дозваниваемся ['progress'] — играем звуки дозвона. Как только дозвонились ['accepted'] — играем звук answered. Как только абонент снимет трубку, мы получим его звуковой поток и засунем его в элемент remoteAudio ['connecting' и 'addstream'].
В конце звонка делаем closeMediaStream. Можно расслабиться.
При тестировании были обнаружены две вещи.
В целом пример готов, можно брать и звонить. Не нужно ставить никаких софтфонов или разрабатывать свои. Не нужно париться с деплоем этого софта на клиентские тачки. Просто открываем браузер и звоним.
Еще библиотека позволяет набирать дополнительные номера в тоновом режиме. То есть вы вполне можете позвонить, например, в колл-центр банка и добраться до нужного пункта по голосовому меню. Для этого достаточно выполнить такую команду:
this._call.sendDTMF(‘доп. номер’)

Было несколько моментов, которые реально заставили понервничать.
Я оставил эту часть за рамками статьи, но помимо исходящих вызовов нам требовалось и входящие принимать. И какое-то время пришлось приседать со входящим звонком, который приходил и тут же обрывался. Всё решилось уже упомянутой выше настройкой rtcpMuxPolicy и включением мультиплексирования на астериске, но тупили мы довольно долго.
И еще есть проблемы с дозвоном самому себе — когда вызов и прием звонка делаются на одной машине. Уже точно не помню, но соединение успешно устанавливалось, ошибок не было и звука тоже:) Время поджимало, так что мы на этот спецэффект забили. Но имейте в виду, что тестить входящие звонки лучше на отдельной тачке.
Напоследок хочется отметить, что работу связки JSSIP + Asterisk мы протестировали на своём колл-центре, всё работает отлично, по крайней мере в хроме, что нас полностью устраивает. Главное — разрешить браузеру доступ к медиа-устройствам и зарегистрировать пользователей на сервере звонилки.
Готовый пример можно посмотреть на гитхабе [5].
Про webrct [1]
Про SIP: тыц [6], тыц [3]
Про Asterisk [7]
Библиотека JSSIP [4]
Автор: Лобода Владимир
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/314836
Ссылки в тексте:
[1] WebRTC: https://webrtc.org/
[2] тут: https://habr.com/ru/company/Voximplant/blog/344794/
[3] тут: https://habr.com/ru/post/188352/
[4] JSSIP: https://jssip.net/
[5] гитхабе: https://github.com/viloboda/WebRTCCall
[6] тыц: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB_%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F_%D1%81%D0%B5%D0%B0%D0%BD%D1%81%D0%B0
[7] Про Asterisk: https://habr.com/ru/post/54751/
[8] Источник: https://habr.com/ru/post/448266/?utm_source=habrahabr&utm_medium=rss&utm_campaign=448266
Нажмите здесь для печати.