VoxImplant. Blog

Video Conferencing

Video Conference

A lot of people use video conferencing functionality in Skype, but Skype is standalone application and it’s hard to integrate it with your own service. VoxImplant lets developers embed similar functionality into any web or mobile application. This tutorial will explain how to build video conferencing service with dial-in/dial-out functionality (to connect PSTN participants) and browser-based client application using WebRTC capabilities and VoxImplant.

Let’s assume our video conference room will have some URL that can be shared among the participants. We will be creating VoxImplant application users on-the-fly (and delete them when they aren’t needed anymore) using VoxImplant HTTP API for browser-based client application. It will require server-side script that manages users and VoxImplant entities. All scenarios, scripts and related files are available at our GitHub page.

We will be sending video between participants directly (using peer-to-peer), while audio will be mixed on VoxImplant side to enable PSTN participants. We will need to create 2 conferences to mix and route audiostreams correctly – 1 for web-clients and 2 for PSTN clients:

Streams Scheme

Red arrows show audio and video streams between participants using web client, blue ones show audio streams for communication with PSTN participants. VoxImplant’s flexible audio stream routing makes it easier than it seems.

First, we need to create new VoxImplant application, let’s call it “videoconf”.

The next step – create simple VoxEngine scenario that controls peer-to-peer calls between web clients. Let’s call it “VideoConferenceP2P”:

VoxEngine.forwardCallToUserDirect();

Our next scenario is usually called “gatekeeper” — it controls calls from web client and forwards them to conference with some conferenceID, sent from webSDK, it also transfers text messages between conference and web client to notify about new participants. Let’s call it “VideoConferenceGatekeeper”:

  1. /**
  2. * Video Conference Gatekeeper
  3. * Handle inbound calls and route them to the conference
  4. */
  5. var call,
  6. conferenceId,
  7. conf;
  8.  
  9. /**
  10. * Inbound call handler
  11. */
  12. VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {
  13. // Get conference id from headers
  14. conferenceId = e.headers['X-Conference-Id'];
  15. Logger.write('User '+e.callerid+' is joining conference '+conferenceId);
  16.  
  17. call = e.call;
  18. /**
  19.   * Play some audio till call connected event
  20.   */
  21. call.startEarlyMedia();
  22. call.startPlayback("http://cdn.voximplant.com/bb_remix.mp3", true);
  23. /**
  24.   * Add event listeners
  25.   */
  26. call.addEventListener(CallEvents.Connected, sdkCallConnected);
  27. call.addEventListener(CallEvents.Disconnected, function (e) {
  28. VoxEngine.terminate();
  29. });
  30. call.addEventListener(CallEvents.Failed, function (e) {
  31. VoxEngine.terminate();
  32. });
  33. call.addEventListener(CallEvents.MessageReceived, function(e) {
  34. Logger.write("Message Received: "+e.text);
  35. try {
  36. var msg = JSON.parse(e.text);
  37. } catch(err) {
  38. Logger.write(err);
  39. }
  40.  
  41. if (msg.type == "ICE_FAILED") {
  42. conf.sendMessage(e.text);
  43. } else if (msg.type == "CALL_PARTICIPANT") {
  44. conf.sendMessage(e.text);
  45. }
  46. });
  47. // Answer the call
  48. call.answer();
  49. });
  50.  
  51. /**
  52. * Connected handler
  53. */
  54. function sdkCallConnected(e) {
  55. // Stop playing audio
  56. call.stopPlayback();
  57. Logger.write('Joining conference');
  58. // Call conference with specified id
  59. conf = VoxEngine.callConference('conf_'+conferenceId, call.callerid(), call.displayName(), {"X-ClientType": "web"});
  60. Logger.write('CallerID: '+call.callerid()+' DisplayName: '+call.displayName());
  61. // Add event listeners
  62. conf.addEventListener(CallEvents.Connected, function (e) {
  63. Logger.write("VideoConference Connected");
  64. VoxEngine.sendMediaBetween(conf, call);
  65. });
  66. conf.addEventListener(CallEvents.Disconnected, VoxEngine.terminate);
  67. conf.addEventListener(CallEvents.Failed, VoxEngine.terminate);
  68. conf.addEventListener(CallEvents.MessageReceived, function(e) {
  69. call.sendMessage(e.text);
  70. });
  71. }

