Эмитация рации с помощью Freeswitch и немного про voip-конференции

от автора

Вот даже такое бывает, что надо заставить такого монстра как 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *