Guide B - Build a Webphone App

This is the second tutorial illustrating how to build custom applications with OnSIP and SIP.js. In the first tutorial, we built a simple WebRTC button application to call a preconfigured destination. We also saw how to interact with OnSIP applications and use basic audio controls.

In this tutorial, we will expand our simple widget to include webphone functionality, such as selecting any destination address to invite, logging in using your own OnSIP identity, receiving inbound invitations, and finally, enabling video calling.

If you prefer to see the final result before starting, you can view the live demo here. The entire application is also available in the OnSIP tutorials repo on GitHub.

OnSIP maintains a webphone application for our cloud phone system customers called the OnSIP app. Feel free to check it out— it's free with an OnSIP account.

Let’s begin!

Start With A Solid Foundation

This tutorial builds upon Guide A - Embed a WebRTC Button. It is highly recommended to begin there. The full code for that Tutorial acts as the starting point for this guide and can be found on GitHub.

Control Your Destination

Our first WebRTC button demo allowed any user to place a call at the click of a button. This makes it ideal for placement on web sites where customers would like to call in to a specific destination, such as a sales or support queue. Most users who are looking for a phone app, however, need the ability to select their own destination, such as another SIP address, the extension or name of another user, or a telephone number.

For this tutorial, we will allow the user to specify a SIP address to invite by entering text input before clicking the invite button. Before we send the Invite, we will read the value of this field to determine where we will send it.

In index.html, add the text input at the top of the user agent div:

1
2
3
4
5
<div id="user-agent">
  <input type="text" id="destination-input"/>
  <button id="invite-button">Invite Session</button>
  ...
</div>

In main.js, save the destination input when the app is constructed:

1
2
3
4
5
function MyApp() {
  // ...

  this.destinationInput = document.getElementById('destination-input');
}

Finally, we modify the sendInvite method to read the destination input:

1
2
3
4
5
6
7
8
9
sendInvite: function () {
  var destination = this.destinationInput.value;
  if (!destination) { return; }

  var session = this.ua.invite(destination, this.remoteMedia);

  this.setSession(session);
  this.inviteButton.disabled = true;
},

You can now call any OnSIP SIP address! If you are an OnSIP customer, you can try calling any SIP address in your account, such as a User, Attendant Menu, or Conference Suite. If you are not an OnSIP customer, you could try a few of these addresses:

  • welcome@onsip.com - the OnSIP Welcome menu.
  • chickens@junctionnetworks.com - “Nobody here but us chickens.”

Use Your Own Identity

So far, we have been sending invite requests anonymously. Anonymous user agents do not have to be configured in advance and are not challenged for credentials. This makes them useful for quickly sending messages to other OnSIP endpoints. When you would like to indicate your own identity to the party you are calling, or if you’d like to receive calls, you need to configure a user agent with your own SIP address and credentials. To do this, you first need an OnSIP user.

  • If you are an OnSIP customer, you already have a User and will use the SIP address and password you use to log in to the Admin Portal. Remember, SIP addresses look like email addresses: user@domain.
  • If you are not yet an OnSIP customer, sign up for a free developer account. Upon account confirmation, you will receive an email containing your SIP address, likely of the form yourname@yourdomain.onsip.com).

It is not good practice to hard code your SIP address and password into an application. Instead, we will set up a form to prompt the user. Let’s also hide the user agent UI until our new identity form is filled out.

In index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
  <!-- Our simply identity form. -->
  <form id="identity-form">
    <label>SIP Address <input type="text" id="address-input"/></label>
    <label>Password <input type="password" id="password-input"/></label>
    <button id="auth-button">Authenticate</button>
  </form>

  <!--
    Initially hide the user agent div.
    We will show it again once the form is filled out.
  -->
  <div id="user-agent" style="display: none">
    ...
  </div>
  ...
</body>

Be sure to make our new form elements available in main.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
function MyApp() {
  this.addressInput = document.getElementById('address-input');
  this.passwordInput = document.getElementById('password-input');
  this.identityForm = document.getElementById('identity-form');
  this.identityForm.addEventListener('submit', function (e) {
    e.preventDefault();
    this.requestCredentials();
  }.bind(this), false);

  this.userAgentDiv = document.getElementById('user-agent');

  // ...
}

Once you have a SIP address and password, you can use the Admin API to fetch your SIP credentials. First, we will request the credentials from the API. When we get a response, we use the credentials to create a user agent. If there is an error fetching the credentials, we will fall back to an anonymous user agent like before. Here are the new and modified methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
MyApp.prototype = {

  /* Request SIP credentials from the Admin API */
  requestCredentials: function () {
    var xhr = new XMLHttpRequest();
    xhr.onload = this.setCredentials.bind(this);
    xhr.open('get', 'https://api.onsip.com/api/?Action=UserRead&Output=json');

    var userPass = this.addressInput.value + ':' + this.passwordInput.value;
    xhr.setRequestHeader('Authorization',
                         'Basic ' + btoa(userPass));
    xhr.send();
  },

  /*
   * When we get a response to the API, read it to build
   * a credentials object for SIP.js configuration.
   */
  setCredentials: function (e) {
    var xhr = e.target;
    var user, credentials;

    if (xhr.status === 200) {
      user = JSON.parse(xhr.responseText).Response.Result.UserRead.User;
      credentials = {
        uri: this.addressInput.value,
        authorizationUser: user.AuthUsername,
        password: user.Password,
        displayName: user.Contact.Name
      };
    } else {
      alert('Authentication failed! Proceeding as anonymous.');
      credentials = {};
    }

    this.createUA(credentials);
  },

  /*
   * We modify our previous `createUA` method to
   * accept a credentials object as an argument.
   *
   * Since the identity process is complete, hide the form
   * and show the user agent div.
   */
  createUA: function (credentials) {
    this.identityForm.style.display = 'none';
    this.userAgentDiv.style.display = 'block';
    this.ua = new SIP.UA(credentials);
  },

  sendInvite: function () {...},
  setSession: function () {...},
  setStatus: function () {...},
  raiseVolume: function () {...},
  lowerVolume: function () {...},
  toggleMute: function () {...},
};

