Source: lib/videoRoom.js

var Janus = require('../externals/janus.nojquery');
var Promise = require('bluebird/js/release/bluebird');

/**
 * VideoRoom usecase
 * @class
 * @classdesc VideoRoom usecase
 * @param  {object} onEvents       Event handlers: onCreated, onError, onJoined, onDestroyed, onPeerJoined, onPeerLeft
 * @param  {object} domElements    DOM elements: videos
 * @param  {object} options        options: stream
 * @return {Promise}  VideoRoom methods: createVideoroom, destroyVideoroom, getPeers, joinVideoroom, toggleAudio, toggleVideo
 * @example
 * var onEvents = {
 *     onCreated: function(id) {
 *          // Created
 *     },
 *     onError: function(cause, code) {
 *          // Error
 *     },
 *     onJoined: function() {
 *          // Joined
 *     },
 *     onDestroyed: function() {
 *          // Destroyed
 *     },
 *     onPeerJoined: function(id, display) {
 *          // Peer Joined
 *     },
 *     onPeerLeft: function(id, display) {
 *          // Peer Left
 *     }
 * };
 *
 * var domElements = {
 *     videos: document.getElementById('videortc-videos') // App container
 * };
 *
 * var options = {
 *     stream: {
 *         audioEnabled: true,
 *         videoEnabled: true,
 *         aDeviceId: null,
 *         vDeviceId: null,
 *         voiceProcessor: false
 *     }
 * };
 *
 * usecases.videoRoom(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 videoRoom = function(onEvents, domElements, options) {

    if(!domElements || !domElements.videos || domElements.videos.nodeType !== 1) return null;

    // Stream options
    var streamOptions = (options && options.stream) ? options.stream : {};
    if(!streamOptions.audioEnabled) streamOptions.audioEnabled = false;
    if(!streamOptions.videoEnabled) streamOptions.videoEnabled = false;
    if(!streamOptions.aDeviceId) streamOptions.aDeviceId = undefined;
    if(!streamOptions.vDeviceId) streamOptions.vDeviceId = undefined;
    if(!streamOptions.voiceProcessor) streamOptions.voiceProcessor = false;

    // Vars
    var videoroomHandle = null;
    var feeds = [];
    var bitrateTimer = [];
    var idVideoroom;
    var nPeers = 1;
    var roomPin = null;

    /**
     * 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 = '';
        }
      }
    };

    /**
     * Creates a new VideoRoom / Conference
     * @param  {Boolean} isPrivate       Is private?
     * @param  {string}  adminKey        Administrator Room Key (Destroy)
     * @param  {string}  userKey         User Room Key (Join)
     * @param  {string|enum}  quality    Quality: "high", "medium" or "low"
     * @param  {integer}  maxPublishers  Number of publishers/peers
     * @param  {Boolean} isRecord        Record the videoroom
     * @return {nothing}
     * @example
     * action.createVideoroom(true, 'MyP455w0rd', 'Us3rPw', 'medium', 6, false);
     */
    var createVideoroom = function(isPrivate, adminKey, userKey, quality, maxPublishers, isRecord) {
        var bitRate = 128000;
        var mPublishers = maxPublishers;
        if(quality.toLowerCase() === 'high') bitRate = 256000;
        if(quality.toLowerCase() === 'medium') bitRate = 128000;
        if(quality.toLowerCase() === 'low') bitRate = 64000;
        if(maxPublishers > 6) mPublishers = 6;
        var register = {
            "request": "create",
            "is_private": isPrivate || true,
            "secret": adminKey || Date.now()+'', // To destroy
            "bitrate": bitRate,
            "publishers": mPublishers || 6,
            "record": isRecord || false
        };
        if(userKey) register.pin = userKey;
        videoroomHandle.send({
            "message": register,
            "success": function(data) {
                // VideoRoom created
                if(onEvents && onEvents.onCreated) onEvents.onCreated(data.room);
            }
        });
    };

    /**
     * Destroys a Videoroom (Admin key is required)
     * @param  {integer} id       VideoRoom Id
     * @param  {string} adminKey Administrator Room Key
     * @return {nothing}
     * @example
     * action.destroyVideoroom(743267423, 'MyP455w0rd');
     */
    var destroyVideoroom = function(id, adminKey) {
        var request = {
            "request": "destroy",
            "room": id,
            "secret": adminKey
        };
        videoroomHandle.send({"message": request});
    };

    /**
     * Join into a VideoRoom
     * @param  {string} name User Name
     * @param  {integer} idRoom VideoRoom ID
     * @param  {string} pin Videoroom PIN to Join (Optional)
     * @return {nothing}
     * @example
     * action.joinVideoroom('Mike', 743267423, 'ivr1234');
     */
    var joinVideoroom = function(name, idRoom, pin) {
        var username = name || 'Peer';
        idVideoroom = idRoom || 1234;
        var register = { "request": "join", "room": idVideoroom, "ptype": "publisher", "display": username };
        if(pin){
            roomPin = pin;
            register.pin = roomPin;
        }
        videoroomHandle.send({"message": register});
    };

    /**
     * Gets the number of peers/users connected into the current Videoroom
     * @return {integer} Number of peers/users connected into the current Videoroom
     */
    var getPeers = function() {
        return nPeers;
    };

    /**
     * Toggle Audio stream (Mute/Unmute)
     * @return {boolean} Is audio muted?
     * @example
     * action.toggleAudio(); // true or false
     */
    var toggleAudio = function() {
        if (videoroomHandle.isAudioMuted()) videoroomHandle.unmuteAudio();
        else videoroomHandle.muteAudio();
        return videoroomHandle.isAudioMuted();
    };

    /**
     * Toggle Video stream (Mute/Unmute)
     * @return {boolean} Is video muted?
     * @example
     * action.toggleVideo(); // true or false
     */
    var toggleVideo = function() {
        if (videoroomHandle.isVideoMuted()) videoroomHandle.unmuteVideo();
        else videoroomHandle.muteVideo();
        return videoroomHandle.isVideoMuted();
    };

    /**
     * New Remote Feed
     * @param  {integer} id      Id
     * @param  {string} display  Name
     * @return {nothing}
     * @private
     */
    var newRemoteFeed = function(id, display) {
        // A new feed has been published, create a new plugin handle and attach to it as a listener
        var remoteFeed = null;
        window.VideoRTC.connection.handle.attach(
    		{
    			plugin: "janus.plugin.videoroom",
    			success: function(pluginHandle) {
    				remoteFeed = pluginHandle;
    				// We wait for the plugin to send us an offer
    				var listen = { "request": "join", "room": idVideoroom || 1234, "ptype": "listener", "feed": id };
                    if(roomPin) listen.pin = roomPin;
    				remoteFeed.send({"message": listen});
    			},
    			error: function(error) {
    				Janus.error("  -- Error attaching plugin...", error);
    			},
    			onmessage: function(msg, jsep) {
    				Janus.debug(" ::: Got a message (listener) :::");
    				var event = msg["videoroom"];
    				Janus.debug("Event: " + event);
    				if(event != undefined && event != null) {
    					if(event === "attached") {
    						// Subscriber created and attached
    						for(var i=1;i<6;i++) {
    							if(feeds[i] === undefined || feeds[i] === null) {
    								feeds[i] = remoteFeed;
    								remoteFeed.rfindex = i;
    								break;
    							}
    						}
    						remoteFeed.rfid = msg["id"];
    						remoteFeed.rfdisplay = msg["display"];
    						//Janus.log("Successfully attached to feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") in room " + msg["room"]);
    					} else if(msg["error"] !== undefined && msg["error"] !== null) {
    						Janus.error(msg["error"]);
    					} else {
    						// What has just happened?
    					}
    				}
    				if(jsep !== undefined && jsep !== null) {
    					Janus.debug("Handling SDP as well...");
    					Janus.debug(jsep);
    					// Answer and attach
    					remoteFeed.createAnswer(
    						{
    							jsep: jsep,
    							media: { audioSend: false, videoSend: false },	// We want recvonly audio/video
    							success: function(jsep) {
    								Janus.debug("Got SDP!");
    								Janus.debug(jsep);
    								var body = { "request": "start", "room": idVideoroom };
    								remoteFeed.send({"message": body, "jsep": jsep});
    							},
    							error: function(error) {
    								Janus.error("WebRTC error:", error);
    							}
    						});
    				}
    			},
    			onlocalstream: function(stream) {
    				// The subscriber stream is recvonly, we don't expect anything here
    			},
    			onremotestream: function(stream) {
                    Janus.debug("Remote feed #" + remoteFeed.rfindex);
                    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'+remoteFeed.rfindex);
                    var tpl = '';
                    if(!target || target.innerHTML.length === 0) {
                        tpl += '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="videoremote'+remoteFeed.rfindex+'"></video>';
                        tpl += generateLabel('remote-name'+remoteFeed.rfindex, remoteFeed.rfdisplay, '#55D8D5', 'left:0; margin-top: 10px;');
                        tpl += generateLabel('remote-resolution'+remoteFeed.rfindex, '', '#8DCC6D', 'left:0; margin-top: 40px;');
                        tpl += generateLabel('remote-bitrate'+remoteFeed.rfindex, '', '#FF6E5F', 'left:0; margin-top: 70px;');
                        var newNode = document.createElement('div');
                        newNode.id = 'container-remote'+remoteFeed.rfindex;
                        newNode.className = 'videortc-video-container';
                        newNode.innerHTML = tpl;
                        container.appendChild(newNode);

                        var videoElem = document.getElementById('videoremote'+remoteFeed.rfindex);
                        videoElem.onplaying = function() {
                            var width = this.videoWidth;
                            var height = this.videoHeight;
                            document.getElementById('remote-resolution'+remoteFeed.rfindex).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'+remoteFeed.rfindex).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);
                            if(streamOptions.voiceProcessor) {
                                nodeNoCamera.innerHTML = '';
                                var nodeVolume = document.createElement('span');
                                nodeVolume.className = "volume-level-processor";
                                nodeNoCamera.insertBefore(nodeVolume, nodeNoCamera.firstChild);
                                setVoiceProcessor(stream, nodeVolume);
                            }
                            // 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[remoteFeed.rfindex] = setInterval(function() {
                            // Display updated bitrate, if supported
                            var bitrate = remoteFeed.getBitrate();
                            document.getElementById('remote-bitrate'+remoteFeed.rfindex).innerHTML = bitrate;
                        }, 1000);

                    }

                    nPeers++;
                    //if(onEvents && onEvents.onAcceptedVideo) onEvents.onAcceptedVideo();

    			},
    			oncleanup: function() {
    				Janus.log(" ::: Got a cleanup notification (remote feed " + id + ") :::");
    				//$('#waitingvideo'+remoteFeed.rfindex).remove();
    				//$('#curbitrate'+remoteFeed.rfindex).remove();
    				//$('#curres'+remoteFeed.rfindex).remove();
    				if(bitrateTimer[remoteFeed.rfindex] !== null && bitrateTimer[remoteFeed.rfindex] !== null)
    					clearInterval(bitrateTimer[remoteFeed.rfindex]);
    				bitrateTimer[remoteFeed.rfindex] = null;
    			}
    		});

    };

    /**
     * Publish Own Stream
     * @return {nothing}
     * @private
     */
    var publishOwnStream = function() {
        videoroomHandle.createOffer(
    		{
    			media: {
                    audioRecv: false,
                    audioSend: streamOptions.audioEnabled,
                    videoRecv: false,
                    videoSend: streamOptions.videoEnabled,
                    data: false,
                    audio: {
    					deviceId: {
    						exact: streamOptions.aDeviceId || undefined
    					}
    				},
    				video: {
    					deviceId: {
    						exact: streamOptions.vDeviceId || undefined
    					}
    				}
                }, // Publishers are sendonly
    			success: function(jsep) {
    				var publish = { "request": "configure", "audio": true, "video": true };
    				videoroomHandle.send({"message": publish, "jsep": jsep});
    			},
    			error: function(error) {
                    Janus.error("WebRTC error:", error);
                    publishOwnStream();
    			}
    		}
        );
    };

    var setVoiceProcessor = function(stream, nodeNoCamera) {
        var audioCtx = new AudioContext();
        var source = audioCtx.createMediaStreamSource(stream);
        // We create an analyzer
        var analyser = audioCtx.createAnalyser();
        analyser.smoothingTimeConstant = 0.3;
        analyser.fftSize = 1024;
        // We create an audio processor
        var processor = audioCtx.createScriptProcessor(2048, 1, 1);
        processor.onaudioprocess = function(audio) {
            var array = new Uint8Array(analyser.frequencyBinCount);
            analyser.getByteFrequencyData(array);
            // Average
            var prom = 0;
            for (var i = 0; i < array.length; i++) {
                prom += array[i];
            }
            prom = prom / array.length;
            var ratio = 0.7;
            prom = ratio*prom;
            // We paint the border box
            nodeNoCamera.style.border = "solid " + prom + "px";
            nodeNoCamera.style["border-radius"] = (15 + prom) + "px";
        }
        source.connect(analyser);
        analyser.connect(processor);
        processor.connect(audioCtx.destination);
    };

    return new Promise(function (resolve, reject) {

        window.VideoRTC.connection.handle.attach({
            plugin: "janus.plugin.videoroom",
            success: function(pluginHandle) {
                // Plugin attached! 'pluginHandle' is our handle
                videoroomHandle = pluginHandle;
                resolve({
                    closeUsecase: closeUsecase,
                    createVideoroom: createVideoroom,
                    destroyVideoroom: destroyVideoroom,
                    getPeers: getPeers,
                    joinVideoroom: joinVideoroom,
                    toggleAudio: toggleAudio,
                    toggleVideo: toggleVideo
                });
            },
            error: function(cause) {
                // Couldn't attach to the plugin
                Janus.error("  -- Error attaching plugin...", error);
                reject(cause);
            },
            consentDialog: function(on) {
                // e.g., Darken the screen if on=true (getUserMedia incoming), restore it otherwise
            },
            onmessage: function(msg, jsep) {
                // We got a message/event (msg) from the plugin
                // If jsep is not null, this involves a WebRTC negotiation
                var error = msg["error"];
                var code = msg["error_code"];
                if(error != null && error != undefined) {
                    Janus.error(error);
                    if(onEvents && onEvents.onError) onEvents.onError(error, code);
                    return;
                }
                var event = msg["videoroom"];
                if(event !== undefined && event !== null) {
                    if(event === "joined") {
                        // Publisher/manager created, negotiate WebRTC and attach to existing feeds, if any
                        publishOwnStream();
                        // Any new feed to attach to?
                        if(msg["publishers"] !== undefined && msg["publishers"] !== null) {
                            var list = msg["publishers"];
                            Janus.debug("Got a list of available publishers/feeds:");
                            Janus.debug(list);
                            for(var f in list) {
                                var id = list[f]["id"];
                                var display = list[f]["display"];
                                Janus.debug("  >> [" + id + "] " + display);
                                newRemoteFeed(id, display)
                            }
                        }
                        if(onEvents && onEvents.onJoined) onEvents.onJoined();
                    } else if(event === "destroyed") {
                        // The room has been destroyed
                        Janus.warn("The room has been destroyed!");
                        if(onEvents && onEvents.onDestroyed) onEvents.onDestroyed();
                    } else if(event === "event") {
                        // Any new feed to attach to?
                        if(msg["publishers"] !== undefined && msg["publishers"] !== null) {
                            var list = msg["publishers"];
                            Janus.debug("Got a list of available publishers/feeds:");
                            Janus.debug(list);
                            for(var f in list) {
                                var id = list[f]["id"];
                                var display = list[f]["display"];
                                Janus.debug("  >> [" + id + "] " + display);
                                newRemoteFeed(id, display)
                            }
                            if(onEvents && onEvents.onPeerJoined) onEvents.onPeerJoined(id, display);
                        } else if(msg["leaving"] !== undefined && msg["leaving"] !== null) {
                            // One of the publishers has gone away?
                            var leaving = msg["leaving"];
                            Janus.log("Publisher left: " + leaving);
                            var remoteFeed = null;
                            for(var i=1; i<6; i++) {
                                if(feeds[i] != null && feeds[i] != undefined && feeds[i].rfid == leaving) {
                                    remoteFeed = feeds[i];
                                    break;
                                }
                            }
                            if(remoteFeed != null) {
                                nPeers--;
                                Janus.debug("Feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") has left the room, detaching");
                                if(onEvents && onEvents.onPeerLeft) onEvents.onPeerLeft(remoteFeed.rfid, remoteFeed.rfdisplay);
                                document.getElementById('container-remote'+remoteFeed.rfindex).remove();
                                feeds[remoteFeed.rfindex] = null;
                                remoteFeed.detach();
                            }
                        } else if(msg["unpublished"] !== undefined && msg["unpublished"] !== null) {
                            // One of the publishers has unpublished?
                            var unpublished = msg["unpublished"];
                            Janus.log("Publisher left: " + unpublished);
                            if(unpublished === 'ok') {
                                // That's us
                                videoroomHandle.hangup();
                                return;
                            }
                            var remoteFeed = null;
                            for(var i=1; i<6; i++) {
                                if(feeds[i] != null && feeds[i] != undefined && feeds[i].rfid == unpublished) {
                                    remoteFeed = feeds[i];
                                    break;
                                }
                            }
                            if(remoteFeed != null) {
                                nPeers--;
                                Janus.debug("Feed " + remoteFeed.rfid + " (" + remoteFeed.rfdisplay + ") has left the room, detaching");
                                if(onEvents && onEvents.onPeerLeft) onEvents.onPeerLeft(remoteFeed.rfid, remoteFeed.rfdisplay);
                                document.getElementById('container-remote'+remoteFeed.rfindex).remove();
                                feeds[remoteFeed.rfindex] = null;
                                remoteFeed.detach();
                            }
                        } else if(msg["error"] !== undefined && msg["error"] !== null) {
                            if(onEvents && onEvents.onError) onEvents.onError(msg["error"], msg["error_code"]);
                        }
                    }
                }
                if(jsep !== undefined && jsep !== null) {
                    Janus.debug("Handling SDP as well...");
                    Janus.debug(jsep);
                    videoroomHandle.handleRemoteJsep({jsep: jsep});
                }

            },
            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);
                    if(streamOptions.voiceProcessor) {
                        nodeNoCamera.innerHTML = '';
                        var nodeVolume = document.createElement('span');
                        nodeVolume.className = "volume-level-processor";
                        nodeNoCamera.insertBefore(nodeVolume, nodeNoCamera.firstChild);
                        setVoiceProcessor(stream, nodeVolume);
                    }
                }
            },
            onremotestream: function(stream) {
                // The publisher stream is sendonly, we don't expect anything here
                Janus.debug(" ::: Got a remote stream :::");
            },
            oncleanup: function() {
                // PeerConnection with the plugin closed, clean the UI
                // The plugin handle is still valid so we can create a new one
            },
            detached: function() {
                // Connection with the plugin closed, get rid of its features
                // The plugin handle is not valid anymore
            }
        });

    });
};

exports.videoRoom = videoRoom;