Робимо відео-конференції у браузері за 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-акаунті

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.