var Janus = require('../externals/janus.nojquery'); var Promise = require('bluebird/js/release/bluebird'); var DataChannel = require('../modules/datachannel'); /** * A SplitAgent Usecase is started * @class * @classdesc SplitAgent Use Case * @param {object} onEvents Event handlers: onDestroyed, onError, onJoined, onDataReceived, onAcceptedVideo * @param {object} domElements DOM elements: client, agent, screenAgent * @param {object} options Available options: dataChannel, session, stream * @return {Promise} SplitAgent methods: getLicense, sendData, startRecording, startScreensharing, stopRecording, stopScreensharing, toggleVideo * @example * var onEvents = { * onDestroyed: function() { * // Destroyed * }, * onError: function(cause) { * // Error * }, * onJoined: function(screenRoom) { * // Joined * }, * onDataReceived: function(type, data, filename) { * // Data received * if(type === 'application/x-chat') { } * if(type === 'text/plain') { } * if(type === 'application/pdf') { } * if(type === 'application/zip') { } * if(type === 'application/x-rar') { } * if(type === 'image/jpeg') {} * if(type === 'image/png') {} * if(type === 'application/x-docx') {} * if(type === 'application/x-pptx') {} * if(type === 'application/x-xlsx') {} * if(type === 'application/vnd.oasis.opendocument.text') {} * }, * onAcceptedVideo: function() { * // Accepted Video * } * }; * * var domElements = { * client: document.getElementById('clientvideo'), * agent: document.getElementById('agentvideo'), * screenAgent: document.getElementById('screen') * }; * * var options = { * dataChannel: { * dataEnabled: true, * allowedTypes: ['application/x-chat', 'image/jpeg', 'image/png', 'application/pdf'], * maxSize: 5 // In MB * }, * session: { * agentName: 'Anna', * sessionId: 6655 * }, * stream: { * aDeviceId: null, * vDeviceId: null * } * }; * * usecases.splitAgent(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 splitAgent = function(onEvents, domElements, options) { if(!domElements || !domElements.agent || !domElements.client || !domElements.screenAgent || domElements.agent.nodeType !== 1 || domElements.client.nodeType !== 1 || domElements.screenAgent.nodeType !== 1) return null; // Stream options var streamOptions = (options && options.stream) ? options.stream : {}; if(!streamOptions.aDeviceId) streamOptions.aDeviceId = undefined; if(!streamOptions.vDeviceId) streamOptions.vDeviceId = undefined; // Session options var sessionOptions = (options && options.session) ? options.session : {}; if(!sessionOptions.agentName) sessionOptions.agentName = 'Unknown'; if(!sessionOptions.sessionId) sessionOptions.sessionId = 1234; // DataChannel options var optionsDataChannel = (options && options.dataChannel) ? options.dataChannel : {}; var result = DataChannel.initialize(optionsDataChannel); if(!result) console.log("Datachannel options can't be loaded:"); // Vars var splitAgentHandle = null; var screenHandle = null; var agentName = sessionOptions.agentName; var sessionId = sessionOptions.sessionId; var screenid = null; var feeds = []; var bitrateTimer = []; var datainfo = []; var prefixName = 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]; obj.innerHTML = ''; } } }; /** * The Agent can connect to establish a Video session * @param {integer} id Session ID * @param {string} display Display name * @return {nothing} * @private */ var joinVideoSession = function(id, display) { var register = { "request": "join", "room": id || 1234, "ptype": "publisher", "display": display || 'Agent' }; splitAgentHandle.send({"message": register}); }; /** * The Agent sends a request to start recording the session * @param {string} recordPrefix Prefix of records: e.g: '__default__' * @return {nothing} * @example * action.startRecording(); */ var startRecording = function(recordPrefix) { prefixName = recordPrefix; var req = { "request": "startRec", "room": sessionId } splitAgentHandle.send({"message": req}); // ScreenSharing if(screenHandle !== null) { var req = { "request": "startRec", "room": screenid } splitAgentHandle.send({"message": req}); } }; /** * The Agent sends a request to stop recording the session * @return {nothing} * @example * action.stopRecording(); */ var stopRecording = function() { var req = { "request": "stopRec", "room": sessionId } splitAgentHandle.send({"message": req}); // ScreenSharing if(screenHandle !== null) { var req = { "request": "stopRec", "room": screenid } splitAgentHandle.send({"message": req}); } }; /** * Start recording (Slave) * @param {integer} id Session ID * @param {string} recordPrefix Prefix of records: e.g: '__default__' * @return {nothing} * @private */ var recordVideoroom = function(id, recordPrefix, recordId) { var req = { "request": "configure", "audio": true, "room": id, "record": true, "prefixtheme": recordPrefix, "record_id": recordId } splitAgentHandle.send({"message": req}); }; /** * Start recording (Slave) (ScreenRoom) * @param {integer} id Screen ID * @param {string} recordPrefix Prefix of records: e.g: '__ScreenSharingdefault__' * @return {nothing} * @private */ var recordScreenroom = function(id, recordPrefix, recordId) { var req = { "request": "configure", "audio": true, "room": id, "record": true, "prefixtheme": recordPrefix + 'ScreenSharing', // Hidden on VideoPlayer "record_id": recordId } screenHandle.send({"message": req}); }; /** * Stop recording (Slave) * @param {integer} id Session ID * @return {nothing} * @private */ var stoprecordVideoroom = function(id) { var req = { "request": "configure", "room": id, "record": false } splitAgentHandle.send({"message": req}); }; /** * Stop recording (Slave) * @param {integer} id Session ID * @return {nothing} * @private */ var stoprecordScreenroom = function(id) { var req = { "request": "configure", "room": id, "record": false } screenHandle.send({"message": req}); }; /** * The VideoGateway is requested about the features of the contracted license * @return {Object} License Information (Screensharing, Livechat and VideoRecording ) * @example * var myLicense = action.getLicense(); * console.log(myLicense); */ var getLicense = function() { return window.VideoRTC.license; }; /** * Toggle local Video stream (Mute/Unmute) * @return {boolean} Is video muted? * @example * action.toggleVideo(); // true or false */ var toggleVideo = function() { if (splitAgentHandle.isVideoMuted()) splitAgentHandle.unmuteVideo(); else splitAgentHandle.muteVideo(); return splitAgentHandle.isVideoMuted(); }; /** * 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, splitAgentHandle); }; /** * New remote Feed * @param {integer} id Session ID * @param {string} display Display name * @return {nothing} * @private */ var newRemoteFeed = function(id, display) { var remoteFeed = null; window.VideoRTC.connection.handle.attach({ plugin: "janus.plugin.split", success: function(pluginHandle) { remoteFeed = pluginHandle; var listen = { "request": "join", "room": sessionId, "ptype": "listener", "feed": id }; remoteFeed.send({"message": listen}); }, error: function(cause) { console.log(" -- Error attaching plugin...", error); }, destroyed: function() { }, consentDialog: function(on) { }, onmessage: function(msg, jsep) { Janus.debug(" ::: Got a message (listener) :::"); var event = msg["split"]; 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) { console.log(msg["error"]); } else { // What has just happened? } } if (jsep !== undefined && jsep !== null) { // Answer and attach remoteFeed.createAnswer( { jsep: jsep, media: {audioSend: false, videoSend: false, data: true}, // We want recvonly audio/video success: function (jsep) { var body = {"request": "start", "room": sessionId}; remoteFeed.send({"message": body, "jsep": jsep}); }, error: function (error) { Janus.error("WebRTC error:", error); } }); } }, onlocalstream: function(stream) { }, 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.client; var tpl = ''; tpl += '<div id="container-remote1">'; tpl += '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="videoremote1"></video>'; tpl += generateLabel('remote-name', remoteFeed.rfdisplay, '#55D8D5', 'left:0; margin-top: 10px;'); tpl += generateLabel('remote-resolution', '', '#8DCC6D', 'left:0; margin-top: 40px;'); tpl += generateLabel('remote-bitrate', '', '#FF6E5F', 'left:0; margin-top: 70px;'); tpl += '</div>'; container.innerHTML += tpl; var remoteContainer = document.getElementById('container-remote1'); var videoElem = document.getElementById('videoremote1'); videoElem.onplaying = function() { var width = this.videoWidth; var height = this.videoHeight; document.getElementById('remote-resolution').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').innerHTML = width+'x'+height; }, 2000); } }; Janus.attachMediaStream(videoElem, stream); videoElem.muted = true; 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"; remoteContainer.insertBefore(nodeNoCamera, remoteContainer.firstChild); // We Hide Bitrate and Resolution labels for(var i = 0; i < remoteContainer.childNodes.length; i++) { if(remoteContainer.childNodes[i].id && (remoteContainer.childNodes[i].id.indexOf('remote-resolution') >= 0 || remoteContainer.childNodes[i].id.indexOf('remote-bitrate') >= 0 )) { remoteContainer.childNodes[i].style.display = 'none'; } } } bitrateTimer[remoteFeed.rfindex] = setInterval(function() { // Display updated bitrate, if supported var bitrate = remoteFeed.getBitrate(); document.getElementById('remote-bitrate').innerHTML = bitrate; }, 1000); 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; }, detached: function() { }, ondataopen: function (data) { Janus.log("The DataChannel is available!"); }, ondata: function (data) { Janus.debug("We got data from the DataChannel! " + data); DataChannel.receive(data, onEvents.onDataReceived); } }); }; /** * Publish own stream * @return {nothing} * @private */ var publishOwnFeed = function() { var mediaOffer = { audioRecv: false, videoRecv: false, audioSend: true, videoSend: true, data: true, audio: { deviceId: { exact: streamOptions.aDeviceId || undefined } }, video: { deviceId: { exact: streamOptions.vDeviceId || undefined } } }; splitAgentHandle.createOffer( { media: mediaOffer, // Publishers are sendonly success: function (jsep) { var publish = {"request": "configure", "audio": false, "video": true, "record": false}; // useAudio splitAgentHandle.send({"message": publish, "jsep": jsep}); }, error: function (error) { console.log("WebRTC error:", error); publishOwnFeed(); } }); }; /** * The Agent sends a request to start Screensharing * @param {integer} screenId Screen Id * @param {function} cOk Callback Ok * @param {function} cKo Callback Error * @return {nothing} * @example * action.startScreensharing(1234, function() { * // Success * }, function(cause) { * // Error * }) */ var startScreensharing = function(screenId, cOk, cKo) { if(!getLicense().screensharing) { cKo ('Screensharing is not a feature of your Gateway'); return false; } 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; } screenid = screenId; window.VideoRTC.connection.handle.attach({ plugin: "janus.plugin.split", success: function(pluginHandle) { // Plugin attached! 'pluginHandle' is our handle screenHandle = pluginHandle; var register = { "request": "join", "room": screenId || 1234, "ptype": "publisher", "display": 'Screen' }; screenHandle.send({"message": register}); }, error: function(cause) { }, destroyed: function() { }, consentDialog: function(on) { }, onmessage: function(msg, jsep) { var event = msg["split"]; if (event != undefined && event != null) { if (event === "joined") { var capture = 'screen'; screenHandle.createOffer( { //media: { audioRecv: false, videoRecv: false, audioSend: useAudio, videoSend: true/*, video:"hires"*/}, // Publishers are sendonly media: { video: capture, audio: false, videoRecv: false }, success: function(jsep) { var publish = { "request": "configure", "audio": false, "video": true }; screenHandle.send({"message": publish, "jsep": jsep}); }, 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); } } ); // Any new feed to attach to? /*if (msg["publishers"] !== undefined && msg["publishers"] !== null) { var list = msg["publishers"]; for (var f in list) { var id = list[f]["id"]; var display = list[f]["display"]; newRemoteFeed(id, display); } }*/ } } if(jsep !== undefined && jsep !== null) { Janus.debug("Handling SDP as well..."); Janus.debug(jsep); screenHandle.handleRemoteJsep({jsep: jsep}); } }, onlocalstream: function(stream) { // We have a local stream (getUserMedia worked!) to display var container = domElements.screenAgent; container.innerHTML = '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="screenAgent"></video>'; var videoElem = document.getElementById('screenAgent'); Janus.attachMediaStream(videoElem, stream); videoElem.muted = true; cOk(); }, onremotestream: function(stream) { }, oncleanup: function() { }, detached: function() { } }); }; /** * The Agent sends a request to stop Screensharing * @param {function} cOk Callback Ok * @param {function} cKo Callback Error * @return {nothing} * @example * action.stopScreensharing(function() { * // Success * }, function(cause) { * // Error * }) */ var stopScreensharing = function(cOk, cKo) { screenHandle.detach(); var container = domElements.screenAgent; container.innerHTML = ''; container.style = ''; cOk(); }; return new Promise(function (resolve, reject) { window.VideoRTC.connection.handle.attach({ plugin: "janus.plugin.split", success: function(pluginHandle) { // Plugin attached! 'pluginHandle' is our handle splitAgentHandle = pluginHandle; joinVideoSession(sessionId, agentName); splitAgentHandle.send({ message: { request: "license_info" }, "success": function(data) { if(data && data["error_code"]){ reject(data["error"]); } else { resolve({ closeUsecase: closeUsecase, getLicense: getLicense, startRecording: startRecording, stopRecording: stopRecording, startScreensharing: startScreensharing, stopScreensharing: stopScreensharing, toggleVideo: toggleVideo, sendData: sendData }); } } }); }, error: function(cause) { if(cause && cause.indexOf('482') >= 0) { reject("Sorry, all channels are busy!"); } else if(cause && cause.indexOf('480') >= 0) { reject("License has been expired. Please, contact with an Administrator"); } else { reject(cause); } }, destroyed: function() { }, consentDialog: function(on) { }, onmessage: function(msg, jsep) { var event = msg["split"]; if (event != undefined && event != null) { if (event === "joined") { // Publisher/manager created, negotiate WebRTC and attach to existing feeds, if any publishOwnFeed(true); // TODO: Implementar // Any new feed to attach to? if (msg["publishers"] !== undefined && msg["publishers"] !== null) { var list = msg["publishers"]; for (var f in list) { var id = list[f]["id"]; var display = list[f]["display"]; newRemoteFeed(id, display); } } var screenRoom = msg["screenroom"] > 0 ? msg["screenroom"] : undefined; if(onEvents && onEvents.onJoined) onEvents.onJoined(screenRoom); } else if (event === "destroyed") { // 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"]; for (var f in list) { var id = list[f]["id"]; var display = list[f]["display"]; newRemoteFeed(id, display); } } else if (msg["leaving"] !== undefined && msg["leaving"] !== null) { // One of the publishers has gone away? var leaving = msg["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) { // REVIEW: Clean container? 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 splitAgentHandle.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) { // TODO: Clean container? feeds[remoteFeed.rfindex] = null; remoteFeed.detach(); } } else if(msg["startedRec"] !== undefined && msg["startedRec"] !== null) { recordVideoroom(sessionId, prefixName, msg["recordId"]); if(screenHandle !== null) recordScreenroom(screenid, prefixName, msg["recordId"]); } else if(msg["stoppedRec"] !== undefined && msg["stoppedRec"] !== null) { stoprecordVideoroom(sessionId); if(screenHandle !== null) stoprecordScreenroom(screenid); } else if (msg["error"] !== undefined && msg["error"] !== null) { if(onEvents && onEvents.onError) onEvents.onError(msg["error"]); } else if (msg['result'] !== undefined && msg['result'] !== null) { var result = msg['result']; if (result['event'] == 'license_info') { window.VideoRTC.license = { screensharing: (result['screensharing'] == 'true'), livechat: (result['livechat'] == 'true'), videorecording: (result['videorecording'] == 'true') }; console.log("License", window.VideoRTC.license); } } } } if (jsep !== undefined && jsep !== null) { splitAgentHandle.handleRemoteJsep({jsep: jsep}); } }, onlocalstream: function(stream) { // We have a local stream (getUserMedia worked!) to display var container = domElements.agent; container.innerHTML = '<video autoplay poster="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" id="videoAgent"></video>'; var videoElem = document.getElementById('videoAgent'); 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"; container.insertBefore(nodeNoCamera, container.firstChild); } }, onremotestream: function(stream) { }, oncleanup: function() { }, detached: function() { } }); }); }; exports.splitAgent = splitAgent;