The objective of this tutorial is to achieve a call with several partners and get used to basic events of the SippoSDK session.

We are going to use the previously created app and upgrade it to handle calls.

To perform this tutorial we will take as an starting point the result of the tutorial 2 - Register.

So on the following link you will find the following structure:

project/
├──js/
|   ├── libs/
|   │   └── sippojs-janus-24.2.0.js
|   ├── services/
|   │   └── auth.js
|   ├── utils/
|   │   └── logger.js
|   └── app.js
├── index.html
└── style.css

We are going to create a new service called CallService for this tutorial.

We will work with the following files:

The first step is to analyze all the Sippo classes, methods and events that we will use for creating our application. This way we will understand better how Sippo works and we will discover new features that can be implemented.

Take your time to check all of them and discover all their capabilities.

3.1. Classes

The main classes that we will use for implementing our new features are the following:

3.2. Methods

These are the main methods that we will use:

3.3. Events

And finally we will listen the following events from the ConferenceManager and Conference classes:

The first step is to add a new panel to the interface:

<div class="panel">
  <h3>Call panel</h3>
  <button onclick="createAndJoin()">Create & join room</button>
  <button onclick="leave()">Leave room</button>
  <button onclick="answer()">Answer</button> <br>
  <label>Callee:</label>
  <input id="call-callee" type="text">
  <button onclick="invite()">Invite</button> <br>
  <div class="call-video-panel">
    <h4>Local video</h4>
    <video id="call-local-video" autoplay playsinline></video>
  </div>
  <div class="call-video-panel">
    <h4>Remote videos</h4>
    <div id="call-remote-videos"></div>
  </div>
  <audio id="call-audio" autoplay></audio>
</div>

You will notice that the video element has two attributes: autoplay and playsinline. Both attributes are necessary to play the video automatically. If we don't include them, we will see the video frozen. In special, the second attribute is to be able to autoplay the videos in Safari.

Finally, do not forget to add the service that we will create to our index.html:

    <!-- App services -->
    <script src="js/services/auth.js"></script>
    <script src="js/services/call.js"></script>

Now is the turn of defining the public functions. The goal of these functions is to be used as an interconnection layer between the user interface and the CallService. These function will be called when the user press a button.

You will see that we use some methods from the CallService. Don't worry, because we will see their implementation later.

The first step is to create an instance of the CallService at the beginning of the file:

// Define the services
let logger = new LoggerService();
let authSvc = new AuthService();
let callSvc = new CallService();

In the next subsections we will define all the new functions that are defined in this file.

5.1. Create and join

The first step is to pass to the CallService all the HTML elements that will be used to attach the media streams.

Then we call the createAndJoin() method that is defined inside the CallService.

function createAndJoin() {
  logger.log('Trying to create and login into conference');
  let localVideoEl = document.getElementById('call-local-video');
  let remoteVideosEl = document.getElementById('call-remote-videos');
  let audioEl = document.getElementById('call-audio');
  callSvc.setLocalVideoEl(localVideoEl);
  callSvc.setRemoteVideosEl(remoteVideosEl);
  callSvc.setAudioEl(audioEl);
  callSvc.createAndJoin().then( () => {
    logger.succ('Conference created and join to it.');
  }).catch( () => {
    logger.error('Cannot create and join the conference. Are you registered?');
  });
}

Take into account that after running this method, we will enter in an empty room. We will see how to invite a user in the next steps.

5.2. Leave

We only call the method with the same name from the CallService:

function leave() {
  callSvc.leave().then( () => {
    logger.succ('Conference leaving successful.');
  }).catch( () => {
    logger.error('Cannot leave. Are you in a conference?');
  });
}

5.3. Answer

It works in similar way that the createAndJoin() function. We pass all the HTML elements to the CallService and launch the answer() method:

function answer() {
  let localVideoEl = document.getElementById('call-local-video');
  let remoteVideosEl = document.getElementById('call-remote-videos');
  let audioEl = document.getElementById('call-audio');
  callSvc.setLocalVideoEl(localVideoEl);
  callSvc.setRemoteVideosEl(remoteVideosEl);
  callSvc.setAudioEl(audioEl);
  logger.log('Trying to answer.');
  callSvc.answer().then( () => {
    logger.succ('Answer successful.')
  }).catch( () => {
    logger.error('Cannot answer. Are you receiving a call?');
  });
}

5.4. Invite

Now is the turn of adding a new participant to an ongoing call. We get the user from the input HTML element and we pass its value to the invite(callee: string) method that is defined in the CallService:

function invite() {
  let callee = document.getElementById('call-callee').value;
  logger.log('Trying to invite to ' + callee + '.');
  callSvc.invite(callee).then( () => {
    logger.succ('Invite sent to ' + callee);
  }).catch( () => {
    logger.error('Cannot send invite. Are you in a conference?');
  });
}

5.5. Refresh session

Finally, we modify the _refreshSession() method and add the setter for the session inside the CallService:

function _refreshSession() {
  // Save the session in the other services
  let session = authSvc.getSession();
  callSvc.setSession(session);
}

Now is the turn of creating the skeleton for our brand new service. Notice that in the constructor we define all the variables that need to be shared between methods.

