Вот даже такое бывает, что надо заставить такого монстра как Freeswitch работать по принципу обычной рации.
Один говорит, все слушают.
А поможет нам в этом NodeJs и npm модуль modesl для взаимодействия с Freeswitch.
В какой-то момент у нас в организации в большом проекте беспроводной связи заказчику потребовалась эмуляция поведения рации поверх voip-телефонии. За основу системы был взят Freeswitch. В общем система связи организована на основе mesh-сети и соответственно централизованного сервера нет, на каждом узле есть свой экземпляр Freeswitch-а, отвечающий за различные голосовые сценарии.
Задача
В рации как известно все очень просто: один говорит и все слушают, что и требуется реализовать. Так же необходимо сделать конференц-связь для нескольких независимых групп, причем любой абонент может одновременно быть в нескольких из них. И естественно в сети должны быть адресные вызовы.
В распоряжении есть:
- Freeswitch — user agent, который с правильным подбором модулей даже кофе сварить сможет.
- modesl — npm модуль для взаимодействия с Freeswitch используя Event Socket Library.
- mod_conference — модулья FS для создания и работы с голосовыми конференциями.
- mod_sofia — модулья FS для работы с SIP.
Общая схема
Логика конечно странная и запутанная, но раз заказчик просит, надо делать. Примерная структура выглядит так:
Sip client — это может быть и mod_portaudio и linphone или к примеру baresip.
Local Conference — это внутренняя конференция для каждого узла, задача которой поддержать хитрую логику работы с эмуляцией рации и переключениями между глобальными конференциями.
Global Conference — это общая конференция, их может быть несколько, что позволит объединять разных пользователей в разные группы.
Подготовка
Как подключить modesl
Подключить modesl можно следующим образом:
var esl = require( "modesl"); var localServer = "localhost"; var localServerPort = 8021; var localServerUser = "ClueCon"; var connectionCallback = function() { //соединение создано, можно работать connection.on( "esl::end", function( event) { //обрабатываем завершение соединения, например можно переподключиться }); } //создаем соединение var connection = new esl.Connection( localServer, localServerPort, localServerUser, connectionCallback); connection.once( "error", function() { //обрабатываем ошибки соединения });
Возможности Connection в modesl:
- Подписка на события от Freeswitch. Вот например подписка на все события:
connection.on( "esl::event::**", function( event) {});
- Синхронный вызов команды Freeswitch
Connection.prototype.api = function(command, args, cb)
- Асинхронный вызов команды Freeswitch
Connection.prototype.bgapi = function(command, args, jobid, cb)
Как работать с конференциями в Freeswitch
Конференцию можно создать просто прописав все в xml конфигах, либо можно сделать это передавая из xml-конфигов все управление на определенный адрес и порт. Мы выберем 2й вариант.
В конфигах Freeswitch в файле dialplan/public.xml надо прописать что-то наподобие:
<extension name="conference_server"> <condition field="destination_number" expression="^(5555)$"> <action application="socket" data="127.0.0.1:8087 async full"/> </condition> </extension>
Это значит что если позвонят по номеру 5555 то перенаправить управление по адресу 127.0.0.1:8087.
Так же следует написать скрипт для создания конференции:
var esl = require('modesl'); var esl_server = new esl.Server({port: 8087, myevents:true}, function(){ console.log("ConferenceServer server is up"); }); esl_server.on( 'connection::ready', function( conn, id) { console.log( 'ConferenceServer new call', id); conn.execute( 'conference', 'ConfName@default', function( err, result){ console.log( arguments); }); conn.on('esl::end', function( evt, body) { console.log( "ConferenceServer call ended ", id); }); });
Реализация
Вход в конференцию
Первоначально при запуске узла происходит звонок в локальную конференцию. Для входа в какую-то из глобальных конференций узел осуществляет звонок из Local Conference в Global Conference. В Freeswitch это можно сделать так:
fs_cli
conference [conference name] dial sofia/internal/[sip address]
nodejs
self.dial = function( sipAddress, callback){ self.connection.bgapi( "conference", conferenceName + " dial sofia/internal/" + sipAddress, function( result){ var resultId = result.getBody().indexOf( "SUCCESS") if( resultId == -1){ var body = result.getBody(); var startIndex = body.indexOf( '['); var result = body.substring( startIndex + 1, body.length - 2); callbackHelper.call( callback, "Conference call error: " + result); } else callbackHelper.call( callback, null); }); };
Это очень интересная и удобная функция позволяющая объединять разные конференции в одну.
Дальше мы делаем deaf для этой Global Conference внутри Local Conference ( это позволить нам сделать так, чтобы разные глобальные конференции, в которые входит узел, не слышали друг друга).
Сделать это можно так:
fs_cli
conference [conference name] deaf [memberId]
nodejs
self.deaf = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " deaf " + memberId, function( result){ callbackHelper.call( callback, null); }); };
Откуда взялся memberId, его можно получить подписавшись на событие conference_add_member.
Разговор
На каждом узле перед началом разговора Local Conference имеет следующий вид:
Узел слышит всех, и глобальные конференции не слышат друг друга.
Когда узлу нужно что-то сказать в одну из глобальных конференций, надо сначала сделать всех участником кроме себя mute. Это делается просто пробежавшись по списку участников вот такой функцией
self.mute = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " mute " + memberId, function( result){ callbackHelper.call( callback, null); }); };
.
Затем надо сделать undeaf для той глобальной конференции куда будет говорить узел
self.undeaf = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " undeaf " + memberId, function( result){ callbackHelper.call( callback, null); }); };
.
В результате схематически получим следующее:
Так как всем глобальным конференциям сделан mute, активная глобальная конференция (та которой сделан undeaf) не услышит другие. Когда разговор завершится мы вернем все в прежнее состояние.
function FsConferenceAPI( connection){ var self = this; self.connection = connection; self.unmuteAll = function( conferenceName){ self.connection.bgapi( "conference", conferenceName + " unmute all", function( result){}); }; self.dial = function( sipAddress, callback){ self.connection.bgapi( "conference", conferenceName + " dial sofia/internal/" + sipAddress, function( result){ var resultId = result.getBody().indexOf( "SUCCESS") if( resultId == -1){ var body = result.getBody(); var startIndex = body.indexOf( '['); var result = body.substring( startIndex + 1, body.length - 2); callbackHelper.call( callback, "Conference call error: " + result); } else callbackHelper.call( callback, null); }); }; self.kick = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " kick " + memberId, function( result){ var body = result.getBody(); if( body.indexOf( "OK kicked " + memberId) != -1) callbackHelper.call( callback, null); else callbackHelper.call( callback, body); }); }; self.kickAll = function( conferenceName, callback){ self.connection.bgapi( "conference", conferenceName + " kick all", function( result){ var body = result.getBody(); if( body.indexOf( "OK kicked") != -1) callbackHelper.call( callback, null); else callbackHelper.call( callback, body); }); }; self.deaf = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " deaf " + memberId, function( result){ callbackHelper.call( callback, null); }); }; self.undeaf = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " undeaf " + memberId, function( result){ callbackHelper.call( callback, null); }); }; self.mute = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " mute " + memberId, function( result){ callbackHelper.call( callback, null); }); }; self.unmute = function( conferenceName, memberId, callback){ self.connection.bgapi( "conference", conferenceName + " unmute " + memberId, function( result){ callbackHelper.call( callback, null); }); }; }
Теперь когда весь базовый функционал готов, достаточно написать высокоуровневую логику работы. Но это уже тема для отдельной статьи :).
Выводы
Схема получилась достаточно большая и запутанная.
Решить эту задачу не используя локальные конференции для каждого узла можно, но придется делать адресные вызовы во все глобальные конференции в которых мы хотим участвовать. В этом случае мы столкнемся с другой проблемой: sip-клиент не должен ставить звонки на удержание и будет необходимо использовать переключение между активными звонками. У такой схемы тоже будут свои плюсы и минусы.
Важный вывод в том, что Freeswitch — это уникальный инструмент позволяющий реализовать самые разнообразные схемы работы с голосом.
ссылка на оригинал статьи http://habrahabr.ru/post/268773/
Добавить комментарий