Because we are requesting credentials and creating a UA after the identity form is submitted, we no longer want to create the UA right away:

1
2
3
var myApp = new MyApp();
// myApp.createUA();
}());

Next, take your SIP address (user@domain) and password and enter them into the two fields in our new identity form. Now, when you invite other endpoints to join sessions with you, they will see your identity, rather than Anonymous. If your account has Public Switched Telephone Network (PSTN) credits, you can also place authenticated calls to the PSTN just by entering in the 11 digit phone number. If you mistype your SIP address or password, the app will alert the error and proceed anonymously.

Receive Inbound Calls

Once we have our identity set up, we are ready to not only invite sessions but to receive Invites as well. This lets us accept inbound sessions from other users, Queues, or the public telephone network. First, we have the SIP.js user agent listen for incoming Invite requests. This will look familiar, as it closely mirrors the session events we listened for in the previous tutorial.

1
2
3
4
5
6
7
8
// Update `createUA` to listen for Invite requests
createUA: function (credentials) {
  this.identityForm.style.display = 'none';
  this.userAgentDiv.style.display = 'block';
  this.ua = new SIP.UA(credentials);

  this.ua.on('invite', this.handleInvite.bind(this));
},

In the handleInvite method, SIP.js provides us with the incoming Session. For now, we will just accept it right away. We can even reuse our setSession method and remoteMedia elements to set up our session event listeners and render the remote audio, respectively. If we receive an invitation to join a session while we are already in a session with someone else, we reject this new incoming request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
MyApp.prototype = {
  requestCredentials: function () {...},
  setCredentials: function (e) {...},

  createUA: function (credentials) {
    this.identityForm.style.display = 'none';
    this.userAgentDiv.style.display = 'block';
    this.ua = new SIP.UA(credentials);

    this.ua.on('invite', this.handleInvite.bind(this));
  },

  handleInvite: function () {
    if (this.session) {
      session.reject();
      return;
    }

    this.setSession(session);

    this.setStatus('Ring ring! ' + session.remoteIdentity.uri.toString() + ' is calling!', true);

    this.acceptButton.disabled = false;
  },

  sendInvite: function () {...},
  setSession: function () {...},
  setStatus: function () {...},
  raiseVolume: function () {...},
  lowerVolume: function () {...},
  toggleMute: function () {...},
};

Once we notify the user that their phone is ringing, they can click the Accept button to begin the session.

1
2
3
4
5
<div id="user-agent" style="display: none">
 ...
 <button id="accept-button" disabled>Accept Session</button>
 <button id="terminate-button"> Terminate Session</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function MyApp() {
  // ...

  this.acceptButton = document.getElementById('accept-button');
  this.acceptButton.addEventListener('click', this.acceptSession.bind(this), false);
  
}

MyApp.prototype = {
  requestCredentials: function () {...},
  setCredentials: function (e) {...},
  createUA: function (credentials) {...},
  handleInvite: function () {...},

  acceptSession: function () {
    if (!this.session) { return; }

    this.acceptButton.disabled = true;
    this.session.accept(this.remoteMedia);
  },

  sendInvite: function () {...},
  setSession: function () {...},
  setStatus: function () {...},
  raiseVolume: function () {...},
  lowerVolume: function () {...},
  toggleMute: function () {...},
};

Try opening the app in two tabs or windows, or recruit a fellow OnSIP User to log in using their identity. You should be able to sign in and enter their SIP address to join a two-way audio session with them.

Enable Video Calling

We now have an application that can authenticate with its own identity and make and receive calls to any destination. If you have a device with a camera, you can also step it up a notch and enable video calling. SIP.js makes it easy:

In index.html, replace our <audio> element with a <video> element:

1
2
  <!-- <audio id="remote-media"></audio> -->
  <video id="remote-media"></video>

This works through the magic of ua.invite(target, remoteMediaElement) and session.accept(remoteMediaElement). SIP.js inspects the type of media element it is provided and automatically switches between audio and video to match. Try opening the application in two tabs or windows. Can you video call yourself?

All Done!

Congratulations, again! You have now built not only an app that can place WebRTC calls but also a fully capable webphone. From here, the sky is the limit. Will you customize your webphone to integrate tightly with your application or platform, or perhaps add new features that only you have imagined?

Topics: Developer Docs