Source: lib/videoCall.js

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
 *     },
 *     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;