Simple call handling
Downloads
Explained
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() {
var numberTag = document.getElementById('number');
var callTag = document.getElementById('call');
var myPhone = IPCortex.PBX.owned[0];
if ( !myPhone || !myPhone.webrtc ) {
throw(Error('Cannot find WebRTC capable device'));
}
myPhone.enableRTC();
myPhone.addListener('update', function (device) {
device.calls.forEach(function(call) {
processCall(call);
});
});
numberTag.addEventListener('keyup', function(e) {
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.
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;
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' ) {
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' ) {
var videoTag = document.getElementById('phone');
if (call.remoteMedia)
attachMediaStream(videoTag, call.remoteMedia[0]);
}
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) {
switch(call.state) {
case('dial'):
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'):
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'):
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'):
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.
calls[call.uid].name.innerHTML = call.label;
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.
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);
calls[call.uid] = {
state: 'new',
elem: elem,
name: elem.getElementsByTagName('span')[0],
buttons: elem.getElementsByTagName('i')
};
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();
});
}