var Janus = require('../externals/janus.nojquery');
var Promise = require('bluebird/js/release/bluebird');
var DataChannel = require('../modules/datachannel');
/**
* VideoCall usecase
* @class
* @classdesc VideoCall usecase
* @param {object} onEvents Event handlers: onAccepted, onCalling, onDataReceived, onGotPeers, onHangUp, onIncomingCall, onRegistered, onSetCall, onFileTransferOk, onFileTransferKo
* @param {object} domElements DOM elements: videos
* @param {object} options Available options: dataChannel, display
* @return {Promise} VideoCall methods: call, acceptCall, closeUsecase, getPeers, hangUp, register, sendData, setCall, toggleAudio, toggleVideo
* @example
* var onEvents = {
* onAccepted: function(userName) {
* // Accepted
* },
* onCalling: function() {
* // Calling
* },
* onDataReceived: function(type, data, filename) {
* // Data received
* if(type === 'application/x-chat') { }
* else if(type === 'text/plain') { }
* else if(type === 'application/pdf') { }
* else if(type === 'application/zip') { }
* else if(type === 'application/x-rar') { }
* else if(type === 'image/jpeg') {}
* else if(type === 'image/png') {}
* else if(type === 'application/x-docx') {}
* else if(type === 'application/x-pptx') {}
* else if(type === 'application/x-xlsx') {}
* else if(type === 'application/vnd.oasis.opendocument.text') {}
* },
* onGotPeers: function(list) {
* // Peers registered ready
* },
* onHangUp: function(userName, reason) {
* // HangUp / Decline
* },
* onIncomingCall: function(userName) {
* // Incoming call
* },
* onRegistered: function(userName, isRegistered) {
* // Registered.
* // If username is already registered it returns: "username 'name' is already taken"
* },
* onSetCall: function() {
* // Set
* },
* onFileTransferOk: function(fileId) {
* // File Transfer OK
* },
* onFileTransferKo: function(fileId) {
* // File Transfer KO
* }
* };
*
* var domElements = {
* videos: document.getElementById('videos')
* };
*
* var options = { // Optional
* dataChannel: {
* dataEnabled: true,
* allowedTypes: ['application/x-chat', 'image/jpeg', 'image/png', 'application/pdf'],
* maxSize: 5, // In MB
* fileTransmission: {
* timeout: 5, // Minutes
* retransmissionDelay: 15 // Seconds
* }
* },
* display: {
* namePattern: /^__default__/i
* }
* };
*
* usecases.videoCall(onEvents, domElements, options)
* .then(function(action) {
* // Use Case has been atacched succesfully
* ...
* })
* .catch(function(cause) {
* // Error attaching the Use Case
* console.log("Error Attach " + cause );
* })
*/
var videoCall = function(onEvents, domElements, options) {
var videocallHandle = null;
var bitrateTimer = null;
var remoteFeedIndex = "";
var remoteFeedName = "Name";
var jsep = null;
var getPeersFilter = null;
var getPeersOrder = null;
// Display options
var optionsDisplay = (options && options.display) ? options.display : {};
/**
* Close the current UseCase. It's recommended combine with disconnect method
* @return {nothing}
* @example
* action.closeUsecase();
* myVideoApp.disconnect(); // Recommended
*/
var closeUsecase = function() {
if (domElements) {
for (var element in domElements) {
if (!domElements.hasOwnProperty(element))
continue;
var obj = domElements[element];
if(obj && obj.innerHTML) obj.innerHTML = '';
}
}
};
/**
* Get list of peers registered/incall (Array)
* @param {RegExp} filter RegExp to filter the list of peers. e.g: /^__default__/i
* @param {string} order ["ASC"|"DESC"] Order array of peers
* @return {nothing}
*/
var getPeers = function(filter, order) {
getPeersFilter = filter;
getPeersOrder = order;
videocallHandle.send({
"message": {
"request": "list"
}
});
};
/**
* Register a peer in the VideoCall Usecase
* @param {string} userName (Alphanumeric)
* @return {nothing}
*/
var register = function(userName) {
videocallHandle.send({
"message": {
"request": "register",
"username": userName || Date.now() + ''
}
});
};
/**
* Configure the call settings on the fly
* @param {boolean} audio Audio Enabled
* @param {boolean} video Video Enabled
* @param {string} quality Call Quality: "high", "medium", "low"
* @return {nothing}
*/
var setCall = function(audio, video, quality) {
var bitrate = 128000;
if (quality === 'high')
bitrate = 256000;
else if (quality === 'low')
bitrate = 64000;
videocallHandle.send({
"message": {
"request": "set",
"audio": audio,
"video": video,
"bitrate": bitrate || 64000,
"record": false
}
});
};
/**
* Hang up/Decline a call
* @return {nothing}
*/
var hangUp = function() {
videocallHandle.send({
"message": {
"request": "hangup"
}
});
//videocallHandle.hangup();
};
/**
* Accept an incoming call
* @return {nothing}
*/
var acceptCall = function() {
videocallHandle.createAnswer({
jsep: jsep,
// No media provided: by default, it's sendrecv for audio and video
media: {
data: true
}, // Let's negotiate data channels as well
success: function(jsep) {
var body = {
"request": "accept"
};
videocallHandle.send({"message": body, "jsep": jsep});
},
error: function(error) {
Janus.error("WebRTC error:", error);
}
});
};
/**
* Call to peer registered
* @param {string} userName Name of a peer registered
* @return {nothing}
*/
var call = function(userName) {
videocallHandle.createOffer({
// By default, it's sendrecv for audio and video...
media: {
data: true
}, // ... let's negotiate data channels as well
success: function(jsep) {
var body = {
"request": "call",
"username": userName
};
videocallHandle.send({"message": body, "jsep": jsep});
},
error: function(error) {
Janus.error("WebRTC error...", error);
}
});
};
/**
* Sends a message (Chat or File) through the DataChannel
* @param {string} type MIME Type (e.g: 'application/x-chat', 'text/plain', 'application/pdf', 'application/zip', 'application/x-rar', 'image/jpeg', 'image/png', 'application/x-docx', 'application/x-pptx', 'application/x-xlsx', 'application/vnd.oasis.opendocument.text')
* @param {string} data Data Content
* @param {function} cOk Callback success function
* @param {function} cKo Callback failed function
* @param {string} (Optional) filename File Name (e.g: file.zip)
* @return {nothing}
* @example
* action.sendData('application/x-chat', 'Hello Mike!',
* function(cOk) {
* // Success
* },
* function(error) {
* // Error
* console.log(error);
* }
* )
*/
var sendData = function(type, data, cOk, cKo, filename) {
DataChannel.send(type, data, cOk, cKo, filename);
};
/**
* Toggle local Audio stream (Mute/Unmute)
* @return {boolean} Is audio muted?
* @example
* action.toggleAudio(); // true or false
*/
var toggleAudio = function() {
if (videocallHandle.isAudioMuted())
videocallHandle.unmuteAudio();
else
videocallHandle.muteAudio();
return videocallHandle.isAudioMuted();
};
/**
* Toggle local Video stream (Mute/Unmute)
* @return {boolean} Is video muted?
* @example
* action.toggleVideo(); // true or false
*/
var toggleVideo = function() {
if (videocallHandle.isVideoMuted())
videocallHandle.unmuteVideo();
else
videocallHandle.muteVideo();
return videocallHandle.isVideoMuted();
};
return new Promise(function(resolve, reject) {
window.VideoRTC.connection.handle.attach({
plugin: "janus.plugin.videocall",
success: function(pluginHandle) {
videocallHandle = pluginHandle;
// DataChannel options
var optionsDataChannel = (options && options.dataChannel) ? options.dataChannel : {};
var cbks = {
fileOk: onEvents.onFileTransferOk || function() {},
fileKo: onEvents.onFileTransferKo || function() {}
};
var result = DataChannel.initialize(optionsDataChannel, pluginHandle, cbks);
if(!result) console.log("Datachannel options can't be loaded:");
resolve({
call: call,
acceptCall: acceptCall,
closeUsecase: closeUsecase,
getPeers: getPeers,
hangUp: hangUp,
register: register,
sendData: sendData,
setCall: setCall,
toggleAudio: toggleAudio,
toggleVideo: toggleVideo
});
},
error: function(cause) {
Janus.error(" -- Error attaching plugin...", cause);
reject(cause);
},
consentDialog: function(on) {
// Nothing
},
onmessage: function(msg, jsepp) {
if (jsepp)
jsep = jsepp;
var result = msg["result"];
if (result !== null && result !== undefined) {
if (result["list"] !== undefined && result["list"] !== null) {
var list = [];
if(getPeersFilter instanceof RegExp) {
for(var i = 0; i < result["list"].length; i++) {
if(getPeersFilter.test(result["list"][i])) {
list.push(result["list"][i]);
}
}
}
else {
list = result["list"];
}
if(getPeersOrder === 'ASC') {
list.sort();
}
else if (getPeersOrder === 'DESC') {
list.reverse();
}
if (onEvents && onEvents.onGotPeers)
onEvents.onGotPeers(list);
}
else if (result["event"] !== undefined && result["event"] !== null) {
var event = result["event"];
if (event === 'registered') {
Janus.log("Successfully registered as " + result["username"] + "!");
if (onEvents && onEvents.onRegistered)
onEvents.onRegistered(result["username"], true);
}
else if (event === 'calling') {
Janus.log("Waiting for the peer to answer...");
if (onEvents && onEvents.onCalling)
onEvents.onCalling();
}
else if (event === 'incomingcall') {
Janus.log("Incoming call from " + result["username"] + "!");
remoteFeedName = result["username"];
if (onEvents && onEvents.onIncomingCall)
onEvents.onIncomingCall(result["username"]);
}
else if (event === 'accepted') {
var peer = result["username"];
if (peer === null || peer === undefined) {
Janus.log("Call started!");
} else {
Janus.log(peer + " accepted the call!");
remoteFeedName = peer;
}
// TODO Video call can start
if (jsep !== null && jsep !== undefined)
videocallHandle.handleRemoteJsep({jsep: jsep});
if (onEvents && onEvents.onAccepted)
onEvents.onAccepted(result["username"]);
}
else if (event === 'hangup') {
Janus.log("Call hung up by " + result["username"] + " (" + result["reason"] + ")!");
if (onEvents && onEvents.onHangUp)
onEvents.onHangUp(result["username"], result["reason"]);
}
else if (event === 'set') {
if (onEvents && onEvents.onSetCall)
onEvents.onSetCall();
}
}
} else {
var error = msg["error"];
if (error.indexOf("already taken") > 0) {
if (onEvents && onEvents.onRegistered)
onEvents.onRegistered(error, false);
}
videocallHandle.hangup();
if (bitrateTimer !== null && bitrateTimer !== null)
clearInterval(bitrateTimer);
bitrateTimer = null;
}
},
onlocalstream: function(stream) {
// We have a local stream (getUserMedia worked!) to display
Janus.debug(" ::: Got a local stream :::");
var container = domElements.videos;
var newNode = document.createElement('div');
newNode.id = 'localvideo';
newNode.className = 'videortc-video-container';
newNode.innerHTML = '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="videolocal1"></video>';
container.insertBefore(newNode, container.firstChild);
var videoElem = document.getElementById('videolocal1');
Janus.attachMediaStream(videoElem, stream);
videoElem.muted = true;
var videoTracks = stream.getVideoTracks();
if (videoTracks === null || videoTracks === undefined || videoTracks.length === 0) {
// No remote video
var nodeNoCamera = document.createElement('div');
nodeNoCamera.innerHTML = 'No camera available';
nodeNoCamera.style.position = "relative";
nodeNoCamera.style.height = "100%";
nodeNoCamera.style.width = "100%";
nodeNoCamera.style.top = "100%";
nodeNoCamera.style.left = "50%";
nodeNoCamera.style.transform = "translate(-50%,-50%)";
nodeNoCamera.className = "no-camera-available";
newNode.insertBefore(nodeNoCamera, newNode.firstChild);
}
},
onremotestream: function(stream) {
Janus.debug("Remote feed #" + remoteFeedIndex);
var generateLabel = function(id, content, backgroundColor, position) {
var output = '';
output += '<span id="' + id + '" style="';
output += ' position:absolute;margin:10px;padding:3px 5px;color:white;font-weight:bold;border-radius:5px;';
output += 'background:' + backgroundColor + ';';
output += position;
output += '">' + content + '</span>';
return output;
};
var container = domElements.videos;
var target = document.getElementById('container-remote' + remoteFeedIndex);
var tpl = '';
var displayName = remoteFeedName;
if(optionsDisplay && optionsDisplay.namePattern && optionsDisplay.namePattern instanceof RegExp) {
displayName = displayName.replace(optionsDisplay.namePattern, '');
}
if (!target || target.innerHTML.length === 0) {
tpl += '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="videoremote' + remoteFeedIndex + '"></video>';
tpl += generateLabel('remote-name' + remoteFeedIndex, displayName, '#55D8D5', 'left:0; margin-top: 10px;');
tpl += generateLabel('remote-resolution' + remoteFeedIndex, '', '#8DCC6D', 'left:0; margin-top: 40px;');
tpl += generateLabel('remote-bitrate' + remoteFeedIndex, '', '#FF6E5F', 'left:0; margin-top: 70px;');
var newNode = document.createElement('div');
newNode.id = 'container-remote' + remoteFeedIndex;
newNode.className = 'videortc-video-container';
newNode.innerHTML = tpl;
container.appendChild(newNode);
var videoElem = document.getElementById('videoremote' + remoteFeedIndex);
videoElem.onplaying = function() {
var width = this.videoWidth;
var height = this.videoHeight;
document.getElementById('remote-resolution' + remoteFeedIndex).innerHTML = width + 'x' + height;
if (window.VideoRTC.getBrowser() === 'firefox') {
// Firefox Stable has a bug: width and height are not immediately available after a playing
setTimeout(function() {
var width = videoElem.videoWidth;
var height = videoElem.videoHeight;
document.getElementById('remote-resolution' + remoteFeedIndex).innerHTML = width + 'x' + height;
}, 2000);
}
};
Janus.attachMediaStream(videoElem, stream);
var videoTracks = stream.getVideoTracks();
if (videoTracks === null || videoTracks === undefined || videoTracks.length === 0 || videoTracks[0].muted) {
// No remote video
var nodeNoCamera = document.createElement('div');
nodeNoCamera.innerHTML = 'No camera available';
nodeNoCamera.style.position = "relative";
nodeNoCamera.style.height = "100%";
nodeNoCamera.style.width = "100%";
nodeNoCamera.style.top = "100%";
nodeNoCamera.style.left = "50%";
nodeNoCamera.style.transform = "translate(-50%,-50%)";
nodeNoCamera.className = "no-camera-available";
newNode.insertBefore(nodeNoCamera, newNode.firstChild);
// We Hide Bitrate and Resolution labels
for (var i = 0; i < newNode.childNodes.length; i++) {
if (newNode.childNodes[i].id && (newNode.childNodes[i].id.indexOf('remote-resolution') >= 0 || newNode.childNodes[i].id.indexOf('remote-bitrate') >= 0)) {
newNode.childNodes[i].style.display = 'none';
}
}
}
bitrateTimer = setInterval(function() {
// Display updated bitrate, if supported
var bitrate = videocallHandle.getBitrate();
document.getElementById('remote-bitrate' + remoteFeedIndex).innerHTML = bitrate;
}, 1000);
}
},
ondataopen: function(data) {
Janus.log("The DataChannel is available!");
},
ondata: function(data) {
Janus.debug("We got data from the DataChannel! " + data);
var displayName = remoteFeedName;
if(optionsDisplay && optionsDisplay.namePattern && optionsDisplay.namePattern instanceof RegExp) {
displayName = displayName.replace(optionsDisplay.namePattern, '');
}
DataChannel.receive(data, onEvents.onDataReceived, displayName);
},
oncleanup: function() {
Janus.log(" ::: Got a cleanup notification :::");
if (bitrateTimer !== null && bitrateTimer !== null)
clearInterval(bitrateTimer);
bitrateTimer = null;
},
detached: function() {
// Nothing
}
});
});
};
exports.videoCall = videoCall;