Вот даже такое бывает, что надо заставить такого монстра как Freeswitch работать по принципу обычной рации.
Один говорит, все слушают.
А поможет нам в этом NodeJs и npm модуль modesl для взаимодействия с Freeswitch.
В какой-то момент у нас в организации в большом проекте беспроводной связи заказчику потребовалась эмуляция поведения рации поверх voip-телефонии. За основу системы был взят Freeswitch. В общем система связи организована на основе mesh-сети и соответственно централизованного сервера нет, на каждом узле есть свой экземпляр Freeswitch-а, отвечающий за различные голосовые сценарии.
В рации как известно все очень просто: один говорит и все слушают, что и требуется реализовать. Так же необходимо сделать конференц-связь для нескольких независимых групп, причем любой абонент может одновременно быть в нескольких из них. И естественно в сети должны быть адресные вызовы.
В распоряжении есть:
Логика конечно странная и запутанная, но раз заказчик просит, надо делать. Примерная структура выглядит так:
Sip client — это может быть и mod_portaudio и linphone или к примеру baresip.
Local Conference — это внутренняя конференция для каждого узла, задача которой поддержать хитрую логику работы с эмуляцией рации и переключениями между глобальными конференциями.
Global Conference — это общая конференция, их может быть несколько, что позволит объединять разных пользователей в разные группы.
Подключить modesl можно следующим образом:
Возможности Connection в modesl:
Конференцию можно создать просто прописав все в xml конфигах, либо можно сделать это передавая из xml-конфигов все управление на определенный адрес и порт. Мы выберем 2й вариант.
В конфигах Freeswitch в файле dialplan/public.xml надо прописать что-то наподобие:
Это значит что если позвонят по номеру 5555 то перенаправить управление по адресу 127.0.0.1:8087.
Так же следует написать скрипт для создания конференции:
Первоначально при запуске узла происходит звонок в локальную конференцию. Для входа в какую-то из глобальных конференций узел осуществляет звонок из Local Conference в Global Conference. В Freeswitch это можно сделать так:
fs_cli
nodejs
Это очень интересная и удобная функция позволяющая объединять разные конференции в одну.
Дальше мы делаем deaf для этой Global Conference внутри Local Conference ( это позволить нам сделать так, чтобы разные глобальные конференции, в которые входит узел, не слышали друг друга).
Сделать это можно так:
fs_cli
nodejs
Откуда взялся memberId, его можно получить подписавшись на событие conference_add_member.
На каждом узле перед началом разговора Local Conference имеет следующий вид:
Узел слышит всех, и глобальные конференции не слышат друг друга.
Когда узлу нужно что-то сказать в одну из глобальных конференций, надо сначала сделать всех участником кроме себя mute. Это делается просто пробежавшись по списку участников вот такой функцией
Затем надо сделать undeaf для той глобальной конференции куда будет говорить узел
В результате схематически получим следующее:
Так как всем глобальным конференциям сделан mute, активная глобальная конференция (та которой сделан undeaf) не услышит другие. Когда разговор завершится мы вернем все в прежнее состояние.
Теперь когда весь базовый функционал готов, достаточно написать высокоуровневую логику работы. Но это уже тема для отдельной статьи :).
Схема получилась достаточно большая и запутанная.
Решить эту задачу не используя локальные конференции для каждого узла можно, но придется делать адресные вызовы во все глобальные конференции в которых мы хотим участвовать. В этом случае мы столкнемся с другой проблемой: sip-клиент не должен ставить звонки на удержание и будет необходимо использовать переключение между активными звонками. У такой схемы тоже будут свои плюсы и минусы.
Важный вывод в том, что 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 — это уникальный инструмент позволяющий реализовать самые разнообразные схемы работы с голосом.