API Version: Development
/* Enable chat to allow feature (video negotiation) messages to be exchanged */
if ( IPCortex.PBX.enableChat() ) {
console.log(TAG, 'Chat enable, enabling av feature');
/* Register to receive new Av instances */
IPCortex.PBX.enableFeature(
'av',
function(av) {
/* Listen for updates to the Av instance */
av.addListener('update', processFeed);
processFeed(av);
},
['chat']
);
}
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.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) {
/* Only process the Av instance if it has remote media */
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);
/* Accept remote parties offer */
av.accept();
} else if ( av.remoteMedia[id].status == 'connected' && ! video ) {
console.log(TAG, 'New remote media source ' + av.remoteMedia[id]);
/* Create a new video tag to play/display the remote media */
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 ) {
/* Remove any video tags that are no longer in a 'connected' state */
video.parentNode.removeChild(video);
}
}
/* Create a feed container to hold video tags if it doesn't exist */
if ( videos.length && ! feed ) {
feed = document.createElement('div');
document.body.appendChild(feed);
feed.className = 'feed';
feed.id = av.id;
}
/* Add the new video tags to the feed container */
videos.forEach(
function(video) {
feed.appendChild(video);
}
);
/* Remove the feed container if empty */
if ( feed && feed.children.length < 1 )
feed.parentNode.removeChild(feed);
}
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.av.accept()
. The update listener will then fire again with the stream status
as 'connected'.<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.<video>
still exists. This means that the stream has ended and that we need to cleanup - by removing its <video>
./* API is ready, loop through the list of contacts */
IPCortex.PBX.contacts.forEach(
function(contact) {
/* Listen for updates in case the contact changes state */
contact.addListener('update', processContact);
processContact(contact);
}
);
/* Enable chat to allow feature (video negotiation) messages to be exchanged */
var chatEnabled = IPCortex.PBX.enableChat(
function(room) {
/* Listen for updates to clean up dead rooms */
room.addListener('update',
function(room) {
if ( rooms[room.roomID] && room.state == 'dead' )
delete rooms[room.roomID];
}
);
/* If the room has come into existence due to a video request,
start video with the stored stream */
if ( room.cID == media.cID && media.stream ) {
console.log(TAG, 'New room, starting video chat');
/* Listen for updates on the Av instance */
room.videoChat(media.stream).addListener('update', processFeed);
media = {};
}
rooms[room.roomID] = room;
}
);
if (chatEnabled) {
console.log(TAG, 'Chat enable, enabling av feature');
/* Register to receive new Av instances */
IPCortex.PBX.enableFeature(
'av',
function(av) {
/* Listen for updates to the Av instance */
av.addListener('update', processFeed);
processFeed(av);
},
['chat']
);
}
processContact()
function. The 'update' listener is important because we need to listen for changes to online state, as you will see below.var media = {};
var rooms = {};
function processContact(contact) {
/* Don't process contacts that match the logged in user */
if ( contact.cID == IPCortex.PBX.Auth.id )
return;
var element = document.getElementById(contact.cID);
/* Return early if contact exists */
if ( element ) {
/* Remove offline contacts */
if ( ! contact.canChat && element.parentNode )
element.parentNode.removeChild(element);
return;
}
/* Create online contact */
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(
/* Grab the user media */
function(stream) {
/* Check to see if a room already exists to start video on */
for ( var roomID in rooms ) {
if ( rooms[roomID].cID != contact.cID )
continue;
/* Listen for updates on the Av instance */
rooms[roomID].videoChat(stream).addListener('update', processFeed);
return;
}
/* No room, store stream and contact ID and open room */
media = {cID: contact.cID, stream: stream};
contact.chat();
}
).catch(
function() {
console.log(TAG, 'getUserMedia failed');
}
);
}
);
}
}
getUserMedia()
. We then check whether a chatroom with the desired contact already exists or whether one needs to be created.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.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.enableChat()
callback above is now relevant because it will be called with the new chatroom. Here it is again:/* If the room has come into existence due to a video request,
start video with the stored stream */
if ( room.cID == media.cID && media.stream ) {
console.log(TAG, 'New room, starting video chat');
/* Listen for updates on the Av instance */
room.videoChat(media.stream).addListener('update', processFeed);
media = {};
}
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.processFeed()
function which runs when there is an offered stream.var accepted = {};
function processFeed(av) {
/* Only process the Av instance if it has remote media */
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 the remote party is offering create an invite */
if ( accepted[av.id] )
return;
console.log(TAG, 'Offer received from ' + av.remoteMedia[id].cN);
/* Mark the offer as accepted as we may get another
update with the 'offer' state still set */
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);
/* Grab the user media and accept the offer with the returned stream */
navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(
function(stream) {
av.accept(stream);
}
).catch(
function() {
console.log(TAG, 'getUserMedia failed');
}
);
}
);
/* Listen for updates to clean up the invite if the far end cancels */
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]);
/* Create a new video tag to play/display the remote media */
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 ) {
/* Remove any video tags that are no longer in a 'connected' state */
video.parentNode.removeChild(video);
}
}
/* Create a feed container to hold video tags if it doesn't exist */
if ( videos.length && ! feed ) {
feed = document.createElement('div');
document.body.appendChild(feed);
feed.className = 'feed';
feed.id = av.id;
}
/* Add the new video tags to the feed container */
videos.forEach(
function(video) {
feed.appendChild(video);
}
);
/* Remove the feed containers if empty */
if ( feed && feed.children.length < 1 )
feed.parentNode.removeChild(feed);
}
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.