📜 ⬆️ ⬇️

We make video conferences in the browser in 10 minutes

Video conferencing via Skype has long taken its place in daily communications, users appreciate the convenience of this communication format and more and more companies are trying to hold meetings in this format. But Skype has a big disadvantage: it is a separate application that is difficult to integrate into another service. And there are a lot of services where you can embed video conferencing with benefit, starting from business automation systems and ending with services for group learning a foreign language. Today I will show you how to build a video conferencing engine that works directly from the browser on webRTC and allows you to connect to the conference from regular phones in 10 minutes with the help of available tools and voximplant.



Voximplant uses user profiles that can be created using the HTTP API. To demonstrate the video conference, we made a small application that, at the url-invitation, requests the name of the participant, creates a user profile and returns the authentication parameters https://github.com/voximplant .
')
Unlike sound, voximplant transfers video between participants, peer-to-peer, which corresponds to the mechanics of webRTC. To organize a conference, participants need to make video connections to each other - this will work well for up to ten users, which, with a margin, covers most of the work scenarios. And the sound will be automatically mixed with standard voximplant mechanisms. For correct sound mixing, we will create two internal conferences: # 1 for video calls and # 2 for participants from regular phones:



Red arrows show audio and video streams between conference participants in the browser, and blue arrows show audio streams for participants from phones. One of the advantages of voximplant is the flexibility to work with different streams on the cloud side, which allows you to create a variety of solutions.

To get started, register with voximplant.com and create a new application called “videoconf”.

Then in the settings of this application we will create the first, simplest scenario. It will be responsible for sending p2p audio / video between web clients and is called “VideoConferenceP2P”:

code
VoxEngine.forwardCallToUserDirect(); 



The next scenario in telephony is called “gatekeeper” - it processes the call from the web client and then redirects it to the conference with the corresponding conferenceID received from the webSDK, plus provides for sending text messages between the conference and the client to notify about the connection of new participants. Let's call this script “VideoConferenceGatekeeper”:

code
 /** * 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); }); } 



The next scenario is for incoming calls from ordinary phones to the conference phone number, which can be rented in a couple of clicks through the voximplant interface. After the connection, the voice synthesizer prompts the caller to enter the conference ID and makes the connection. Let's call this script “VideoConferencePSTNgatekeeper”:

code
 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(); } 



The latest and largest scenario is responsible for creating two conferences, connecting and disconnecting participants, manages audio streams and removes disconnected user profiles that are no longer needed. Let's call this script “VideoConference”, if you copy the code from the example - do not forget to substitute your values ​​“account_name” and “api_key”:

code
 /** * 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() })); } 



To make the voximplant cloud know when to run which script, the scripts connect to the application using rules . We will need the following rules:

The order of the rules is important! You can use drag'n'drop to drag (change priority) .

As a result, the settings for the application should look like this:



This is all you need to configure in the cloud. The frontend part of the service is done using our web sdk and is quite simple. After connecting, you need to make a call to “joinconf” and pass in the title “conferenceid”. When a user becomes a conference participant, in the MessageReceived event, they will receive a list of web clients and you can initiate outgoing peer-to-peer calls using the “P2P” script to receive videos from those clients that are not yet connected. To enable P2P mode, a special “X-DirectCall” header is transmitted in the “call” method. Also, the Frontend section places video transmission rectangles on the screen and allows you to invite a participant to make an outgoing call from the conference scenario. The source code for all scripts and client applications is available on our GitHub account.

Source: https://habr.com/ru/post/261037/


All Articles