Next scenario controls inbound calls to the conference from some phone access number(s) connected to the app. Special IVR will ask a caller to enter conference ID, let’s call it “VideoConferencePSTNgatekeeper”:

  1. var pin = "", call;
  2.  
  3. VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {
  4. call = e.call;
  5. e.call.addEventListener(CallEvents.Connected, handleCallConnected);
  6. e.call.addEventListener(CallEvents.Disconnected, handleCallDisconnected);
  7. e.call.answer();
  8. });
  9.  
  10. function handleCallConnected(e) {
  11. e.call.say("Hello, please enter your conference pin using keypad and press pound key to join the conference.", Language.UK_ENGLISH_FEMALE);
  12. e.call.addEventListener(CallEvents.ToneReceived, function (e) {
  13. e.call.stopPlayback();
  14. if (e.tone == "#") {
  15. // Try to call conference according the specified pin
  16. var conf = VoxEngine.callConference('conf_'+pin, e.call.callerid(), e.call.displayName(), {"X-ClientType": "pstn_inbound"});
  17. conf.addEventListener(CallEvents.Connected, handleConfConnected);
  18. conf.addEventListener(CallEvents.Failed, handleConfFailed);
  19. } else {
  20. pin += e.tone;
  21. }
  22. });
  23. e.call.handleTones(true);
  24. }
  25.  
  26. function handleConfConnected(e) {
  27. VoxEngine.sendMediaBetween(e.call, call);
  28. }
  29.  
  30. function handleConfFailed(e) {
  31. VoxEngine.terminate();
  32. }
  33.  
  34. function handleCallDisconnected(e) {
  35. VoxEngine.terminate();
  36. }

