Video Client
Downloads
Explained
This is an example of a video client. We will take you through the steps to create a functional video client
using the video and chat subsystems. This will include accepting video request with and without a media stream,
processing contacts and chat rooms.
One way video, receive only
if ( IPCortex.PBX.enableChat() ) {
console.log(TAG, 'Chat enable, enabling av feature');
IPCortex.PBX.enableFeature(
'av',
function(av) {
av.addListener('update', processFeed);
processFeed(av);
},
['chat']
);
}
After a successful IPCortex.PBX.startFeed()
, two steps are required to set up the video ('av') subsystem. The first is to enable the chat subsystem with IPCortex.PBX.enableChat()
which returns a boolean indicating success/failure. The 'av' negotiation messages are exchanged via the chat subsystem, although these are transparent to the user.
The second step is to enable the 'av' subsystem with IPCortex.PBX.enableFeature('av', callback, ['chat'])
. The last parameter is an Array
of transport mechanisms. Currently the only supported one is chat, but this option is here to allow future updates. The callback is called on every inbound (offered) 'av' request. Inside our callback we delegate initial processing and processing of subsequent updates to our 'av' call to the processFeed()
function.
function processFeed(av) {
if ( typeof(av.remoteMedia) != 'object' )
return;
var videos = [];
var feed = document.getElementById(av.id);
for ( var id in av.remoteMedia ) {
var video = document.getElementById(id);
if ( av.remoteMedia[id].status == 'offered' ) {
console.log(TAG, 'Accepting offer ' + av.remoteMedia[id].cN);
av.accept();
} else if ( av.remoteMedia[id].status == 'connected' && ! video ) {
console.log(TAG, 'New remote media source ' + av.remoteMedia[id]);
video = document.createElement('video');
attachMediaStream(video, av.remoteMedia[id]);
videos.push(video);
video.id = id;
video.play();
} else if ( av.remoteMedia[id].status != 'connected' && video ) {
video.parentNode.removeChild(video);
}
}
if ( videos.length && ! feed ) {
feed = document.createElement('div');
document.body.appendChild(feed);
feed.className = 'feed';
feed.id = av.id;
}
videos.forEach(
function(video) {
feed.appendChild(video);
}
);
if ( feed && feed.children.length < 1 )
feed.parentNode.removeChild(feed);
}
The majority of processFeed()
is <video>
DOM management which we will not go through here but the thing to look at is the for
loop over the av.remoteMedia
array. A loop is required because an 'av' instance can have multiple streams.
For each stream, we are interested in three cases:
- The stream is 'offered'. This means that the 'av' call is inbound and hasn't been answered yet. In this case we accept the call with
av.accept()
. The update listener will then fire again with the stream status
as 'connected'.
- The stream is 'connected' but there is no associated
<video>
in the DOM. This means that we haven't processed this stream yet - we have just accepted it. In this case we create a <video>
and attach the stream to it.
- The stream is no longer connected but its associated
<video>
still exists. This means that the stream has ended and that we need to cleanup - by removing its <video>
.
In all other cases, we don't need to perform any specific action.
TEST - Load the page over HTTPS and login using incognito mode (Chrome) to avoid clashes.
Start keevio, login as a different user, open a chat room with the first user and start video.
You should see the keevio user's feed on the incognito browser window.
Two way video, offer only
IPCortex.PBX.contacts.forEach(
function(contact) {
contact.addListener('update', processContact);
processContact(contact);
}
);
var chatEnabled = IPCortex.PBX.enableChat(
function(room) {
room.addListener('update',
function(room) {
if ( rooms[room.roomID] && room.state == 'dead' )
delete rooms[room.roomID];
}
);
if ( room.cID == media.cID && media.stream ) {
console.log(TAG, 'New room, starting video chat');
room.videoChat(media.stream).addListener('update', processFeed);
media = {};
}
rooms[room.roomID] = room;
}
);
if (chatEnabled) {
console.log(TAG, 'Chat enable, enabling av feature');
IPCortex.PBX.enableFeature(
'av',
function(av) {
av.addListener('update', processFeed);
processFeed(av);
},
['chat']
);
}
The first difference from the 'one-way' sample above is that we have to get a list of contacts to allow the user to initiate a call with someone. As usual, for cleanliness, we delegate initial and subsequent processing of contacts to the processContact()
function. The 'update' listener is important because we need to listen for changes to online state, as you will see below.
Ignore the section after the room 'update' listener for now: we will come back to it.
var media = {};
var rooms = {};
function processContact(contact) {
if ( contact.cID == IPCortex.PBX.Auth.id )
return;
var element = document.getElementById(contact.cID);
if ( element ) {
if ( ! contact.canChat && element.parentNode )
element.parentNode.removeChild(element);
return;
}
if ( contact.canChat ) {
var element = document.createElement('div');
document.getElementById('contacts').appendChild(element);
element.innerHTML = contact.name;
element.className = 'contact';
element.id = contact.cID;
var offer = document.createElement('i');
element.appendChild(offer);
offer.className = 'material-icons contact-offer';
offer.innerHTML = 'video_call';
offer.addEventListener('click',
function() {
navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(
function(stream) {
for ( var roomID in rooms ) {
if ( rooms[roomID].cID != contact.cID )
continue;
rooms[roomID].videoChat(stream).addListener('update', processFeed);
return;
}
media = {cID: contact.cID, stream: stream};
contact.chat();
}
).catch(
function() {
console.log(TAG, 'getUserMedia failed');
}
);
}
);
}
}
Again, there is a lot of DOM management here - maintaining the list of online contacts. The section to note is the 'click' listener on the 'offer' button. The user will click this to initiate a video call with that contact. In the handler, we first get the user's own media stream with the webRTC getUserMedia()
. We then check whether a chatroom with the desired contact already exists or whether one needs to be created.
If a chatroom exists then all that remains to do is to start a video call in that room with room.videoChat(stream)
. Note that we pass it the user's media and the 'av' instance is returned. On this we subscribe to updates, and manage the 'av' streams in the same way as last time - with the processFeed()
function.
If a chatroom doesn't yet exist, then we have to create one first. We do this with contact.chat()
. Because the room creation is asynchronous, we populate the media object with the contact's ID and our media stream (to avoid having to request it from the user again) so that we can pass it to the callback.
The section of code that we ignored in the enableChat()
callback above is now relevant because it will be called with the new chatroom. Here it is again:
if ( room.cID == media.cID && media.stream ) {
console.log(TAG, 'New room, starting video chat');
room.videoChat(media.stream).addListener('update', processFeed);
media = {};
}
If it's the room we're interested in (the one we just created) then its contact ID (cID) will match the one in the media
object. We now have a room with the desired contact, and our own media stream so we are ready to call room.videoChat()
in the same way as before. Finally, we empty the media
object because we have finished passing the call information to the chat handler.
TEST - Load the page over HTTPS and login using incognito mode (Chrome) to avoid clashes. Start keevio and login as
another user. In the incognito window, click the video icon next to the keevio user. This should show a video request in keevio.
Two way video, offer and accept
To extend our sample to accept an incoming call, we simply have to extend the processFeed()
function which runs when there is an offered stream.
var accepted = {};
function processFeed(av) {
if ( typeof(av.remoteMedia) != 'object' )
return;
var videos = [];
var feed = document.getElementById(av.id);
for ( var id in av.remoteMedia ) {
var video = document.getElementById(id);
if ( av.remoteMedia[id].status == 'offered' ) {
if ( accepted[av.id] )
return;
console.log(TAG, 'Offer received from ' + av.remoteMedia[id].cN);
accepted[av.id] = true;
invite = document.createElement('div');
document.body.appendChild(invite);
invite.innerHTML = 'VIDEO CHAT: ' + av.remoteMedia[id].cN;
invite.className = 'invite';
var accept = document.createElement('i');
invite.appendChild(accept);
accept.className = 'material-icons contact-accept';
accept.innerHTML = 'done';
accept.addEventListener('click',
function() {
invite.parentNode.removeChild(invite);
navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(
function(stream) {
av.accept(stream);
}
).catch(
function() {
console.log(TAG, 'getUserMedia failed');
}
);
}
);
av.addListener('update',
function(av) {
if ( av.state == 'closed' )
invite.parentNode.removeChild(invite);
}
);
} else if ( av.remoteMedia[id].status == 'connected' && ! video ) {
console.log(TAG, 'New remote media source ' + av.remoteMedia[id]);
video = document.createElement('video');
attachMediaStream(video, av.remoteMedia[id]);
videos.push(video);
video.id = id;
video.play();
} else if ( av.remoteMedia[id].status != 'connected' && video ) {
video.parentNode.removeChild(video);
}
}
if ( videos.length && ! feed ) {
feed = document.createElement('div');
document.body.appendChild(feed);
feed.className = 'feed';
feed.id = av.id;
}
videos.forEach(
function(video) {
feed.appendChild(video);
}
);
if ( feed && feed.children.length < 1 )
feed.parentNode.removeChild(feed);
}
The main difference to note between this processFeed()
and the previous is that we add an accept button when there is an incoming call. The click handler calls av.accept()
function as before, except that it also passes the user's media stream.
We now have a two-way video calling application which displays a list of contacts for the user to initiate calls with. It also notifies the user of inbound calls and allows them to accept it.