API Version: Development

Simple call handling

Downloads

Explained

This sample assumes understanding of the Number Dialing sample as it is used as a starting point.
This simple call handling sample allows calls to be dialed, answered, placed on hold, and disconnected. As per the Number Dialing sample, it uses keevio phone via the API as an interface to WebRTC in order to emulate a VoIP phone.
The <body> HTML used for this project is as follows:
<body>
    <video id="phone" autoplay></video>
    <div id="calls">
        <div class="calls-title">Calls</div>
        <input type="text" id="number" size="20" placeholder="Number to dial"></input>
        <button type="button" id="call">Call</button>
        <div class="call" style="display: none" id="clone">
            <span>Caller name here</span>
            <i class="material-icons call-hangup">call_end</i>
            <i class="material-icons call-hold">phone_paused</i>
            <i class="material-icons call-talk">call</i>
            <i class="material-icons call-ring">ring_volume</i>
        </div>
    </div>
    <script src="call-manage.js"></script>
</body>
This adds a 'calls' area to the previous project, and a hidden <div class="call"> which is cloned as needed in order to display each call. Each 'call' div has four buttons which will be displayed or hidden depending on the call state.
function runApp() {
    /* Grab references to DOM elements */
    var numberTag = document.getElementById('number');
    var callTag = document.getElementById('call');

    /* Grab the first owned device for the logged in user */
    var myPhone = IPCortex.PBX.owned[0];

    /* Very simple error-check */
    if ( !myPhone || !myPhone.webrtc ) {
        throw(Error('Cannot find WebRTC capable device'));
    }
    /* Enable WebRTC connection */
    myPhone.enableRTC();

    /* Wait for new call events to arrive */
    myPhone.addListener('update', function (device) {
        device.calls.forEach(function(call) {
            processCall(call);
        });
    });
    numberTag.addEventListener('keyup', function(e) {
        /* Clean up input box content */
        if ( numberTag.value.search(/[^0-9\*\#]/) != -1 )
            numberTag.value = numberTag.value.replace(/[^0-9\*\#]/, '');
    });
    callTag.addEventListener('click', function(e) {
        if ( !numberTag.value )
            return;
        myPhone.dial(numberTag.value);
        numberTag.value = '';
        e.preventDefault();
    });
}
The main app function is very similar here to that in the basic dial example. In order to clean up the code, the handling of each call update is relocated into processCall(), and a very simple error-check is added to make sure we can find a keevio phone for the current user. Other than that, the same code is used to enable the Device update listener and to manage the number input box and the 'call' button events.
processCall() is actually split into two parts. A new call (the call's unique ID has not been seen before) drops through to newCall() to have the DOM elements created, and it then returns to processCall() so that its visual state can be updated.
processCall() itself can be broken down into smaller chunks:
function processCall(call) {
    /*
     * 1 - If the call is not recognised, create it.
     * 2 - If the call is going-away, delete it.
     * 3 - If the call is newly 'up' then attach it&apos;s audio
     * 4 - Update the buttons
     * 5 - Update the caller name and save state for next time
     */
}

1 - If the call is not recognised, create it.

The call does not exist in our calls storage object - As long as it is not an already-dead call, create it with newCall() (see below) and then come back to continue processing.
if ( !calls[call.uid] ) {
    if( call.state === 'dead' )
        return;
    /* A new call has appeared, and is not 'dead' */
    newCall(call);
}

2 - If the call is going-away, delete it.

If a Call state of 'dead' is received, then that Call object will almost immediately become invalid, so the application needs to clean up all references to the Call object. In this case we also remove the relevant DOM elements that hold references to the call handling functions. Once done return early as no further processing is possible.
if( call.state === 'dead' ) {
    /* A known call is now dead, delete it */
    calls[call.uid].elem.parentNode.removeChild(calls[call.uid].elem);
    delete calls[call.uid];
    return;
}

3 - If the call is newly 'up' then attach its audio

A call that has newly been answered, or has just moved from 'hold' to 'up' is probably the call we want to be listening to, so attach the incoming audio stream to the video tag.
IMPORTANT: In this example, we make no effort to mute() outbound media streams, and assume that the IPCortex Communication System will handle this cleanly for us. In a more complete application that may not be a safe assumption.
if ( call.state === 'up' && calls[call.uid].state !== 'up' ) {
    /* A call not previously 'up' is now 'up' so attach its media stream */
    var videoTag = document.getElementById('phone');
    if (call.remoteMedia)
        attachMediaStream(videoTag, call.remoteMedia[0]);
}

4 - Update the buttons

Depending on the current call state, different icon buttons are relevant. This section of the code compares the current call state to the last known call state, and if they differ, it makes sure that the correct icons are displayed or hidden.
if (call.state !== calls[call.uid].state) {
    /* Call state has changed, so possibly update the control icons
     * The control icons are in an array of
     *    [hangup, hold, talk, ringing]
     */
    switch(call.state) {
        case('dial'):   /* Outbound call being dialled */
            console.log(TAG, 'Call', call.uid, 'Outbound dial state');
            calls[call.uid].buttons[0].style.display = 'block';
            calls[call.uid].buttons[1].style.display = 'none';
            calls[call.uid].buttons[2].style.display = 'none';
            calls[call.uid].buttons[3].style.display = 'block';
            break;
        case('ring'):   /* Inbound call ringing */
            console.log(TAG, 'Call', call.uid, 'Inbound ring state');
            calls[call.uid].buttons[0].style.display = 'block';
            calls[call.uid].buttons[1].style.display = 'none';
            calls[call.uid].buttons[2].style.display = 'block';
            calls[call.uid].buttons[3].style.display = 'block';
            break;
        case('up'):     /* Call is up and proceeding */
            console.log(TAG, 'Call', call.uid, 'is up');
            calls[call.uid].buttons[0].style.display = 'block';
            calls[call.uid].buttons[1].style.display = 'block';
            calls[call.uid].buttons[2].style.display = 'none';
            calls[call.uid].buttons[3].style.display = 'none';
            break;
        case('hold'):   /* Call is on hold */
            console.log(TAG, 'Call', call.uid, 'is on hold');
            calls[call.uid].buttons[0].style.display = 'block';
            calls[call.uid].buttons[1].style.display = 'none';
            calls[call.uid].buttons[2].style.display = 'block';
            calls[call.uid].buttons[3].style.display = 'none';
            break;
        default:
            calls[call.uid].buttons[0].style.display = 'block';
            calls[call.uid].buttons[1].style.display = 'none';
            calls[call.uid].buttons[2].style.display = 'none';
            calls[call.uid].buttons[3].style.display = 'none';
            break;
    };
}

5 - Update the caller name and save state for next time

Finally, update the caller name and save the current state so future changes can be detected.
NOTE: Observe that the Call.label attribute will update if the call is transferred, so needs to be updated regularly to be sure it is accurate.
/* Ensure that the caller name is up to date */
calls[call.uid].name.innerHTML = call.label;

/* Record the call's previous state to detect changes */
calls[call.uid].state = call.state;

newCall()

The newCall() function firstly clones the 'call' div and adds the copied elements into the page, it then searches the for relevant child elements for later use so that the call recipient-name can be updated, and the buttons can be hidden or displayed based on the call state. Before it returns, it adds some on-click events to the relevant buttons to call the Call.talk(), Call.hold() or Call.hangup() methods - These are created with the in-scope copy of the 'call' class, so each of these calls will operate on the relevant call object.
/* For a new call, clone the DOM element to make the call visible,
 * and store the necessary data for later
 */
function newCall(call) {
    var callsTag = document.getElementById('calls');
    var elem = document.getElementById('clone').cloneNode(true);
    elem.style.display = 'block';
    elem.id = call.uid;
    callsTag.appendChild(elem);

    /* Store important call info for later */
    calls[call.uid] = {
        state: 'new',
        elem: elem,
        name: elem.getElementsByTagName('span')[0],
        buttons: elem.getElementsByTagName('i')
    };

    /* Add quick and dirty handlers onto the buttons for this call
     * The control icons are in an array of
     *    [hangup, hold, talk, ringing]
     */
    var inputs = calls[call.uid].buttons;
    inputs[0].addEventListener('click', function() {
        console.log(TAG, 'Hangup call', call.uid);
        call.hangup();
    });
    inputs[1].addEventListener('click', function() {
        console.log(TAG, 'Hold call', call.uid);
        call.hold();
    });
    inputs[2].addEventListener('click', function() {
        console.log(TAG, 'Start/Unhold call', call.uid);
        call.talk();
    });
}