Вот даже такое бывает, что надо заставить такого монстра как 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 — это уникальный инструмент позволяющий реализовать самые разнообразные схемы работы с голосом.

Комментарии (0)