Pull to refresh
69.18
Voximplant
Облачная платформа голосовой и видеотелефонии

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

Reading time9 min
Views29K
Обратите внимание, что данная статья устарела. Актуальная информация на тему создания видеоконференций доступна по ссылке.
Видеоконференции через 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 to 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, в маске указываем телефонный номер конференции, в сценарии указываем “VideoConferencePSTNgatekeeper”
  • InboundCall, в маске указываем строку “joinconf” (это номер, который мы будем набирать из Web SDK при подключении к конференции), в сценарии указываем “VideoConferenceGatekeeper”
  • Fwd, в маске указываем строку “conf_[A-Za-z0-9]+”, в сценарии указываем “VideoConference” — это правило будет срабатывать при звонке в конференцию через “callConference”.
  • P2P, маску оставляем “.*”, в сценарии указываем “VideoConferenceP2P”

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

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


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

Articles

Information

Website
www.voximplant.com
Registered
Founded
Employees
101–200 employees
Location
Россия