Видеоконференции через 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/
Добавить комментарий