SIGN UP

Video Conferencing

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”:

/**
 * Video Conference Gatekeeper
 * Handle inbound calls and route them to the conference
 */
let call;
let conferenceId;
let conf;

/**
 * Inbound call handler
 */
VoxEngine.addEventListener(AppEvents.CallAlerting, (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, (e) => {
    VoxEngine.terminate();
  });
  call.addEventListener(CallEvents.Failed, (e) => {
    VoxEngine.terminate();
  });
  call.addEventListener(CallEvents.MessageReceived, (e) => {
    Logger.write(`Message Received: ${e.text}`);
    try {
      const 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, (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, (e) => {
    call.sendMessage(e.text);
  });
}

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”:

let pin = '';
let call;

VoxEngine.addEventListener(AppEvents.CallAlerting, (e) => {
  const 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, (e) => {
    e.call.stopPlayback();
    if (e.tone === '#') {
      // Try to call conference according the specified pin
      const 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();
}

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”:

/**
 * Require Conference module to get conferencing functionality
 */
require(Modules.Conference);

let videoconf;
let pstnconf;
let calls = [];
let pstnCalls = [];
let clientType;
  /**
   * HTTP API Access Info for user auto delete
   */
const apiURL = 'https://api.voximplant.com/platform_api';
const account_name = 'your_voximplant_account_name';
const 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, (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 {
    const 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
    let caller = msg.caller.substr(0, msg.caller.indexOf('@'));
    caller = caller.replace('sip:', '');
    Logger.write(`Sending notification to ${caller}`);
    const 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 (let k = 0; k < calls.length; k++) {
      calls[k].sendMessage(e.text);
    }
    Logger.write(`Calling participant with number ${msg.number}`);
    const 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 (let 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 (let 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 (let k = 0; k < calls.length; k++){
    calls[k].sendMessage(JSON.stringify({
      type: 'CALL_PARTICIPANT_FAILED',
      number: e.call.number()
    }));
  }
}

function removePSTNparticipant(call) {
  for (let 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() {
  let info = {
    peers: [],
    pstnCalls: []
  };
  for (let k = 0; k < calls.length; k++) {
    info.peers.push({
      callerid: calls[k].callerid(),
      displayName: calls[k].displayName()
    });
  }
  for (let k = 0; k < pstnCalls.length; k++) {
    info.pstnCalls.push({
      callerid: pstnCalls[k].number()
    });
  }
  for (let 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 (let 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 (let i = 0; i < calls.length; i++) {
    if (calls[i].callerid() === e.call.callerid()) {
      /**
       * Make HTTP request to delete user via HTTP API
       */
      const 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 (let k = 0; k < calls.length; k++) {
    calls[k].sendMessage(JSON.stringify({
      type: 'CALL_PARTICIPANT_DISCONNECTED',
      number: e.call.callerid()
    }));
  }
}

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

Tags:webrtcweb sdkconferencingvideo conferenceskype
B6A24216-9891-45D1-9D1D-E7359CEB8282 Created with sketchtool.

Comments(0)

Add your comment

To leave a comment, first confirm that you are not a robot. It's free

Recommend

Get your free developer account or talk with our sales team to learn more about Voximplant solutions
SIGN UP
Contact sales

Please complete this field.

Please complete this field.

Please complete this field.

Choose the solution

Please complete this field.

Please complete this field.