And the last one scenario controls two conferences, participants connections and disconnects, audio streams and deletes app users. Let’s call it “VideoConference”, don’t forget to insert your “account_name” and “api_key”:

  1. /**
  2. * Require Conference module to get conferencing functionality
  3. */
  4. require(Modules.Conference);
  5.  
  6. var videoconf,
  7. pstnconf,
  8. calls = [],
  9. pstnCalls = [],
  10. clientType,
  11. /**
  12.   * HTTP API Access Info for user auto delete
  13.   */
  14. apiURL = "https://api.voximplant.com/platform_api",
  15. account_name = "your_voximplant_account_name",
  16. api_key = "your_voximplant_api_key";
  17.  
  18. // Add event handler for session start event
  19. VoxEngine.addEventListener(AppEvents.Started, handleConferenceStarted);
  20.  
  21. function handleConferenceStarted(e) {
  22. // Create 2 conferences right after session to manage audio in the right way
  23. videoconf = VoxEngine.createConference();
  24. pstnconf = VoxEngine.createConference();
  25. }
  26.  
  27. /**
  28. * Handle inbound call
  29. */
  30. VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {
  31. // get caller's client type
  32. clientType = e.headers["X-ClientType"];
  33. // Add event handlers depending on the client type
  34. if (clientType == "web") {
  35. e.call.addEventListener(CallEvents.Connected, handleParticipantConnected);
  36. e.call.addEventListener(CallEvents.Disconnected, handleParticipantDisconnected);
  37. } else {
  38. pstnCalls.push(e.call);
  39. e.call.addEventListener(CallEvents.Connected, handlePSTNParticipantConnected);
  40. e.call.addEventListener(CallEvents.Disconnected, handlePSTNParticipantDisconnected);
  41. }
  42. e.call.addEventListener(CallEvents.Failed, handleConnectionFailed);
  43. e.call.addEventListener(CallEvents.MessageReceived, handleMessageReceived);
  44. // Answer the call
  45. e.call.answer();
  46. });
  47.  
  48. /**
  49. * Message handler
  50. */
  51. function handleMessageReceived(e) {
  52. Logger.write("Message Recevied: " + e.text);
  53. try {
  54. var msg = JSON.parse(e.text);
  55. } catch (err) {
  56. Logger.write(err);
  57. }
  58.  
  59. if (msg.type == "ICE_FAILED") {
  60. // P2P call failed because of ICE problems - sending notification to retry
  61. var caller = msg.caller.substr(0, msg.caller.indexOf('@'));
  62. caller = caller.replace("sip:", "");
  63. Logger.write("Sending notification to " + caller);
  64. var call = getCallById(caller);
  65. if (call != null) call.sendMessage(JSON.stringify({
  66. type: "ICE_FAILED",
  67. callee: msg.callee,
  68. displayName: msg.displayName
  69. }));
  70. } else if (msg.type == "CALL_PARTICIPANT") {
  71. // Conference participant decided to add PSTN participant (outbound call)
  72. for (var k = 0; k < calls.length; k++) calls[k].sendMessage(e.text);
  73. Logger.write("Calling participant with number " + msg.number);
  74. var call = VoxEngine.callPSTN(msg.number);
  75. pstnCalls.push(call);
  76. call.addEventListener(CallEvents.Connected, handleOutboundCallConnected);
  77. call.addEventListener(CallEvents.Disconnected, handleOutboundCallDisconnected);
  78. call.addEventListener(CallEvents.Failed, handleOutboundCallFailed);
  79. }
  80. }
  81.  
  82. /**
  83. * PSTN participant connected
  84. */
  85. function handleOutboundCallConnected(e) {
  86. e.call.say("You have joined a conference", Language.UK_ENGLISH_FEMALE);
  87. e.call.addEventListener(CallEvents.PlaybackFinished, function (e) {
  88. for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({
  89. type: "CALL_PARTICIPANT_CONNECTED",
  90. number: e.call.number()
  91. }));
  92. VoxEngine.sendMediaBetween(e.call, pstnconf);
  93. e.call.sendMediaTo(videoconf);
  94. });
  95. }
  96.  
  97. /**
  98. * PSTN participant disconnected
  99. */
  100. function handleOutboundCallDisconnected(e) {
  101. Logger.write("PSTN participant disconnected " + e.call.number());
  102. removePSTNparticipant(e.call);
  103. for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({
  104. type: "CALL_PARTICIPANT_DISCONNECTED",
  105. number: e.call.number()
  106. }));
  107. }
  108.  
  109. /**
  110. * Call to PSTN participant failed
  111. */
  112. function handleOutboundCallFailed(e) {
  113. Logger.write("Call to PSTN participant " + e.call.number() + " failed");
  114. removePSTNparticipant(e.call);
  115. for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({
  116. type: "CALL_PARTICIPANT_FAILED",
  117. number: e.call.number()
  118. }));
  119. }
  120.  
  121. function removePSTNparticipant(call) {
  122. for (var i = 0; i < pstnCalls.length; i++) {
  123. if (pstnCalls[i].number() == call.number()) {
  124. Logger.write("Caller with number " + call.number() + " disconnected");
  125. pstnCalls.splice(i, 1);
  126. }
  127. }
  128. }
  129.  
  130. function handleConnectionFailed(e) {
  131. Logger.write("Participant couldn't join the conference");
  132. }
  133.  
  134. function participantExists(callerid) {
  135. for (var i = 0; i < calls.length; i++) {
  136. if (calls[i].callerid() == callerid) return true;
  137. }
  138. return false;
  139. }
  140.  
  141. function getCallById(callerid) {
  142. for (var i = 0; i < calls.length; i++) {
  143. if (calls[i].callerid() == callerid) return calls[i];
  144. }
  145. return null;
  146. }
  147.  
  148. /**
  149. * Web client connected
  150. */
  151. function handleParticipantConnected(e) {
  152. if (!participantExists(e.call.callerid())) calls.push(e.call);
  153. e.call.say("You have joined the conference.", Language.UK_ENGLISH_FEMALE);
  154. e.call.addEventListener(CallEvents.PlaybackFinished, function (e) {
  155. videoconf.sendMediaTo(e.call);
  156. e.call.sendMediaTo(pstnconf);
  157. sendCallsInfo();
  158. });
  159. }
  160.  
  161. function sendCallsInfo() {
  162. var info = {
  163. peers: [],
  164. pstnCalls: []
  165. };
  166. for (var k = 0; k < calls.length; k++) {
  167. info.peers.push({
  168. callerid: calls[k].callerid(),
  169. displayName: calls[k].displayName()
  170. });
  171. }
  172. for (k = 0; k < pstnCalls.length; k++) {
  173. info.pstnCalls.push({
  174. callerid: pstnCalls[k].number()
  175. });
  176. }
  177. for (var k = 0; k < calls.length; k++) {
  178. calls[k].sendMessage(JSON.stringify(info));
  179. }
  180. }
  181.  
  182. /**
  183. * Inbound PSTN call connected
  184. */
  185. function handlePSTNParticipantConnected(e) {
  186. e.call.say("You have joined the conference .", Language.UK_ENGLISH_FEMALE);
  187. e.call.addEventListener(CallEvents.PlaybackFinished, function (e) {
  188. VoxEngine.sendMediaBetween(e.call, pstnconf);
  189. e.call.sendMediaTo(videoconf);
  190. for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({
  191. type: "CALL_PARTICIPANT_CONNECTED",
  192. number: e.call.callerid(),
  193. inbound: true
  194. }));
  195. });
  196. }
  197.  
  198. /**
  199. * Web client disconnected
  200. */
  201. function handleParticipantDisconnected(e) {
  202. Logger.write("Disconnected:");
  203. for (var i = 0; i < calls.length; i++) {
  204. if (calls[i].callerid() == e.call.callerid()) {
  205. /**
  206.   * Make HTTP request to delete user via HTTP API
  207.   */
  208. var url = apiURL + "/DelUser/?account_name=" + account_name +
  209. "&api_key=" + api_key +
  210. "&user_name=" + e.call.callerid();
  211. Net.httpRequest(url, function (res) {
  212. Logger.write("HttpRequest result: " + res.text);
  213. });
  214. Logger.write("Caller with id " + e.call.callerid() + " disconnected");
  215. calls.splice(i, 1);
  216. }
  217. }
  218. if (calls.length == 0) VoxEngine.terminate();
  219. }
  220.  
  221. function handlePSTNParticipantDisconnected(e) {
  222. removePSTNparticipant(e.call);
  223. for (var k = 0; k < calls.length; k++) calls[k].sendMessage(JSON.stringify({
  224. type: "CALL_PARTICIPANT_DISCONNECTED",
  225. number: e.call.callerid()
  226. }));
  227. }

To let VoxImplant use the right VoxEngine scenario for a call we need to define the application rules:

  • InboundFromPSTN, put phone number in the Pattern field and assign “VideoConferencePSTNgatekeeper” scenario
  • InboundCall, put “joinconf” in the Pattern field (we will be dialing this number from Web SDK) and assign “VideoConferenceGatekeeper” scenario
  • Fwd, put “conf_[A-Za-z0-9]+” in the Pattern field, and assign the “VideoConference” scenario — this rule will be used while call to the conference using “callConference” method.
  • P2P, put “.*” in the Pattern field, and assign the “VideoConferenceP2P” scenario.

Rules order is important! Use drag’n’drop to change the order if required.
In the end we will have Rules as following:

Application Rules

That’s all setup for VoxImplant account. Web client is built using web sdk. After connection is established we need to make a call to “joinconf” and send “conferenceid” in call headers. When user joins the conference web client receives a list of participants in MessageReceived event and then initiates outbound peer-to-peer calls using “P2P” scenario. For peer-to-peer mode “X-DirectCall” should be set while using “call” method on WebSDK side. Web client also renders participant’s videos and lets connect PSTN participants to the conference by making outbound calls. You can get the client and scenarios code at our GitHub page https://github.com/voximplant/videoconference

Comments