Делаем видео-конференции в браузере за 10 минут

от автора

Видеоконференции через Skype уже давно заняли свое место в ежедневных коммуникациях, пользователи оценили удобство такого формата общения и все больше компаний стараются проводить встречи именно в этом формате. Но у скайпа есть большой минус: это отдельное приложение, которое трудно интегрировать в другой сервис. А сервисов, куда можно с пользой для дела встроить видеоконференции великое множество, начиная от систем бизнес-автоматизации и заканчивая сервисами группового обучения иностранному языку. Сегодня я покажу вам, как с помощью подручных средств и voximplant за 10 минут собрать движок видеоконференций, работающий прямо из браузера на webRTC и спозволяющий подключаться к конференции с обычных телефонов.

Voximplant использует профили пользователей, которые можно создавать с помощью HTTP API. Для демонстрации видеоконференции мы сделали небольшое приложение, которое по url-приглашению запрашивает имя участника, создает профиль пользователя и возвращает параметры аутентификации https://github.com/voximplant.

В отличие от звука, voximplant передает видео между участниками, peer-to-peer, что соответствует механике работы webRTC. Чтобы организовать конференцию, участникам необходимо сделать видео подключения друг к другу — это будет хорошо работать примерно до десяти пользователей, что с запасом покрывает большинство сценариев работы. А звук будет автоматически микшироваться стандартными механизмами voximplant. Для корректного микширования звука мы создадим две внутренние конференции: #1 для видеовызовов и #2 для участников с обычных телефонов:

Красные стрелки показывают аудио и видео потоки между участниками конференции в браузере, а синие стрелки показывают аудио-потоки для участников с телефонов. Одно из преимуществ voximplant — возможность гибкой работы с разными потоками на стороне облака, что позволяет создавать самые разные решения.

Для начала зарегистрируемся в voximplant.com и создадим новое приложение с именем “videoconf”.

Затем в настройках этого приложения создадим первый, самый простой сценарий. Он будет отвечать за отправку p2p аудио/видео между web клиентами и называется “VideoConferenceP2P”:

код

VoxEngine.forwardCallToUserDirect(); 

Следующий сценарий в телефонии принято называть “gatekeeper” — он обрабатывает звонок от web-клиента и дальше перенаправляет его в конференцию с соответствующим conferenceID, полученным из webSDK, плюс обеспечивает передачу текстовых сообщений между конференцией и клиентом, для нотификации о подключении новых участников. Назовем этот сценарий “VideoConferenceGatekeeper”:

код

