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) { // 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;