var Janus = require('../externals/janus.nojquery'); var Promise = require('bluebird/js/release/bluebird'); var DataChannel = require('../modules/datachannel'); /** * 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 screenRoomHandle = null; var feeds = []; var bitrateTimer = []; var idVideoroom; var nPeers = 1; var roomPin = null; var username = 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) { 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, data: true }, // 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) { // 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 + ") :::"); if(bitrateTimer[remoteFeed.rfindex] !== null && bitrateTimer[remoteFeed.rfindex] !== null) clearInterval(bitrateTimer[remoteFeed.rfindex]); bitrateTimer[remoteFeed.rfindex] = null; }, ondataopen: function () { Janus.log("The DataChannel is available!"); if(onEvents && onEvents.onAcceptedData) onEvents.onAcceptedData(); }, ondata: function (data) { Janus.debug("We got data from the DataChannel! " + data); DataChannel.receive(data, onEvents.onDataReceived); } }); }; /** * Publish Own Stream * @return {nothing} * @private */ var publishOwnStream = function() { videoroomHandle.createOffer( { media: { audioRecv: false, audioSend: streamOptions.audioEnabled, videoRecv: false, videoSend: streamOptions.videoEnabled, data: true, 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, "data": 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); }; var joinScreenroom = function(screenRoom) { window.VideoRTC.connection.handle.attach({ plugin: "janus.plugin.videoroom", success: function(pluginHandle) { console.log(pluginHandle); // Plugin attached! 'pluginHandle' is our handle screenRoomHandle = pluginHandle; console.log(screenRoomHandle); var register = { "request": "join", "room": screenRoom, "ptype": "publisher", "display": username + '-screen' || 'ScreenClient' }; if(roomPin) register.pin = roomPin; screenRoomHandle.send({"message": register}); }, error: function(cause) { console.log("Error: " + cause); }, consentDialog: function(on) { }, onmessage: function(msg, jsep) { var event = msg.videoroom; if(event !== undefined && event !== null) { if(event === "joined") { 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); //listenScreenSharing(screenRoom, id); } } } else if(event === "destroyed") { Janus.warn("The room has been destroyed!"); } else if(event === "event") { 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); //listenScreenSharing(screenRoom, id); } } else if(msg["leaving"] !== undefined && msg["leaving"] !== null) { var leaving = msg["leaving"]; Janus.log("Publisher left: " + leaving); var container = domElements.screenRemote; container.innerHTML = ''; container.style = ''; //container.className = ''; screenFeed.detach(); if(onEvents && onEvents.onScreensharingClosed) onEvents.onScreensharingClosed(); } else if(msg["unpublished"] !== undefined && msg["unpublished"] !== null) { var unpublished = msg["unpublished"]; Janus.log("Publisher left: " + unpublished); var container = domElements.screenRemote; container.innerHTML = ''; container.style = ''; //container.className = ''; screenFeed.detach(); } else if(msg["error"] !== undefined && msg["error"] !== null) { Janus.error(msg["error"]); } } } if(jsep !== undefined && jsep !== null) { Janus.debug("Handling SDP as well..."); Janus.debug(jsep); screenRoomHandle.handleRemoteJsep({jsep: jsep}); } }, onlocalstream: function(stream) { // We have a local stream (getUserMedia worked!) to display //var container = domElements.screenLocal; //container.innerHTML = '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="screenClient"></video>'; //var videoElem = document.getElementById('screenClient'); //Janus.attachMediaStream(videoElem, stream); //videoElem.muted = true; }, onremotestream: function(stream) { }, oncleanup: function() { }, detached: function() { } }); }; var startScreensharing = function(screenId, cOk, cKo) { if(window.location.protocol !== 'https:') { cKo ('Screensharing requires HTTPS'); return false; } if(window.isMobile) { cKo ('Screensharing is not available on Mobile devices'); return false; } if(!Janus.isExtensionEnabled()) { cKo("You're using a recent version of Chrome but don't have the screensharing extension installed: click <b><a href='https://chrome.google.com/webstore/detail/videortc-screensharing/pkilckpboojemoogepfpkgbihkfkikel' target='_blank'>here</a></b> to do so"); return false; } var capture = 'screen'; console.log(screenRoomHandle); screenRoomHandle.createOffer( { //media: { audioRecv: false, videoRecv: false, audioSend: useAudio, videoSend: true}, // Publishers are sendonly media: { video: capture, audio: false, videoRecv: false }, success: function(jsep) { var publish = { "request": "configure", "audio": false, "video": true, "data": false }; screenRoomHandle.send({"message": publish, "jsep": jsep}); cOk(); }, error: function(error) { console.log(error); if(error && error.name === 'NotAllowedError' && window.getBrowser() === 'firefox') { cKo('Firefox needs that the domain this web application is from is listed in Allowed domains.'); } Janus.error("WebRTC error:", error); } } ); }; var stopScreensharing = function(cOk, cKo) { var leave = { "request": "leave" }; screenRoomHandle.send({"message": leave}); screenRoomHandle.detach(); joinScreenroom(idVideoroom); //var container = domElements.screenLocal; //container.innerHTML = ''; //container.style = ''; cOk(); }; var sendData = function(type, data, cOk, cKo, filename) { DataChannel.send(type, data, cOk, cKo, filename); }; 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; var cbks = { fileOk: onEvents.onFileTransferOk || function() {}, fileKo: onEvents.onFileTransferKo || function() {} }; console.log(cbks); var result = DataChannel.initialize({}, pluginHandle, cbks); if(!result) console.log("Datachannel options can't be loaded:"); resolve({ closeUsecase: closeUsecase, createVideoroom: createVideoroom, destroyVideoroom: destroyVideoroom, getPeers: getPeers, joinVideoroom: joinVideoroom, sendData: sendData, startScreensharing: startScreensharing, stopScreensharing: stopScreensharing, 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(); joinScreenroom(idVideoroom); } 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;