/** * Video Conference Gatekeeper * Handle inbound calls and route them to the conference */ var call,     conferenceId, 	conf;  /** * Inbound call handler */ VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {   	// Get conference id from headers   	conferenceId = e.headers['X-Conference-Id'];   	Logger.write('User '+e.callerid+' is joining conference '+conferenceId);  	      	call = e.call;   	/**     * Play some audio till call connected event     */ 	call.startEarlyMedia();   	call.startPlayback("http://cdn.voximplant.com/bb_remix.mp3", true);   	/**     * Add event listeners     */   	call.addEventListener(CallEvents.Connected, sdkCallConnected);   	call.addEventListener(CallEvents.Disconnected, function (e) { 		VoxEngine.terminate(); 	}); 	call.addEventListener(CallEvents.Failed, function (e) { 		VoxEngine.terminate(); 	});   	call.addEventListener(CallEvents.MessageReceived, function(e) {       	Logger.write("Message Received: "+e.text);         try {           var msg = JSON.parse(e.text);         } catch(err) {           Logger.write(err);         }              	if (msg.type == "ICE_FAILED") {       		conf.sendMessage(e.text);	         } else if (msg.type == "CALL_PARTICIPANT") {           	conf.sendMessage(e.text);         }   	});   	// Answer the call   	call.answer(); });  /** * Connected handler */ function sdkCallConnected(e) {   	// Stop playing audio   	call.stopPlayback();   	Logger.write('Joining conference');   	// Call conference with specified id   	conf = VoxEngine.callConference('conf_'+conferenceId, call.callerid(), call.displayName(), {"X-ClientType": "web"});     	Logger.write('CallerID: '+call.callerid()+' DisplayName: '+call.displayName());   	// Add event listeners   	conf.addEventListener(CallEvents.Connected, function (e) {       Logger.write("VideoConference Connected");       VoxEngine.sendMediaBetween(conf, call);     });     	conf.addEventListener(CallEvents.Disconnected, VoxEngine.terminate);   	conf.addEventListener(CallEvents.Failed, VoxEngine.terminate);     conf.addEventListener(CallEvents.MessageReceived, function(e) {       call.sendMessage(e.text);     });   } 

Следующий сценарий — для входящих звонков с обычных телефонов на телефонный номер конференции, который можно арендовать в пару кликов через интерфейс voximplant. После соединение синтезатор голоса промит звонящего ввести идентификатор конференции и осуществляет подключение. Назовем этот сценарий “VideoConferencePSTNgatekeeper”:

код

var pin = "", call;  VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) { 	call = e.call; 	e.call.addEventListener(CallEvents.Connected, handleCallConnected); 	e.call.addEventListener(CallEvents.Disconnected, handleCallDisconnected); 	e.call.answer(); });  function handleCallConnected(e) { 	e.call.say("Hello, please enter your conference pin using keypad and press pound key to join the conference.", Language.UK_ENGLISH_FEMALE); 	e.call.addEventListener(CallEvents.ToneReceived, function (e) { 		e.call.stopPlayback();		 		if (e.tone == "#") { 			// Try to call conference according the specified pin           	var conf = VoxEngine.callConference('conf_'+pin, e.call.callerid(), e.call.displayName(), {"X-ClientType": "pstn_inbound"});           	conf.addEventListener(CallEvents.Connected, handleConfConnected);           	conf.addEventListener(CallEvents.Failed, handleConfFailed); 		} else { 			pin += e.tone; 		} 	}); 	e.call.handleTones(true); }  function handleConfConnected(e) { 	VoxEngine.sendMediaBetween(e.call, call); }  function handleConfFailed(e) {   	VoxEngine.terminate(); }  function handleCallDisconnected(e) { 	VoxEngine.terminate(); } 

Последний и самый большой сценарий отвечает за создание двух конференций, подключение и отключение участников, управляет аудио потоками и удаляет ставшие не нужными профили отключившихся пользователей. Назовем этот сценарий “VideoConference”, если вы будете копировать код из примера — не забудьте подставить свои значения “account_name” и “api_key”:

код

/** * Require Conference module to get conferencing functionality */ require(Modules.Conference);  var videoconf, 	pstnconf, 	calls = [], 	pstnCalls = [], 	clientType,     /**     * HTTP API Access Info for user auto delete     */ 	apiURL = "https://api.voximplant.com/platform_api", 	account_name = "your_voximplant_account_name", 	api_key = "your_voximplant_api_key";  // Add event handler for session start event VoxEngine.addEventListener(AppEvents.Started, handleConferenceStarted);  function handleConferenceStarted(e) {     // Create 2 conferences right after session to manage audio in the right way 	videoconf = VoxEngine.createConference(); 	pstnconf = VoxEngine.createConference(); }  /** * Handle inbound call */ VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {   	// get caller's client type   	clientType = e.headers["X-ClientType"];  	// Add event handlers depending on the client type	 	if (clientType == "web") { 		e.call.addEventListener(CallEvents.Connected, handleParticipantConnected); 		e.call.addEventListener(CallEvents.Disconnected, handleParticipantDisconnected); 	} else {       	pstnCalls.push(e.call); 		e.call.addEventListener(CallEvents.Connected, handlePSTNParticipantConnected); 		e.call.addEventListener(CallEvents.Disconnected, handlePSTNParticipantDisconnected); 	} 	e.call.addEventListener(CallEvents.Failed, handleConnectionFailed); 	e.call.addEventListener(CallEvents.MessageReceived, handleMessageReceived);   	// Answer the call   	e.call.answer(); });  /** * Message handler */ function handleMessageReceived(e) { 	Logger.write("Message Recevied: " + e.text); 	try { 		var msg = JSON.parse(e.text); 	} catch (err) { 		Logger.write(err); 	} 	 	if (msg.type == "ICE_FAILED") { 		// P2P call failed because of ICE problems - sending notification to retry 		var caller = msg.caller.substr(0, msg.caller.indexOf('@')); 		caller = caller.replace("sip:", ""); 		Logger.write("Sending notification to " + caller); 		var call = getCallById(caller); 		if (call != null) call.sendMessage(JSON.stringify({ 			type: "ICE_FAILED", 			callee: msg.callee,           	displayName: msg.displayName 		})); 	} else if (msg.type == "CALL_PARTICIPANT") { 		// Conference participant decided to add PSTN participant (outbound call) 		for (var k = 0; k < calls.length; k++) calls[k].sendMessage(e.text); 		Logger.write("Calling participant with number " + msg.number); 		var call = VoxEngine.callPSTN(msg.number); 		pstnCalls.push(call); 		call.addEventListener(CallEvents.Connected, handleOutboundCallConnected); 		call.addEventListener(CallEvents.Disconnected, handleOutboundCallDisconnected); 		call.addEventListener(CallEvents.Failed, handleOutboundCallFailed); 	} }  /** * PSTN participant connected */ function handleOutboundCallConnected(e) { 	e.call.say("You have joined a conference", Language.UK_ENGLISH_FEMALE); 	e.call.addEventListener(CallEvents.PlaybackFinished, function (e) { 		for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({ 			type: "CALL_PARTICIPANT_CONNECTED", 			number: e.call.number() 		}));       	VoxEngine.sendMediaBetween(e.call, pstnconf);       	e.call.sendMediaTo(videoconf); 	}); }   /** * PSTN participant disconnected */ function handleOutboundCallDisconnected(e) { 	Logger.write("PSTN participant disconnected " + e.call.number());   	removePSTNparticipant(e.call); 	for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({ 		type: "CALL_PARTICIPANT_DISCONNECTED", 		number: e.call.number() 	})); }  /** * Call to PSTN participant failed */ function handleOutboundCallFailed(e) { 	Logger.write("Call to PSTN participant " + e.call.number() + " failed");   	removePSTNparticipant(e.call); 	for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({ 		type: "CALL_PARTICIPANT_FAILED", 		number: e.call.number() 	})); }  function removePSTNparticipant(call) {   	for (var i = 0; i < pstnCalls.length; i++) {         if (pstnCalls[i].number() == call.number()) {             Logger.write("Caller with number " + call.number() + " disconnected");             pstnCalls.splice(i, 1);         }     } }  function handleConnectionFailed(e) { 	Logger.write("Participant couldn't join the conference"); }  function participantExists(callerid) { 	for (var i = 0; i < calls.length; i++) { 		if (calls[i].callerid() == callerid) return true; 	} 	return false; }  function getCallById(callerid) { 	for (var i = 0; i < calls.length; i++) { 		if (calls[i].callerid() == callerid) return calls[i]; 	} 	return null; }  /** * Web client connected */ function handleParticipantConnected(e) { 	if (!participantExists(e.call.callerid())) calls.push(e.call); 	e.call.say("You have joined the conference.", Language.UK_ENGLISH_FEMALE); 	e.call.addEventListener(CallEvents.PlaybackFinished, function (e) {       	videoconf.sendMediaTo(e.call);       	e.call.sendMediaTo(pstnconf); 		sendCallsInfo(); 	}); }  function sendCallsInfo() {   	var info = {         peers: [],         pstnCalls: []     };     for (var k = 0; k < calls.length; k++) {         info.peers.push({             callerid: calls[k].callerid(),             displayName: calls[k].displayName()         });     }     for (k = 0; k < pstnCalls.length; k++) {         info.pstnCalls.push({             callerid: pstnCalls[k].number()         });     }     for (var k = 0; k < calls.length; k++) {         calls[k].sendMessage(JSON.stringify(info));          	     } }  /** * Inbound PSTN call connected */ function handlePSTNParticipantConnected(e) { 	e.call.say("You have joined the conference .", Language.UK_ENGLISH_FEMALE); 	e.call.addEventListener(CallEvents.PlaybackFinished, function (e) { 		VoxEngine.sendMediaBetween(e.call, pstnconf);       	e.call.sendMediaTo(videoconf);       	for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({ 			type: "CALL_PARTICIPANT_CONNECTED", 			number: e.call.callerid(),           	inbound: true 		})); 	}); }  /** * Web client disconnected */ function handleParticipantDisconnected(e) { 	Logger.write("Disconnected:"); 	for (var i = 0; i < calls.length; i++) { 		if (calls[i].callerid() == e.call.callerid()) {           	/**             * Make HTTP request to delete user via HTTP API             */ 			var url = apiURL + "/DelUser/?account_name=" + account_name + 				"&api_key=" + api_key + 				"&user_name=" + e.call.callerid(); 			Net.httpRequest(url, function (res) { 				Logger.write("HttpRequest result: " + res.text); 			}); 			Logger.write("Caller with id " + e.call.callerid() + " disconnected"); 			calls.splice(i, 1); 		} 	} 	if (calls.length == 0) VoxEngine.terminate(); }  function handlePSTNParticipantDisconnected(e) {   	removePSTNparticipant(e.call); 	for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({ 		type: "CALL_PARTICIPANT_DISCONNECTED", 		number: e.call.callerid() 	})); } 

Чтобы облако voximplant знало, когда выполнять какой сценарий, сценарии подключаются к приложению с помощью правил. Нам понадобятся следующие правила:

  • InboundFromPSTN, в Pattern указываем телефонный номер конференции, в сценарии указываем “VideoConferencePSTNgatekeeper”
  • InboundCall, в Pattern указываем строку “joinconf” (это номер, который мы будем набирать из Web SDK при подключении к конференции), в сценарии указываем “VideoConferenceGatekeeper”
  • Fwd, в Pattern указываем строку “conf_[A-Za-z0-9]+”, в сценарии указываем “VideoConference” — это правило будет срабатывать при звонке в конференцию через “callConference”.
  • P2P, в Pattern оставляем “.*”, в сценарии указываем
  • “VideoConferenceP2P”

Порядок расположения правил важен! Для перетаскивания (изменения приоритета) можно использовать drag’n’drop.

В результате настройки правил для приложения должны выглядит вот так:

Это все, что нужно настроить в облаке. Frontend часть сервиса делается с помощью нашего web sdk и довольно проста. После подключения нужно совершить звонок на “joinconf” и передать в заголовке “conferenceid”. Когда пользователь становится участником конференции, в событии MessageReceived он получат список веб-клиентов и можно инициировать исходящие peer-to-peer звонки с помощью сценария “P2P” для получения видео от тех клиентов, к которым еще нет подключений. для включения именно P2P-режима передается специальный хедер “X-DirectCall” в методе “call”. Также Frontend часть размещает на экране прямоугольники видеотрансляций и позволяет пригласить участника исходящим звонком из сценария конференции. Исходный код всех сценариев и клиентского приложения доступен на нашем GitHub-аккаунте

ссылка на оригинал статьи http://habrahabr.ru/post/261037/


Комментарии

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

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