var CallService = (function() {
  class CallService {
    constructor() {
      this.logger = new LoggerService();
      this.session = null;
      this.conferenceManager = null;
      this.currentConference = null;
      this.localVideoEl = null;
      this.remoteVideosEl = null;
      this.audioEl = null;
    }
  }
  return CallService;
}());

In the following subsections we will define all the methods that are included in the CallService.

6.1. Define setters

We need to include some methods to modify the current values of the variables inside our CallService instance.

When we change the session, we also bind an event to the ConferenceManager. This event is called conferenceInvitation and it will be triggered every time we receive a conference invitation from other user:

setSession(session) {
  this.session = session;
  if (session) {
    this.conferenceManager = this.session.getConferenceManager();
    this.conferenceManager.emitter.on('conferenceInvitation',
        this._onConferenceInvitation.bind(this));
  } else {
    this.conferenceManager.emitter.off('conferenceInvitation',
        this._onConferenceInvitation.bind(this));
    this.conferenceManager = null;
  }
}

We also need to initialize the HTML element in which we will attach the media streams:

setLocalVideoEl(element) {
  this.localVideoEl = element;
}
setRemoteVideosEl(element) {
  this.remoteVideosEl = element;
}
setAudioEl(element) {
  this.audioEl = element;
}

6.2. Create and Join

We create a new conference and bind some events to this conference. This way, if we detect any change in the media streams, we can attach these new streams to the interface.

Finally, we join to the conference:

async createAndJoin() {
  if (!this.session) {
    return Promise.reject();
  }
  this.currentConference = await this.conferenceManager.createConference();
  this.currentConference.emitter.on('localStreamAdded',
      this._onLocalStreamAdded.bind(this));
  this.currentConference.emitter.on('remoteStreams',
      this._onRemoteStreams.bind(this));
  return await this.currentConference.join();
}

6.3. Leave

In case we want to get out of a conference, we can use the leave(codeError: number) method. The value of the codeError is the SIP error code indicating the cause of leaving. Normally, it will be 200.

Although we leave the conference, the rest of the participants can continue with it:

leave() {
  if (!this.session || !this.currentConference) {
    return Promise.reject();
  }
  return this.currentConference.leave(200).then( () => {
    this.currentConference = null;
  });
}

6.4. Answer

This method can be launched by the user when a conferenceInvitation was previously received. In this case we bind the events and launch the join() method from Conference. Just the same way that we have done for createAndJoin():

answer() {
  if (!this.currentConference) {
    return Promise.reject();
  }
  this.currentConference.emitter.on('localStreamAdded',
      this._onLocalStreamAdded.bind(this));
  this.currentConference.emitter.on('remoteStreams',
      this._onRemoteStreams.bind(this));
  return this.currentConference.join();
}

6.5. Invite

We check that a Session and a Conference is defined and, if it is the case, we call the inviteParticipants() method from the Conference instance:

invite(callee) {
  if (!this.session || !this.currentConference) {
    return Promise.reject();
  }
  return this.currentConference.inviteParticipants([callee]);
}

6.6. Local Stream added

This method will be launched every time we receive the event localStreams. In this case we obtain the video stream from the LocalMediaHandler and attach it to the <video> tag from our HTML template:

_onLocalStreamAdded(newStream) {
  this.logger.log('Local stream added received.');
  this.logger.log('Attaching local video stream');
  this.localVideoEl.srcObject = newStream.stream.mediaStream;
}

6.7. Remote Streams

This method is launched every time a remoteStreams event is received. The remote streams can be an audio or video stream. We will have a single audio stream with the audio of all the participants mixed and one video stream per remote participant.

The different between a type and the other is that the audio stream doesn't have a value for the gatewayUsername. This means that its value will be null.

We attach the audio stream directly to the <audio> HTML tag. For the videos we create a video element per participant and we attach it to a div element:

_onRemoteStreams(streams) {
  this.logger.log('Remote stream change received.');
  this.remoteVideosEl.innerHTML = '';
  streams.forEach( (stream, gatewayUsername) => {
    if (gatewayUsername) {
      this.logger.log('Attaching remote video stream.');
      let video = document.createElement('video');
      video.autoplay = true;
      video.setAttribute('playsinline', '');
      video.srcObject = stream[0].mediaStream;
      this.remoteVideosEl.appendChild(video);
    } else {
      this.logger.log('Attaching audio stream.');
      this.audioEl.srcObject = stream[0].mediaStream;
    }
  });
}

6.8. Conference Invitation

Finally, we define the method that will be used each time we detect an conference invitation. This method will receive a Conference instance and we will save it in a class variable.

This is the conference that will be used when the user push the answer button:

_onConferenceInvitation(conference) {
  this.logger.warn('Detected an incoming conference invitation. ' +
  'Press the Answer button to accept it.');
  this.currentConference = conference;
}

Now we have a developed a complete WebRTC application with audio and video capabilities, however we can improve even this with new features.

When executing, the result should be similar to the following one:

t3_final.png

On the next tutorial we will modify the CallService and add some interesting features such as mute audio, video, change the camera or share our screen.

Do not hesitate to contact Quobis if you have any question or to provide feedback info@quobis.com

#alwaysImproving