diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala index 5ae7d4d228c9d5151b778f4ede4633c40c91d228..7dbf35321bdd46b50cafdf6bea1347be55c79e59 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala @@ -7,11 +7,11 @@ case class UserBroadcastCamStartedEvtMsgBody(userId: String, stream: String) object UserBroadcastCamStartMsg { val NAME = "UserBroadcastCamStartMsg" } case class UserBroadcastCamStartMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStartMsgBody) extends StandardMsg -case class UserBroadcastCamStartMsgBody(stream: String) +case class UserBroadcastCamStartMsgBody(stream: String, isHtml5Client: Boolean = false) object UserBroadcastCamStopMsg { val NAME = "UserBroadcastCamStopMsg" } case class UserBroadcastCamStopMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStopMsgBody) extends StandardMsg -case class UserBroadcastCamStopMsgBody(stream: String) +case class UserBroadcastCamStopMsgBody(stream: String, isHtml5Client: Boolean = false) object UserBroadcastCamStoppedEvtMsg { val NAME = "UserBroadcastCamStoppedEvtMsg" } case class UserBroadcastCamStoppedEvtMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStoppedEvtMsgBody) extends BbbCoreMsg diff --git a/bigbluebutton-client/resources/config.xml.template b/bigbluebutton-client/resources/config.xml.template index 0ddab1bbc734a0b18aa95ab92ade5e2156ce0f73..18b860a48d0c1c5c31e4a6408dc91968e34d2b7f 100755 --- a/bigbluebutton-client/resources/config.xml.template +++ b/bigbluebutton-client/resources/config.xml.template @@ -49,6 +49,7 @@ uri="rtmp://HOST/screenshare" showButton="true" enablePause="true" + tryKurentoWebRTC="false" tryWebRTCFirst="false" chromeExtensionLink="" chromeExtensionKey="" diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html index 15a8c952d1c11d41806e92b88b59900173be631d..a799be30e65458465ffedbe1e3d610741d310b9b 100755 --- a/bigbluebutton-client/resources/prod/BigBlueButton.html +++ b/bigbluebutton-client/resources/prod/BigBlueButton.html @@ -142,8 +142,8 @@ <script src="lib/verto-min.js" language="javascript"></script> <script src="lib/verto_extension.js" language="javascript"></script> - <script src="lib/kurento-utils.min.js" language="javascript"></script> <script src="lib/kurento-extension.js" language="javascript"></script> + <script src="lib/kurento-utils.js" language="javascript"></script> <script src="lib/bbb_api_bridge.js?v=VERSION" language="javascript"></script> <script src="lib/sip.js?v=VERSION" language="javascript"></script> diff --git a/bigbluebutton-client/resources/prod/lib/kurento-extension.js b/bigbluebutton-client/resources/prod/lib/kurento-extension.js index cd9c5fab9026142ecae3133278762eabdf33fd9b..386e85339e92b6916bc595731d7d8387261633ad 100644 --- a/bigbluebutton-client/resources/prod/lib/kurento-extension.js +++ b/bigbluebutton-client/resources/prod/lib/kurento-extension.js @@ -1,6 +1,7 @@ var isFirefox = typeof window.InstallTrigger !== 'undefined'; var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; var isChrome = !!window.chrome && !isOpera; +var isSafari = navigator.userAgent.indexOf("Safari") >= 0 && !isChrome; var kurentoHandler = null; Kurento = function ( @@ -20,7 +21,7 @@ Kurento = function ( this.screenConstraints = {}; this.mediaCallback = null; - this.voiceBridge = voiceBridge; + this.voiceBridge = voiceBridge + '-SCREENSHARE'; this.internalMeetingId = internalMeetingId; this.vid_width = window.screen.width; @@ -43,6 +44,7 @@ Kurento = function ( if (chromeExtension != null) { this.chromeExtension = chromeExtension; + window.chromeExtension = chromeExtension; } if (onFail != null) { @@ -57,21 +59,52 @@ Kurento = function ( this.KurentoManager= function () { this.kurentoVideo = null; - this.kurentoScreenShare = null; + this.kurentoScreenshare = null; }; KurentoManager.prototype.exitScreenShare = function () { - if (this.kurentoScreenShare != null) { - if(kurentoHandler.pingInterval) { - clearInterval(kurentoHandler.pingInterval); + console.log(" [exitScreenShare] Exiting screensharing"); + if(typeof this.kurentoScreenshare !== 'undefined' && this.kurentoScreenshare) { + if(this.kurentoScreenshare.pingInterval) { + clearInterval(this.kurentoScreenshare.pingInterval); } - if(kurentoHandler.ws !== null) { - kurentoHandler.ws.onclose = function(){}; - kurentoHandler.ws.close(); + + if(this.kurentoScreenshare.ws !== null) { + this.kurentoScreenshare.ws.onclose = function(){}; + this.kurentoScreenshare.ws.close(); } - kurentoHandler.disposeScreenShare(); - this.kurentoScreenShare = null; - kurentoHandler = null; + + this.kurentoScreenshare.disposeScreenShare(); + this.kurentoScreenshare = null; + } + + if (this.kurentoScreenshare) { + this.kurentoScreenshare = null; + } + + if(typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) { + this.exitVideo(); + } +}; + +KurentoManager.prototype.exitVideo = function () { + console.log(" [exitScreenShare] Exiting screensharing viewing"); + if(typeof this.kurentoVideo !== 'undefined' && this.kurentoVideo) { + if(this.kurentoVideo.pingInterval) { + clearInterval(this.kurentoVideo.pingInterval); + } + + if(this.kurentoVideo.ws !== null) { + this.kurentoVideo.ws.onclose = function(){}; + this.kurentoVideo.ws.close(); + } + + this.kurentoVideo.disposeScreenShare(); + this.kurentoVideo = null; + } + + if (this.kurentoVideo) { + this.kurentoVideo = null; } }; @@ -79,24 +112,21 @@ KurentoManager.prototype.shareScreen = function (tag) { this.exitScreenShare(); var obj = Object.create(Kurento.prototype); Kurento.apply(obj, arguments); - this.kurentoScreenShare = obj; - kurentoHandler = obj; - this.kurentoScreenShare.setScreenShare(tag); + this.kurentoScreenshare = obj; + this.kurentoScreenshare.setScreenShare(tag); }; -// Still unused, part of the HTML5 implementation KurentoManager.prototype.joinWatchVideo = function (tag) { this.exitVideo(); var obj = Object.create(Kurento.prototype); Kurento.apply(obj, arguments); this.kurentoVideo = obj; - kurentoHandler = obj; this.kurentoVideo.setWatchVideo(tag); }; Kurento.prototype.setScreenShare = function (tag) { - this.mediaCallback = this.makeShare; + this.mediaCallback = this.makeShare.bind(this); this.create(tag); }; @@ -112,19 +142,19 @@ Kurento.prototype.init = function () { console.log("this browser supports websockets"); this.ws = new WebSocket(this.socketUrl); - this.ws.onmessage = this.onWSMessage; - this.ws.onclose = function (close) { + this.ws.onmessage = this.onWSMessage.bind(this); + this.ws.onclose = (close) => { kurentoManager.exitScreenShare(); self.onFail("Websocket connection closed"); }; - this.ws.onerror = function (error) { + this.ws.onerror = (error) => { kurentoManager.exitScreenShare(); self.onFail("Websocket connection error"); }; - this.ws.onopen = function() { - self.pingInterval = setInterval(self.ping, 3000); + this.ws.onopen = function () { + self.pingInterval = setInterval(self.ping.bind(self), 3000); self.mediaCallback(); - }; + }.bind(self); } else console.log("this browser does not support websockets"); @@ -135,13 +165,16 @@ Kurento.prototype.onWSMessage = function (message) { switch (parsedMessage.id) { case 'presenterResponse': - kurentoHandler.presenterResponse(parsedMessage); + this.presenterResponse(parsedMessage); + break; + case 'viewerResponse': + this.viewerResponse(parsedMessage); break; case 'stopSharing': kurentoManager.exitScreenShare(); break; case 'iceCandidate': - kurentoHandler.webRtcPeer.addIceCandidate(parsedMessage.candidate); + this.webRtcPeer.addIceCandidate(parsedMessage.candidate); break; case 'pong': break; @@ -159,18 +192,30 @@ Kurento.prototype.presenterResponse = function (message) { var errorMsg = message.message ? message.message : 'Unknow error'; console.warn('Call not accepted for the following reason: ' + errorMsg); kurentoManager.exitScreenShare(); - kurentoHandler.onFail(errorMessage); + this.onFail(errorMessage); } else { console.log("Presenter call was accepted with SDP => " + message.sdpAnswer); this.webRtcPeer.processAnswer(message.sdpAnswer); } } +Kurento.prototype.viewerResponse = function (message) { + if (message.response != 'accepted') { + var errorMsg = message.message ? message.message : 'Unknown error'; + console.warn('Call not accepted for the following reason: ' + errorMsg); + kurentoManager.exitScreenShare(); + this.onFail(errorMessage); + } else { + console.log("Viewer call was accepted with SDP => " + message.sdpAnswer); + this.webRtcPeer.processAnswer(message.sdpAnswer); + } +} + Kurento.prototype.serverResponse = function (message) { if (message.response != 'accepted') { var errorMsg = message.message ? message.message : 'Unknow error'; console.warn('Call not accepted for the following reason: ' + errorMsg); - kurentoHandler.dispose(); + kurentoManager.exitScreenShare(); } else { this.webRtcPeer.processAnswer(message.sdpAnswer); } @@ -178,89 +223,102 @@ Kurento.prototype.serverResponse = function (message) { Kurento.prototype.makeShare = function() { var self = this; - console.log("Kurento.prototype.makeShare " + JSON.stringify(this.webRtcPeer, null, 2)); if (!this.webRtcPeer) { - var options = { - onicecandidate : this.onIceCandidate + onicecandidate : self.onIceCandidate.bind(self) } - console.log("Peer options " + JSON.stringify(options, null, 2)); - - kurentoHandler.startScreenStreamFrom(); - + this.startScreenStreamFrom(); } } Kurento.prototype.onOfferPresenter = function (error, offerSdp) { + let self = this; if(error) { console.log("Kurento.prototype.onOfferPresenter Error " + error); - kurentoHandler.onFail(error); + this.onFail(error); return; } var message = { id : 'presenter', type: 'screenshare', - internalMeetingId: kurentoHandler.internalMeetingId, - voiceBridge: kurentoHandler.voiceBridge, - callerName : kurentoHandler.caller_id_name, + internalMeetingId: self.internalMeetingId, + voiceBridge: self.voiceBridge, + callerName : self.caller_id_name, sdpOffer : offerSdp, - vh: kurentoHandler.vid_height, - vw: kurentoHandler.vid_width + vh: self.vid_height, + vw: self.vid_width }; console.log("onOfferPresenter sending to screenshare server => " + JSON.stringify(message, null, 2)); - kurentoHandler.sendMessage(message); + this.sendMessage(message); } Kurento.prototype.startScreenStreamFrom = function () { - var screenInfo = null; - var _this = this; + var self = this; if (!!window.chrome) { - if (!_this.chromeExtension) { - _this.logError({ + if (!self.chromeExtension) { + self.logError({ status: 'failed', message: 'Missing Chrome Extension key', }); - _this.onFail(); + self.onFail(); return; } } // TODO it would be nice to check those constraints - _this.screenConstraints.video = {}; + if (typeof screenConstraints !== undefined) { + self.screenConstraints = {}; + } + self.screenConstraints.video = {}; + console.log(self); var options = { - //localVideo: this.renderTag, - onicecandidate : _this.onIceCandidate, - mediaConstraints : _this.screenConstraints, + localVideo: document.getElementById(this.renderTag), + onicecandidate : self.onIceCandidate.bind(self), + mediaConstraints : self.screenConstraints, sendSource : 'desktop' }; console.log(" Peer options => " + JSON.stringify(options, null, 2)); - _this.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function(error) { + self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly(options, function(error) { if(error) { console.log("WebRtcPeerSendonly constructor error " + JSON.stringify(error, null, 2)); - kurentoHandler.onFail(error); + self.onFail(error); return kurentoManager.exitScreenShare(); } - _this.webRtcPeer.generateOffer(_this.onOfferPresenter); + self.webRtcPeer.generateOffer(self.onOfferPresenter.bind(self)); console.log("Generated peer offer w/ options " + JSON.stringify(options)); }); } -Kurento.prototype.onIceCandidate = function(candidate) { +Kurento.prototype.onIceCandidate = function (candidate) { + let self = this; console.log('Local candidate' + JSON.stringify(candidate)); var message = { id : 'onIceCandidate', type: 'screenshare', - voiceBridge: kurentoHandler.voiceBridge, + voiceBridge: self.voiceBridge, candidate : candidate } - console.log("this object " + JSON.stringify(this, null, 2)); - kurentoHandler.sendMessage(message); + this.sendMessage(message); +} + +Kurento.prototype.onViewerIceCandidate = function (candidate) { + let self = this; + console.log('Viewer local candidate' + JSON.stringify(candidate)); + + var message = { + id : 'viewerIceCandidate', + type: 'screenshare', + voiceBridge: self.voiceBridge, + candidate : candidate, + callerName: self.caller_id_name + } + this.sendMessage(message); } Kurento.prototype.setWatchVideo = function (tag) { @@ -276,60 +334,61 @@ Kurento.prototype.viewer = function () { if (!this.webRtcPeer) { var options = { - remoteVideo: this.renderTag, - onicecandidate : onIceCandidate + remoteVideo: document.getElementById(this.renderTag), + onicecandidate : this.onViewerIceCandidate.bind(this) } - webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) { + self.webRtcPeer = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options, function(error) { if(error) { - return kurentoHandler.onFail(error); + return self.onFail(error); } - this.generateOffer(onOfferViewer); + this.generateOffer(self.onOfferViewer.bind(self)); }); } }; Kurento.prototype.onOfferViewer = function (error, offerSdp) { + let self = this; if(error) { console.log("Kurento.prototype.onOfferViewer Error " + error); - return kurentoHandler.onFail(); + return this.onFail(); } var message = { - id : 'viewer', - type: 'screenshare', - internalMeetingId: kurentoHandler.internalMeetingId, - voiceBridge: kurentoHandler.voiceBridge, - callerName : kurentoHandler.caller_id_name, + id : 'viewer', type: 'screenshare', + internalMeetingId: self.internalMeetingId, + voiceBridge: self.voiceBridge, + callerName : self.caller_id_name, sdpOffer : offerSdp }; console.log("onOfferViewer sending to screenshare server => " + JSON.stringify(message, null, 2)); - kurentoHandler.sendMessage(message); + this.sendMessage(message); }; Kurento.prototype.ping = function() { + let self = this; var message = { id : 'ping', type: 'screenshare', - internalMeetingId: kurentoHandler.internalMeetingId, - voiceBridge: kurentoHandler.voiceBridge, - callerName : kurentoHandler.caller_id_name, + internalMeetingId: self.internalMeetingId, + voiceBridge: self.voiceBridge, + callerName : self.caller_id_name, }; - kurentoHandler.sendMessage(message); + this.sendMessage(message); } Kurento.prototype.stop = function() { - if (this.webRtcPeer) { - var message = { - id : 'stop', - type : 'screenshare', - voiceBridge: kurentoHandler.voiceBridge - } - kurentoHandler.sendMessage(message); - kurentoHandler.disposeScreenShare(); - } + //if (this.webRtcPeer) { + // var message = { + // id : 'stop', + // type : 'screenshare', + // voiceBridge: kurentoHandler.voiceBridge + // } + // kurentoHandler.sendMessage(message); + // kurentoHandler.disposeScreenShare(); + //} } Kurento.prototype.dispose = function() { @@ -360,19 +419,6 @@ Kurento.prototype.logError = function (obj) { console.error(obj); }; -Kurento.prototype.getChromeScreenConstraints = function(callback, extensionId) { - chrome.runtime.sendMessage(extensionId, { - getStream: true, - sources: [ - "window", - "screen", - "tab" - ]}, - function(response) { - console.log(response); - callback(response); - }); -}; Kurento.normalizeCallback = function (callback) { if (typeof callback == 'function') { @@ -389,30 +435,42 @@ Kurento.normalizeCallback = function (callback) { // this function explains how to use above methods/objects window.getScreenConstraints = function(sendSource, callback) { - var _this = this; - var chromeMediaSourceId = sendSource; - if(isChrome) { - kurentoHandler.getChromeScreenConstraints (function (constraints) { + let chromeMediaSourceId = sendSource; + let screenConstraints = {video: {}}; - var sourceId = constraints.streamId; + if(isChrome) { + getChromeScreenConstraints ((constraints) => { + let sourceId = constraints.streamId; // this statement sets gets 'sourceId" and sets "chromeMediaSourceId" - kurentoHandler.screenConstraints.video.chromeMediaSource = { exact: [sendSource]}; - kurentoHandler.screenConstraints.video.chromeMediaSourceId= sourceId; - console.log("getScreenConstraints for Chrome returns => " +JSON.stringify(kurentoHandler.screenConstraints, null, 2)); + screenConstraints.video.chromeMediaSource = { exact: [sendSource]}; + screenConstraints.video.chromeMediaSourceId = sourceId; + console.log("getScreenConstraints for Chrome returns => "); + console.log(screenConstraints); // now invoking native getUserMedia API - callback(null, kurentoHandler.screenConstraints); + callback(null, screenConstraints); - }, kurentoHandler.chromeExtension); + }, chromeExtension); } else if (isFirefox) { - kurentoHandler.screenConstraints.video.mediaSource= "screen"; - kurentoHandler.screenConstraints.video.width= {max: kurentoHandler.vid_width}; - kurentoHandler.screenConstraints.video.height = {max: kurentoHandler.vid_height}; + screenConstraints.video.mediaSource= "window"; + screenConstraints.video.width= {max: "1280"}; + screenConstraints.video.height = {max: "720"}; - console.log("getScreenConstraints for Firefox returns => " +JSON.stringify(kurentoHandler.screenConstraints, null, 2)); + console.log("getScreenConstraints for Firefox returns => "); + console.log(screenConstraints); // now invoking native getUserMedia API - callback(null, kurentoHandler.screenConstraints); + callback(null, screenConstraints); + } + else if(isSafari) { + screenConstraints.video.mediaSource= "screen"; + screenConstraints.video.width= {max: window.screen.width}; + screenConstraints.video.height = {max: window.screen.vid_height}; + + console.log("getScreenConstraints for Safari returns => "); + console.log(screenConstraints); + // now invoking native getUserMedia API + callback(null, screenConstraints); } } @@ -437,3 +495,22 @@ window.kurentoWatchVideo = function () { window.kurentoInitialize(); window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments); }; + +window.kurentoExitVideo = function () { + window.kurentoInitialize(); + window.kurentoManager.exitVideo(); +} + +window.getChromeScreenConstraints = function(callback, extensionId) { + chrome.runtime.sendMessage(extensionId, { + getStream: true, + sources: [ + "window", + "screen", + "tab" + ]}, + function(response) { + console.log(response); + callback(response); + }); +}; diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index fb01946b07731e6b8c834346614839b865261b5b..942e492c1a348abef542e318decf2231eba5e0c0 100755 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -51,4 +51,13 @@ <script src="/client/lib/jquery.json-2.4.min.js"></script> <script src="/client/lib/verto-min.js"></script> <script src="/client/lib/verto_extension.js"></script> + <!-- + TODO: find a better way to include this + Libs needed for kurento clientside communication. + --> + <script src="/html5client/js/bower_components/reconnectingWebsocket/reconnecting-websocket.js"></script> + <script src="/html5client/js/bower_components/adapter.js/release/adapter.js"></script> + <script src="/html5client/js/bower_components/kurento-utils/dist/kurento-utils.js"></script> + <script src="/html5client/js/adjust-videos.js"></script> + <script src="/client/lib/kurento-extension.js"></script> </body> diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js index 5b953b2ea43f93c1add309adf8b5eb185d52ae17..2c6f548690c4ffbe20d8352b7ef5dab2392630e5 100644 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js @@ -1,5 +1,7 @@ import VertoBridge from './verto'; +import KurentoBridge from './kurento'; -const screenshareBridge = new VertoBridge(); +//const screenshareBridge = new VertoBridge(); +const screenshareBridge = new KurentoBridge(); export default screenshareBridge; diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js new file mode 100755 index 0000000000000000000000000000000000000000..00f12f5b180849329ea65ebf3b888d3fb61b70d2 --- /dev/null +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -0,0 +1,49 @@ +import Users from '/imports/api/users'; +import Auth from '/imports/ui/services/auth'; +import BridgeService from './service'; + +const getUserId = () => { + const userID = Auth.userID; + return userID; +} + +const getMeetingId = () => { + const meetingID = Auth.meetingID; + return meetingID; +} + +const getUsername = () => { + return Users.findOne({ userId: getUserId() }).name; +} + +export default class KurentoScreenshareBridge { + kurentoWatchVideo() { + window.kurentoWatchVideo( + 'screenshareVideo', + BridgeService.getConferenceBridge(), + getUsername(), + getMeetingId(), + null, + null, + ); + } + + kurentoExitVideo() { + window.kurentoExitVideo(); + } + + kurentoShareScreen() { + window.kurentoShareScreen( + 'screenshareVideo', + BridgeService.getConferenceBridge(), + getUsername(), + getMeetingId(), + null, + null, + ); + } + + kurentoExitScreenShare() { + window.kurentoExitScreenShare(); + } +} diff --git a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js index 92b24864c11fe4e244538a58fc0f594d61cdcbe8..aacc7d1eeab570c5cbd5169f0105164bccd2c99d 100644 --- a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js +++ b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import addScreenshare from '../modifiers/addScreenshare'; -export default function handleBroadcastStartedVoice({ body }, meetingId) { +export default function handleScreenshareStarted({ body }, meetingId) { check(meetingId, String); check(body, Object); diff --git a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js index d0308ab5a312c3de18cc23175ce23e9237a92372..11e2871f0c7fb7ab0ad494fd8bb5336774b4fd55 100644 --- a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js +++ b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import clearScreenshare from '../modifiers/clearScreenshare'; -export default function handleBroadcastStartedVoice({ body }, meetingId) { +export default function handleScreenshareStopped({ body }, meetingId) { const { screenshareConf } = body; check(meetingId, String); diff --git a/bigbluebutton-html5/imports/api/video/server/eventHandlers.js b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js new file mode 100644 index 0000000000000000000000000000000000000000..e20b6cd39a60214325a75d8f3428ece95eda1940 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js @@ -0,0 +1,6 @@ +import RedisPubSub from '/imports/startup/server/redis2x'; +import handleUserSharedHtml5Webcam from './handlers/userSharedHtml5Webcam'; +import handleUserUnsharedHtml5Webcam from './handlers/userUnsharedHtml5Webcam'; + +RedisPubSub.on('UserBroadcastCamStartedEvtMsg', handleUserSharedHtml5Webcam); +RedisPubSub.on('UserBroadcastCamStoppedEvtMsg', handleUserUnsharedHtml5Webcam); diff --git a/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js new file mode 100644 index 0000000000000000000000000000000000000000..b0c21ee31cf51227d5e3aef827fb58c0f4aa56ec --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js @@ -0,0 +1,10 @@ +import sharedWebcam from '../modifiers/sharedWebcam'; + +export default function handleUserSharedHtml5Webcam({ header, payload }) { + const meetingId = header.meetingId; + const userId = header.userId; + + check(meetingId, String); + + return sharedWebcam(meetingId, userId); +} diff --git a/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js new file mode 100644 index 0000000000000000000000000000000000000000..bf0f0994f9e453285d70fc095869715c1d4cd7c6 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js @@ -0,0 +1,10 @@ +import unsharedWebcam from '../modifiers/unsharedWebcam'; + +export default function handleUserUnsharedHtml5Webcam({ header, payload }) { + const meetingId = header.meetingId; + const userId = header.userId; + + check(meetingId, String); + + return unsharedWebcam(meetingId, userId); +} diff --git a/bigbluebutton-html5/imports/api/video/server/index.js b/bigbluebutton-html5/imports/api/video/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9a7510e23ef5b6306a53e4b5743c7810976a3c18 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/index.js @@ -0,0 +1,2 @@ +import './eventHandlers'; +import './methods'; diff --git a/bigbluebutton-html5/imports/api/video/server/methods.js b/bigbluebutton-html5/imports/api/video/server/methods.js new file mode 100644 index 0000000000000000000000000000000000000000..1f1dd46f1096c1479436e90cf776da4d57c1ac2f --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/methods.js @@ -0,0 +1,7 @@ +import { Meteor } from 'meteor/meteor'; +import userShareWebcam from './methods/userShareWebcam'; +import userUnshareWebcam from './methods/userUnshareWebcam'; + +Meteor.methods({ + userShareWebcam, userUnshareWebcam, +}); diff --git a/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..6121725948bb5c968bf00df07ca2c4cb0c0a6dc0 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js @@ -0,0 +1,38 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import RedisPubSub from '/imports/startup/server/redis2x'; + +export default function userShareWebcam(credentials, message) { + const REDIS_CONFIG = Meteor.settings.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'UserBroadcastCamStartMsg'; + + const { meetingId, requesterUserId, requesterToken } = credentials; + + Logger.info(' user sharing webcam: ', credentials); + + check(meetingId, String); + check(requesterUserId, String); + check(requesterToken, String); + // check(message, Object); + + // const actionName = 'joinVideo'; + /* TODO throw an error if user has no permission to share webcam + if (!isAllowedTo(actionName, credentials)) { + throw new Meteor.Error('not-allowed', `You are not allowed to share webcam`); + } */ + + const payload = { + stream: message, + isHtml5Client: true, + }; + + const header = { + meetingId, + name: EVENT_NAME, + userId: requesterUserId, + }; + + return RedisPubSub.publish(CHANNEL, EVENT_NAME, meetingId, payload, header); +} diff --git a/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..62b83be491441184ea151c73d9c215974ebb4e8f --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js @@ -0,0 +1,38 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import RedisPubSub from '/imports/startup/server/redis2x'; + +export default function userUnshareWebcam(credentials, message) { + const REDIS_CONFIG = Meteor.settings.redis; + const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; + const EVENT_NAME = 'UserBroadcastCamStopMsg'; + + const { meetingId, requesterUserId, requesterToken } = credentials; + + Logger.info(' user unsharing webcam: ', credentials); + + check(meetingId, String); + check(requesterUserId, String); + check(requesterToken, String); + // check(message, Object); + + // const actionName = 'joinVideo'; + /* TODO throw an error if user has no permission to share webcam + if (!isAllowedTo(actionName, credentials)) { + throw new Meteor.Error('not-allowed', `You are not allowed to share webcam`); + } */ + + const payload = { + stream: message, + isHtml5Client: true, + }; + + const header = { + meetingId, + name: EVENT_NAME, + userId: requesterUserId, + }; + + return RedisPubSub.publish(CHANNEL, EVENT_NAME, meetingId, payload, header); +} diff --git a/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..0a8f96321ba19e5296fa8baf16214a136a4e124b --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js @@ -0,0 +1,32 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; + +export default function sharedWebcam(meetingId, userId) { + check(meetingId, String); + check(userId, String); + + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + meetingId, + userId, + has_stream: true, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding user to collection: ${err}`); + } + + if (numChanged) { + return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`); + } + }; + + return Users.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..817031199b1053c7366fe96b6308eecd1c958b62 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js @@ -0,0 +1,32 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; + +export default function unsharedWebcam(meetingId, userId) { + check(meetingId, String); + check(userId, String); + + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + meetingId, + userId, + has_stream: false, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding user to collection: ${err}`); + } + + if (numChanged) { + return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`); + } + }; + + return Users.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index d825b5dc704208a5eea0c51fc5d469ee351e829d..9970b8e74ed1e5815653a22f11cea19b57b16740 100644 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -83,7 +83,7 @@ Base.defaultProps = defaultProps; const SUBSCRIPTIONS_NAME = [ 'users', 'chat', 'cursor', 'meetings', 'polls', 'presentations', 'annotations', - 'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', + 'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', 'screenshare', ]; const BaseContainer = createContainer(({ params }) => { diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 4cfa5c6b62a1c0d0f394b60003679a81f057048d..cccf78cc5b2a9bb1c4d01f749f927ebd839b0593 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -31,6 +31,22 @@ const intlMessages = defineMessages({ id: 'app.actionsBar.actionsDropdown.presentationDesc', description: 'adds context to upload presentation option', }, + desktopShareLabel: { + id: 'app.actionsBar.actionsDropdown.desktopShareLabel', + description: 'Desktop Share option label', + }, + stopDesktopShareLabel: { + id: 'app.actionsBar.actionsDropdown.stopDesktopShareLabel', + description: 'Stop Desktop Share option label', + }, + desktopShareDesc: { + id: 'app.actionsBar.actionsDropdown.desktopShareDesc', + description: 'adds context to desktop share option', + }, + stopDesktopShareDesc: { + id: 'app.actionsBar.actionsDropdown.stopDesktopShareDesc', + description: 'adds context to stop desktop share option', + }, }); class ActionsDropdown extends Component { @@ -52,7 +68,13 @@ class ActionsDropdown extends Component { } render() { - const { intl, isUserPresenter } = this.props; + const { + intl, + isUserPresenter, + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, + } = this.props; if (!isUserPresenter) return null; @@ -76,6 +98,18 @@ class ActionsDropdown extends Component { description={intl.formatMessage(intlMessages.presentationDesc)} onClick={this.handlePresentationClick} /> + <DropdownListItem + icon="desktop" + label={intl.formatMessage(intlMessages.desktopShareLabel)} + description={intl.formatMessage(intlMessages.desktopShareDesc)} + onClick={handleShareScreen} + /> + <DropdownListItem + icon="desktop" + label={intl.formatMessage(intlMessages.stopDesktopShareLabel)} + description={intl.formatMessage(intlMessages.stopDesktopShareDesc)} + onClick={handleUnshareScreen} + /> </DropdownList> </DropdownContent> </Dropdown> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index ad405b15fd069caf4e9ba80d590cf90fb0bdf962..4412ec793fc341fee7836720065ad6f36760bf1b 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -3,17 +3,30 @@ import styles from './styles.scss'; import EmojiContainer from './emoji-menu/container'; import ActionsDropdown from './actions-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; +import JoinVideoOptionsContainer from '../video-dock/video-menu/container'; const ActionsBar = ({ isUserPresenter, + handleExitAudio, + handleOpenJoinAudio, + handleExitVideo, + handleJoinVideo, + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, }) => ( <div className={styles.actionsbar}> <div className={styles.left}> - <ActionsDropdown {...{ isUserPresenter }} /> + <ActionsDropdown {...{ isUserPresenter, handleShareScreen, handleUnshareScreen, isVideoBroadcasting}} /> </div> <div className={styles.center}> <AudioControlsContainer /> - {/* <JoinVideo /> */} + {Meteor.settings.public.kurento.enableVideo ? + <JoinVideoOptionsContainer + handleJoinVideo={handleJoinVideo} + handleCloseVideo={handleExitVideo} + /> + : null} <EmojiContainer /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index 1037966d1dd4b3a3801fb819d34f28d170cbd464..bcc1935453241983d209ef7f99bdbcf9bcea50c9 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -4,6 +4,8 @@ import { withModalMounter } from '/imports/ui/components/modal/service'; import ActionsBar from './component'; import Service from './service'; import AudioService from '../audio/service'; +import VideoService from '../video-dock/service'; +import ScreenshareService from '../screenshare/service'; import AudioModal from '../audio/audio-modal/component'; @@ -19,10 +21,20 @@ export default withModalMounter(createContainer(({ mountModal }) => { const handleExitAudio = () => AudioService.exitAudio(); const handleOpenJoinAudio = () => mountModal(<AudioModal handleJoinListenOnly={AudioService.joinListenOnly} />); + const handleExitVideo = () => VideoService.exitVideo(); + const handleJoinVideo = () => VideoService.joinVideo(); + const handleShareScreen = () => ScreenshareService.shareScreen(); + const handleUnshareScreen = () => ScreenshareService.unshareScreen(); + const isVideoBroadcasting = () => ScreenshareService.isVideoBroadcasting(); return { isUserPresenter: isPresenter, handleExitAudio, handleOpenJoinAudio, + handleExitVideo, + handleJoinVideo, + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting }; }, ActionsBarContainer)); diff --git a/bigbluebutton-html5/imports/ui/components/app/container.jsx b/bigbluebutton-html5/imports/ui/components/app/container.jsx index 88abd7c9b6e1e4d076700b4a8e27247437c05537..d8d0b54459de4d3f028644695ea34bd7184b6287 100644 --- a/bigbluebutton-html5/imports/ui/components/app/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/app/container.jsx @@ -7,6 +7,7 @@ import Auth from '/imports/ui/services/auth'; import Users from '/imports/api/users'; import Breakouts from '/imports/api/breakouts'; import Meetings from '/imports/api/meetings'; +import Screenshare from '/imports/api/screenshare'; import ClosedCaptionsContainer from '/imports/ui/components/closed-captions/container'; diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx index de9221e18afa228ef46256da32d02c13a5e6e2f3..119567f8a137dc3093fd980b0a9d0859398276b5 100644 --- a/bigbluebutton-html5/imports/ui/components/media/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx @@ -8,7 +8,7 @@ import ScreenshareContainer from '../screenshare/container'; import DefaultContent from '../presentation/default-content/component'; const defaultProps = { - overlay: null, // <VideoDockContainer/>, + overlay: <VideoDockContainer />, content: <PresentationAreaContainer />, defaultContent: <DefaultContent />, }; diff --git a/bigbluebutton-html5/imports/ui/components/media/service.js b/bigbluebutton-html5/imports/ui/components/media/service.js index d583c089f19be3a4c79ea3e9d21b9f3f1164ad9b..9523f64779f3dc4eee14ac7f0f138856d0384a1d 100644 --- a/bigbluebutton-html5/imports/ui/components/media/service.js +++ b/bigbluebutton-html5/imports/ui/components/media/service.js @@ -17,11 +17,11 @@ function shouldShowWhiteboard() { } function shouldShowScreenshare() { - return isVideoBroadcasting(); + return isVideoBroadcasting() && Meteor.settings.public.kurento.enableScreensharing; } function shouldShowOverlay() { - return false; + return Meteor.settings.public.kurento.enableVideo; } export default { diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx index 093de370e6ef54ab0be0c1ed601359aa9f67c039..c381280101b799860a9807c0cabff1e0c31199fe 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx @@ -7,7 +7,7 @@ export default class ScreenshareComponent extends React.Component { render() { return ( - <video id="screenshareVideo" style={{ height: '100%', width: '100%' }} /> + <video id="screenshareVideo" style={{ height: '100%', width: '100%' }} autoPlay playsInline /> ); } } diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index cdb7ecc5ef638abb9c47709b390b6e2b68bab139..33470b2ecd571fe97d01092772178b9240ecaf88 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -1,33 +1,46 @@ import Screenshare from '/imports/api/screenshare'; import VertoBridge from '/imports/api/screenshare/client/bridge'; +import KurentoBridge from '/imports/api/screenshare/client/bridge'; import PresentationService from '/imports/ui/components/presentation/service'; // when the meeting information has been updated check to see if it was // screensharing. If it has changed either trigger a call to receive video // and display it, or end the call and hide the video -function isVideoBroadcasting() { +const isVideoBroadcasting = () => { const ds = Screenshare.findOne({}); if (!ds) { return false; } - return ds.screenshare.stream && !PresentationService.isPresenter(); + + // TODO commented out isPresenter to enable screen viewing to the presenter + return ds.screenshare.stream; // && !PresentationService.isPresenter(); } // if remote screenshare has been ended disconnect and hide the video stream -function presenterScreenshareHasEnded() { - // references a function in the global namespace inside verto_extension.js +const presenterScreenshareHasEnded = () => { + // references a function in the global namespace inside kurento-extension.js // that we load dynamically - VertoBridge.vertoExitVideo(); + KurentoBridge.kurentoExitVideo(); } // if remote screenshare has been started connect and display the video stream -function presenterScreenshareHasStarted() { - // references a function in the global namespace inside verto_extension.js +const presenterScreenshareHasStarted = () => { + // references a function in the global namespace inside kurento-extension.js // that we load dynamically - VertoBridge.vertoWatchVideo(); + //VertoBridge.vertoWatchVideo(); + KurentoBridge.kurentoWatchVideo(); +} + +const shareScreen = () => { + KurentoBridge.kurentoShareScreen(); +} + +const unshareScreen = () => { + console.log("Exiting screenshare"); + KurentoBridge.kurentoExitScreenShare(); } export { - isVideoBroadcasting, presenterScreenshareHasEnded, presenterScreenshareHasStarted, + isVideoBroadcasting, presenterScreenshareHasEnded, presenterScreenshareHasStarted, shareScreen, unshareScreen, }; diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx index fcd9e12dbd8bff5bd7965e6b0cc626e36b7bd76c..90a1bd17d8a975b4dac305f8d902cee595b88ab8 100644 --- a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx @@ -1,11 +1,317 @@ -import React from 'react'; +import React, { Component } from 'react'; import ScreenshareContainer from '/imports/ui/components/screenshare/container'; import styles from './styles'; -const VideoDock = () => ( - <div className={styles.videoDock}> - <ScreenshareContainer /> - </div> -); +window.addEventListener('resize', () => { + window.adjustVideos('webcamArea', true); +}); -export default VideoDock; +class VideoElement extends Component { + constructor(props) { + super(props); + } +} + + +export default class VideoDock extends Component { + + constructor(props) { + super(props); + + this.state = { + videos: {} + }; + + this.state = { + // Set a valid kurento application server socket in the settings + ws: new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl), + webRtcPeers: {}, + wsQueue: [], + }; + + this.state.ws.onopen = () => { + while (this.state.wsQueue.length > 0) { + this.sendMessage(this.state.wsQueue.pop()); + } + }; + + this.sendUserShareWebcam = props.sendUserShareWebcam.bind(this); + this.sendUserUnshareWebcam = props.sendUserUnshareWebcam.bind(this); + + this.unshareWebcam = this.unshareWebcam.bind(this); + this.shareWebcam = this.shareWebcam.bind(this); + } + + componentDidMount() { + const that = this; + const ws = this.state.ws; + const { users } = this.props; + for (let i = 0; i < users.length; i++) { + if (users[i].has_stream) { + console.log("COMPONENT DID MOUNT => " + users[i].userId); + this.start(users[i].userId, false, this.refs.videoInput); + } + } + + document.addEventListener('joinVideo', () => { that.shareWebcam(); });// TODO find a better way to do this + document.addEventListener('exitVideo', () => { that.unshareWebcam(); }); + + ws.addEventListener('message', (msg) => { + const parsedMessage = JSON.parse(msg.data); + + console.debug('Received message new ws message: '); + console.debug(parsedMessage); + + switch (parsedMessage.id) { + + case 'startResponse': + this.startResponse(parsedMessage); + break; + + case 'error': + this.handleError(parsedMessage); + break; + + case 'playStart': + this.handlePlayStart(parsedMessage); + break; + + case 'playStop': + this.handlePlayStop(parsedMessage); + + break; + + case 'iceCandidate': + + const webRtcPeer = this.state.webRtcPeers[parsedMessage.cameraId]; + + if (webRtcPeer !== null) { + webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => { + if (err) { + return console.error(`Error adding candidate: ${err}`); + } + }); + } else { + console.error(' [ICE] Message arrived before webRtcPeer?'); + } + + break; + + } + }); + } + + start(id, shareWebcam, videoInput) { + const that = this; + + const ws = this.state.ws; + + console.log(`Starting video call for video: ${id}`); + console.log('Creating WebRtcPeer and generating local sdp offer ...'); + + const onIceCandidate = function (candidate) { + const message = { + id: 'onIceCandidate', + candidate, + cameraId: id, + }; + that.sendMessage(message); + }; + + const options = { + mediaConstraints: { audio: false, + video: { + width: {min: 320, ideal: 320}, + height: {min: 240, ideal:240}, + frameRate: { min: 5, ideal: 10} + } + }, + onicecandidate: onIceCandidate, + }; + + let peerObj; + if (shareWebcam) { + options.localVideo = videoInput; + peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly; + } else { + peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly; + + options.remoteVideo = document.createElement('video'); + options.remoteVideo.id = `video-elem-${id}`; + options.remoteVideo.width = 120; + options.remoteVideo.height = 90; + options.remoteVideo.autoplay = true; + options.remoteVideo.playsinline = true; + + document.getElementById('webcamArea').appendChild(options.remoteVideo); + } + + this.state.webRtcPeers[id] = new peerObj(options, function (error) { + if (error) { + console.error(' [ERROR] Webrtc error'); + console.error(error); + return; + } + + if (shareWebcam) { + that.state.sharedWebcam = that.state.webRtcPeers[id]; + that.state.myId = id; + } + + this.generateOffer((error, offerSdp) => { + if (error) { + return console.error(error); + } + + console.info(`Invoking SDP offer callback function ${location.host}`); + const message = { + id: 'start', + sdpOffer: offerSdp, + cameraId: id, + cameraShared: shareWebcam, + }; + that.sendMessage(message); + }); + }); + } + + stop(id) { + const { users } = this.props; + if (id == users[0].userId) { + this.unshareWebcam(); + } + const webRtcPeer = this.state.webRtcPeers[id]; + + if (webRtcPeer) { + console.log('Stopping WebRTC peer'); + + if (id == this.state.myId) { + this.state.sharedWebcam.dispose(); + this.state.sharedWebcam = null; + } + + webRtcPeer.dispose(); + delete this.state.webRtcPeers[id]; + } else { + console.log('NO WEBRTC PEER TO STOP?'); + } + + const videoTag = document.getElementById(`video-elem-${id}`); + if (videoTag) { + document.getElementById('webcamArea').removeChild(videoTag); + } + + this.sendMessage({ id: 'stop', cameraId: id }); + + window.adjustVideos('webcamArea', true); + } + + shareWebcam() { + const { users } = this.props; + const id = users[0].userId; + + this.start(id, true, this.refs.videoInput); + } + + unshareWebcam() { + console.log("Unsharing webcam"); + const { users } = this.props; + const id = users[0].userId; + this.sendUserUnshareWebcam(id); + } + + startResponse(message) { + const id = message.cameraId; + const webRtcPeer = this.state.webRtcPeers[id]; + + if (message.sdpAnswer == null) { + return console.debug('Null sdp answer. Camera unplugged?'); + } + + if (webRtcPeer == null) { + return console.debug('Null webrtc peer ????'); + } + + console.log('SDP answer received from server. Processing ...'); + + webRtcPeer.processAnswer(message.sdpAnswer, (error) => { + if (error) { + return console.error(error); + } + }); + + this.sendUserShareWebcam(id); + } + + sendMessage(message) { + const ws = this.state.ws; + + if (ws.readyState == WebSocket.OPEN) { + const jsonMessage = JSON.stringify(message); + console.log(`Sending message: ${jsonMessage}`); + ws.send(jsonMessage, (error) => { + if (error) { + console.error(`client: Websocket error "${error}" on message "${jsonMessage.id}"`); + } + }); + } else { + this.state.wsQueue.push(message); + } + } + + handlePlayStop(message) { + console.log('Handle play stop <--------------------'); + + this.stop(message.cameraId); + } + + handlePlayStart(message) { + console.log('Handle play start <==================='); + + window.adjustVideos('webcamArea', true); + } + + handleError(message) { + console.log(` Handle error ---------------------> ${message.message}`); + } + + render() { + return ( + + <div className={styles.videoDock}> + <div id="webcamArea" /> + <video id="shareWebcamVideo" className={styles.sharedWebcamVideo} ref="videoInput" /> + </div> + ); + } + + shouldComponentUpdate(nextProps, nextState) { + const { users } = this.props; + const nextUsers = nextProps.users; + + if (users) { + let suc = false; + + for (let i = 0; i < users.length; i++) { + if (users && users[i] && + nextUsers && nextUsers[i]) { + if (users[i].has_stream !== nextUsers[i].has_stream) { + console.log(`User ${nextUsers[i].has_stream ? '' : 'un'}shared webcam ${users[i].userId}`); + + if (nextUsers[i].has_stream) { + this.start(users[i].userId, false, this.refs.videoInput); + } else { + this.stop(users[i].userId); + } + + suc = suc || true; + } + } + } + + return true; + } + + return false; + } +} diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx index ad18e501e980831cb554c5070b75b55c51a12f16..0bd4ea2ee4d5f16a57ee285175169b4c161c94aa 100644 --- a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx @@ -1,15 +1,26 @@ -import React from 'react'; +import React, { Component } from 'react'; import { createContainer } from 'meteor/react-meteor-data'; import VideoDock from './component'; +import VideoService from './service'; +import Users from '/imports/api/users'; -const VideoDockContainer = props => ( - <VideoDock> - {props.children} - </VideoDock> -); +class VideoDockContainer extends Component { + constructor(props) { + super(props); + } -export default createContainer(() => { - const data = {}; - return data; -}, VideoDockContainer); + render() { + return ( + <VideoDock {...this.props}> + {this.props.children} + </VideoDock> + ); + } +} + +export default createContainer(() => ({ + sendUserShareWebcam: VideoService.sendUserShareWebcam, + sendUserUnshareWebcam: VideoService.sendUserUnshareWebcam, + users: Users.find().fetch(), +}), VideoDockContainer); diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/service.js b/bigbluebutton-html5/imports/ui/components/video-dock/service.js new file mode 100644 index 0000000000000000000000000000000000000000..99a791ecbfa05e767ec0f3155424a387c0c62b1f --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/service.js @@ -0,0 +1,23 @@ +import { makeCall } from '/imports/ui/services/api'; + +const joinVideo = () => { + var joinVideoEvent = new Event('joinVideo'); + document.dispatchEvent(joinVideoEvent); +} + +const exitVideo = () => { + var exitVideoEvent = new Event('exitVideo'); + document.dispatchEvent(exitVideoEvent); +} + +const sendUserShareWebcam = (stream) => { + makeCall('userShareWebcam', stream); +}; + +const sendUserUnshareWebcam = (stream) => { + makeCall('userUnshareWebcam', stream); +}; + +export default { + sendUserShareWebcam, sendUserUnshareWebcam, joinVideo, exitVideo, +}; diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss index a37e967d4ce74c1e91f784b5d0fac052d3fd6009..4157b2c58194558e64ead582c2b654f8464acf8f 100644 --- a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss @@ -7,9 +7,15 @@ bottom: 0; left: 0; - background-image: url(https://avatars.slack-edge.com/2016-01-04/17715243383_99a961f4cb2bf2cde5c4_512.jpg); background-size: cover; background-position: center; box-shadow: 0 0 5px rgba(0, 0, 0, .5); border-radius: .2rem; } + +.secretButtons { +} + +.sharedWebcamVideo { + display: none; +} diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..9a2ca1c0181283ead37a71a3f41a49a083d8b02a --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { createContainer } from 'meteor/react-meteor-data'; +import Button from '/imports/ui/components/button/component'; +import { withRouter } from 'react-router'; +import { defineMessages, injectIntl } from 'react-intl'; + +const intlMessages = defineMessages({ + joinVideo: { + id: 'app.video.joinVideo', + description: 'Join video button label', + }, + leaveVideo: { + id: 'app.video.leaveVideo', + description: 'Leave video button label', + }, +}); + +class JoinVideoOptions extends React.Component { + render() { + const { + intl, + isSharingVideo, + handleJoinVideo, + handleCloseVideo, + } = this.props; + + if (isSharingVideo) { + return ( + <Button + onClick={handleCloseVideo} + label={intl.formatMessage(intlMessages.leaveVideo)} + color={'danger'} + icon={'video'} + size={'lg'} + circle + /> + ); + } + + return ( + <Button + onClick={handleJoinVideo} + label={intl.formatMessage(intlMessages.joinVideo)} + color={'primary'} + icon={'video_off'} + size={'lg'} + circle + /> + ); + } +} + +export default withRouter(injectIntl(JoinVideoOptions)); diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx new file mode 100755 index 0000000000000000000000000000000000000000..da5950f90a3da413c568f53f93964149487501b2 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { createContainer } from 'meteor/react-meteor-data'; +import Users from '/imports/api/users'; +import Auth from '/imports/ui/services/auth/index'; +import JoinVideoOptions from './component'; + +const JoinVideoOptionsContainer = props => (<JoinVideoOptions {...props} />); + +export default createContainer((params) => { + const userId = Auth.userID; + const user = Users.findOne({ userId: userId }); + + const isSharingVideo = user.has_stream ? true : false; + + return { + isSharingVideo, + handleJoinVideo: params.handleJoinVideo, + handleCloseVideo: params.handleCloseVideo, + }; +}, JoinVideoOptionsContainer); diff --git a/bigbluebutton-html5/private/config/development/public/kurento.yaml b/bigbluebutton-html5/private/config/development/public/kurento.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3a2e239be80123772738cea177f5bda6b7fd79e7 --- /dev/null +++ b/bigbluebutton-html5/private/config/development/public/kurento.yaml @@ -0,0 +1,6 @@ +kurento: + wsUrl: 'HOST' + chromeExtensionKey: 'KEY' + chromeExtensionLink: 'LINK' + enableScreensharing: false + enableVideo: false diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index e267a8ca7e33db7f77b4fe04b9d33f4b30c71a67..65b346187b2d62a584a15b143556c32f2979d15f 100644 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -165,9 +165,11 @@ "app.actionsBar.actionsDropdown.presentationLabel": "Upload a presentation", "app.actionsBar.actionsDropdown.initPollLabel": "Initiate a poll", "app.actionsBar.actionsDropdown.desktopShareLabel": "Share your screen", + "app.actionsBar.actionsDropdown.stopDesktopShareLabel": "Stop sharing your screen", "app.actionsBar.actionsDropdown.presentationDesc": "Upload your presentation", "app.actionsBar.actionsDropdown.initPollDesc": "Initiate a poll", "app.actionsBar.actionsDropdown.desktopShareDesc": "Share your screen with others", + "app.actionsBar.actionsDropdown.stopDesktopShareDesc": "Stop sharing your screen with", "app.actionsBar.emojiMenu.statusTriggerLabel": "Status", "app.actionsBar.emojiMenu.awayLabel": "Away", "app.actionsBar.emojiMenu.awayDesc": "Change your status to away", @@ -256,4 +258,6 @@ "app.guest.waiting": "Waiting for approval to join", "app.notification.recordingStart": "This session is now being recorded", "app.notification.recordingStop": "This session is not being recorded anymore" + "app.video.joinVideo": "Cam off", + "app.video.leaveVideo": "Cam on" } diff --git a/bigbluebutton-html5/public/js/adjust-videos.js b/bigbluebutton-html5/public/js/adjust-videos.js new file mode 100644 index 0000000000000000000000000000000000000000..5d8e44e9420eb6c7941f188bb3ded85642d8d7aa --- /dev/null +++ b/bigbluebutton-html5/public/js/adjust-videos.js @@ -0,0 +1,91 @@ + +(function() { + function adjustVideos(tagId, centerVideos) { + const _minContentAspectRatio = 4 / 3.0; + + function calculateOccupiedArea(canvasWidth, canvasHeight, numColumns, numRows, numChildren) { + const obj = calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows); + obj.occupiedArea = obj.width * obj.height * numChildren; + obj.numColumns = numColumns; + obj.numRows = numRows; + obj.cellAspectRatio = _minContentAspectRatio; + return obj; + } + + function calculateCellDimensions(canvasWidth, canvasHeight, numColumns, numRows) { + const obj = { + width: Math.floor(canvasWidth / numColumns), + height: Math.floor(canvasHeight / numRows), + }; + + if (obj.width / obj.height > _minContentAspectRatio) { + obj.width = Math.min(Math.floor(obj.height * _minContentAspectRatio), Math.floor(canvasWidth / numColumns)); + } else { + obj.height = Math.min(Math.floor(obj.width / _minContentAspectRatio), Math.floor(canvasHeight / numRows)); + } + return obj; + } + + function findBestConfiguration(canvasWidth, canvasHeight, numChildrenInCanvas) { + let bestConfiguration = { + occupiedArea: 0, + }; + + for (let cols = 1; cols <= numChildrenInCanvas; cols++) { + let rows = Math.floor(numChildrenInCanvas / cols); + + // That's a small HACK, different from the original algorithm + // Sometimes numChildren will be bigger than cols*rows, this means that this configuration + // can't show all the videos and shouldn't be considered. So we just increment the number of rows + // and get a configuration which shows all the videos albeit with a few missing slots in the end. + // For example: with numChildren == 8 the loop will generate cols == 3 and rows == 2 + // cols * rows is 6 so we bump rows to 3 and then cols*rows is 9 which is bigger than 8 + if (numChildrenInCanvas > cols * rows) { + rows += 1; + } + + const currentConfiguration = calculateOccupiedArea(canvasWidth, canvasHeight, cols, rows, numChildrenInCanvas); + + if (currentConfiguration.occupiedArea > bestConfiguration.occupiedArea) { + bestConfiguration = currentConfiguration; + } + } + + return bestConfiguration; + } + + // http://stackoverflow.com/a/3437825/414642 + const e = $("#" + tagId).parent(); + const x = e.outerWidth() - 1; + const y = e.outerHeight() - 1; + + const videos = $("#" + tagId + " video:visible"); + + const best = findBestConfiguration(x, y, videos.length); + + videos.each(function (i) { + const row = Math.floor(i / best.numColumns); + const col = Math.floor(i % best.numColumns); + + // Free width space remaining to the right and below of the videos + const remX = (x - best.width * best.numColumns); + const remY = (y - best.height * best.numRows); + + // Center videos + const top = Math.floor(((best.height) * row) + remY / 2); + const left = Math.floor(((best.width) * col) + remX / 2); + + const videoTop = `top: ${top}px;`; + const videoLeft = `left: ${left}px;`; + + $(this).attr('style', videoTop + videoLeft); + }); + + videos.attr('width', best.width); + videos.attr('height', best.height); + } + + console.log(" ---------------------------------- bro!!!"); + + window.adjustVideos = adjustVideos; +})(); diff --git a/bigbluebutton-html5/public/js/bower.json b/bigbluebutton-html5/public/js/bower.json new file mode 100644 index 0000000000000000000000000000000000000000..965dfb86df825ec42fedc9c14cfb63cc40714146 --- /dev/null +++ b/bigbluebutton-html5/public/js/bower.json @@ -0,0 +1,32 @@ +{ + "name": "kurento-hello-world", + "description": "Kurento Browser JavaScript Tutorial", + "authors": [ + "Kurento <info@kurento.org>" + ], + "main": "index.html", + "moduleType": [ + "globals" + ], + "license": "LGPL", + "homepage": "http://www.kurento.org/", + "private": true, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "adapter.js": "5.0.6", + "bootstrap": "3.3.6", + "kurento-utils": "https://github.com/lfzawacki/kurento-utils-js.git#safari11", + "react": "15.1.0", + "reconnectingWebsocket": "1.0.0", + "requirejs": "2.2.0", + "requirejs-react-jsx": "1.0.2", + "requirejs-text": "2.0.15", + "font-awesome": "fontawesome#^4.6.3" + } +} diff --git a/labs/kurento-screenshare/README.md b/labs/bbb-webrtc-sfu/README.md similarity index 100% rename from labs/kurento-screenshare/README.md rename to labs/bbb-webrtc-sfu/README.md diff --git a/labs/bbb-webrtc-sfu/config/default.example.yml b/labs/bbb-webrtc-sfu/config/default.example.yml new file mode 100644 index 0000000000000000000000000000000000000000..54dc22f33aab7a3521e53565bc5e4ec2fa8beb19 --- /dev/null +++ b/labs/bbb-webrtc-sfu/config/default.example.yml @@ -0,0 +1,15 @@ +kurentoUrl: "ws://HOST/kurento" +kurentoIp: "" +localIpAddress: "" +acceptSelfSignedCertificate: false +redisHost : "127.0.0.1" +redisPort : "6379" +clientPort : "3008" +minVideoPort: 30000 +maxVideoPort: 33000 +from-screenshare: "from-screenshare-redis-channel" +to-screenshare: "to-screenshare-redis-channel" +from-video: "from-video-redis-channel" +to-video: "to-video-redis-channel" +from-audio: "from-audio-redis-channel" +to-audio: "to-audio-redis-channel" diff --git a/labs/kurento-screenshare/debug-start.sh b/labs/bbb-webrtc-sfu/debug-start.sh similarity index 100% rename from labs/kurento-screenshare/debug-start.sh rename to labs/bbb-webrtc-sfu/debug-start.sh diff --git a/labs/kurento-screenshare/lib/bbb/messages/Constants.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js similarity index 89% rename from labs/kurento-screenshare/lib/bbb/messages/Constants.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js index 6cccf19f93cadf15684b9a09c2f2ddde37909dbd..a72ae88639b8c9f868120d830ad7a49d5aac452e 100644 --- a/labs/kurento-screenshare/lib/bbb/messages/Constants.js +++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js @@ -17,9 +17,17 @@ FROM_BBB_TRANSCODE_SYSTEM_CHAN : "bigbluebutton:from-bbb-transcode:system", FROM_VOICE_CONF_SYSTEM_CHAN: "from-voice-conf-redis-channel", TO_BBB_TRANSCODE_SYSTEM_CHAN: "bigbluebutton:to-bbb-transcode:system", + FROM_SCREENSHARE: "from-screenshare-redis-channel", + TO_SCREENSHARE: "to-screenshare-redis-channel", + FROM_VIDEO: "from-video-redis-channel", + TO_VIDEO: "to-video-redis-channel", + FROM_AUDIO: "from-audio-redis-channel", + TO_AUDIO: "to-audio-redis-channel", // RedisWrapper events REDIS_MESSAGE : "redis_message", + WEBSOCKET_MESAGE: "ws_message", + GATEWAY_MESSAGE: "gateway_message", // Message identifiers 1x START_TRANSCODER_REQUEST: "start_transcoder_request_message", diff --git a/labs/kurento-screenshare/lib/bbb/messages/Messaging.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js similarity index 69% rename from labs/kurento-screenshare/lib/bbb/messages/Messaging.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js index 92bab79944122fb88e16bb7c30152da68a0f80b7..a352acf1e5f8cfd736e48011316209995bf2a87d 100644 --- a/labs/kurento-screenshare/lib/bbb/messages/Messaging.js +++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js @@ -1,24 +1,24 @@ -var Constants = require('./Constants.js'); +const Constants = require('./Constants.js'); // Messages -var OutMessage = require('./OutMessage.js'); +let OutMessage = require('./OutMessage.js'); -var StartTranscoderRequestMessage = +let StartTranscoderRequestMessage = require('./transcode/StartTranscoderRequestMessage.js')(Constants); -var StopTranscoderRequestMessage = +let StopTranscoderRequestMessage = require('./transcode/StopTranscoderRequestMessage.js')(Constants); -var StartTranscoderSysReqMsg = +let StartTranscoderSysReqMsg = require('./transcode/StartTranscoderSysReqMsg.js')(); -var StopTranscoderSysReqMsg = +let StopTranscoderSysReqMsg = require('./transcode/StopTranscoderSysReqMsg.js')(); -var DeskShareRTMPBroadcastStartedEventMessage = +let DeskShareRTMPBroadcastStartedEventMessage = require('./screenshare/DeskShareRTMPBroadcastStartedEventMessage.js')(Constants); -var DeskShareRTMPBroadcastStoppedEventMessage = +let DeskShareRTMPBroadcastStoppedEventMessage = require('./screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js')(Constants); -var ScreenshareRTMPBroadcastStartedEventMessage2x = +let ScreenshareRTMPBroadcastStartedEventMessage2x = require('./screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js')(Constants); -var ScreenshareRTMPBroadcastStoppedEventMessage2x = +let ScreenshareRTMPBroadcastStoppedEventMessage2x = require('./screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js')(Constants); @@ -31,39 +31,38 @@ function Messaging() {} Messaging.prototype.generateStartTranscoderRequestMessage = function(meetingId, transcoderId, params) { - var statrm = new StartTranscoderSysReqMsg(meetingId, transcoderId, params); + let statrm = new StartTranscoderSysReqMsg(meetingId, transcoderId, params); return statrm.toJson(); } Messaging.prototype.generateStopTranscoderRequestMessage = function(meetingId, transcoderId) { - var stotrm = new StopTranscoderSysReqMsg(meetingId, transcoderId); + let stotrm = new StopTranscoderSysReqMsg(meetingId, transcoderId); return stotrm.toJson(); } Messaging.prototype.generateDeskShareRTMPBroadcastStartedEvent = function(conferenceName, streamUrl, vw, vh, timestamp) { - var stadrbem = new DeskShareRTMPBroadcastStartedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); + let stadrbem = new DeskShareRTMPBroadcastStartedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); return stadrbem.toJson(); } Messaging.prototype.generateDeskShareRTMPBroadcastStoppedEvent = function(conferenceName, streamUrl, vw, vh, timestamp) { - var stodrbem = new DeskShareRTMPBroadcastStoppedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); + let stodrbem = new DeskShareRTMPBroadcastStoppedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); return stodrbem.toJson(); } Messaging.prototype.generateScreenshareRTMPBroadcastStartedEvent2x = function(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp) { - var stadrbem = new ScreenshareRTMPBroadcastStartedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); + let stadrbem = new ScreenshareRTMPBroadcastStartedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); return stadrbem.toJson(); } Messaging.prototype.generateScreenshareRTMPBroadcastStoppedEvent2x = function(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp) { - var stodrbem = new ScreenshareRTMPBroadcastStoppedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); + let stodrbem = new ScreenshareRTMPBroadcastStoppedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); return stodrbem.toJson(); } module.exports = new Messaging(); -module.exports.Constants = Constants; diff --git a/labs/kurento-screenshare/lib/bbb/messages/OutMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/OutMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/OutMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage2x.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/OutMessage2x.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage2x.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..1f2a3fe2134440e14509f4a78d908c5563067f63 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js @@ -0,0 +1,119 @@ +/** + * @classdesc + * Redis wrapper class for connecting to Redis channels + */ + +'use strict'; + +/* Modules */ + +const redis = require('redis'); +const config = require('config'); +const Constants = require('../messages/Constants.js'); +const EventEmitter = require('events').EventEmitter; + +/* Public members */ + +module.exports = class RedisWrapper extends EventEmitter { + constructor(subpattern) { + super(); + // Redis PubSub client holders + this.redisCli = null; + this.redisPub = null; + // Pub and Sub channels/patterns + this.subpattern = subpattern; + } + + static get _retryThreshold() { + return 1000 * 60 * 60; + } + + static get _maxRetries() { + return 10; + } + + startPublisher () { + var options = { + host : config.get('redisHost'), + port : config.get('redisPort'), + //password: config.get('redis.password') + retry_strategy: this._redisRetry + }; + + this.redisPub = redis.createClient(options); + } + + startSubscriber () { + let self = this; + if (this.redisCli) { + console.log(" [RedisWrapper] Redis Client already exists"); + return; + } + + var options = { + host : config.get('redisHost'), + port : config.get('redisPort'), + //password: config.get('redis.password') + retry_strategy: this._redisRetry + }; + + this.redisCli = redis.createClient(options); + + console.log(" [RedisWrapper] Trying to subscribe to redis channel"); + + this.redisCli.on("psubscribe", (channel, count) => { + console.log(" [RedisWrapper] Successfully subscribed to pattern [" + channel + "]"); + }); + + this.redisCli.on("pmessage", this._onMessage.bind(this)); + + if (!this.subpattern) { + throw new Error("[RedisWrapper] No subscriber pattern"); + } + + this.redisCli.psubscribe(this.subpattern); + + console.log(" [RedisWrapper] Started Redis client at " + options.host + ":" + options.port + + " for subscription pattern: " + this.subpattern); + + return ; + } + + stopRedis (callback) { + if (this.redisCli){ + this.redisCli.quit(); + } + callback(false); + } + + publishToChannel (_message, channel) { + let message = _message; + if(this.redisPub) { + console.log(" [RedisWrapper] Sending message to channel [" + channel + "] : " + message ); + console.log(message); + this.redisPub.publish(channel, message); + } + } + + /* Private members */ + + _onMessage (pattern, channel, _message) { + let message = (typeof _message !== 'object')?JSON.parse(_message):_message; + console.log(" [RedisWrapper] Message received from channel [" + channel + "] : " + message); + // use event emitter to throw new message + this.emit(Constants.REDIS_MESSAGE, message); + } + + static _redisRetry (options) { + if (options.error && options.error.code === 'ECONNREFUSED') { + return new Error('The server refused the connection'); + } + if (options.total_retry_time > RedisWrapper._retryThreshold) { + return new Error('Retry time exhausted'); + } + if (options.times_connected > RedisWrapper._maxRetries) { + return undefined; + } + return Math.max(options.attempt * 100, 3000); + } +} diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js new file mode 100644 index 0000000000000000000000000000000000000000..427083768e12f81a0cdd69ea95790837d5b3c294 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js @@ -0,0 +1,121 @@ +/** + * @classdesc + * BigBlueButton redis gateway for bbb-screenshare node app + */ + +'use strict'; + +/* Modules */ + +const C = require('../messages/Constants.js'); +const RedisWrapper = require('./RedisWrapper.js'); +const config = require('config'); +const util = require('util'); +const EventEmitter = require('events').EventEmitter; + +let instance = null; + +module.exports = class BigBlueButtonGW extends EventEmitter { + constructor() { + if(!instance){ + super(); + this.subscribers = {}; + this.publisher = null; + instance = this; + } + + return instance; + } + + addSubscribeChannel (channel) { + if (this.subscribers[channel]) { + return this.subscribers[channel]; + } + + let wrobj = new RedisWrapper(channel); + this.subscribers[channel] = {}; + this.subscribers[channel] = wrobj; + try { + wrobj.startSubscriber(); + wrobj.on(C.REDIS_MESSAGE, this.incomingMessage.bind(this)); + console.log(" [BigBlueButtonGW] Added redis client to this.subscribers[" + channel + "]"); + return Promise.resolve(wrobj); + } + catch (error) { + return Promise.reject(" [BigBlueButtonGW] Could not start redis client for channel " + channel); + } + } + + /** + * Capture messages from subscribed channels and emit an event with it's + * identifier and payload. Check Constants.js for the identifiers. + * + * @param {Object} message Redis message + */ + incomingMessage (message) { + let header; + let payload; + let msg = (typeof message !== 'object')?JSON.parse(message):message; + + // Trying to parse both message types, 1x and 2x + if (msg.header) { + header = msg.header; + payload = msg.payload; + } + else if (msg.core) { + header = msg.core.header; + payload = msg.core.body; + } + + if (header){ + switch (header.name) { + // interoperability with 1.1 + case C.START_TRANSCODER_REPLY: + this.emit(C.START_TRANSCODER_REPLY, payload); + break; + case C.STOP_TRANSCODER_REPLY: + this.emit(C.STOP_TRANSCODER_REPLY, payload); + break; + // 2x messages + case C.START_TRANSCODER_RESP_2x: + payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x]; + this.emit(C.START_TRANSCODER_RESP_2x, payload); + break; + case C.STOP_TRANSCODER_RESP_2x: + payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x]; + this.emit(C.STOP_TRANSCODER_RESP_2x, payload); + break; + + default: + console.log(" [BigBlueButtonGW] Unknown Redis message with ID =>" + header.name); + this.emit(C.GATEWAY_MESSAGE, msg); + } + } + else { + console.log(" [BigBlueButtonGW] Unknown Redis message =>"); + this.emit(C.GATEWAY_MESSAGE, msg); + } + } + + publish (message, channel) { + if (!this.publisher) { + this.publisher = new RedisWrapper(); + this.publisher.startPublisher(); + } + + if (typeof this.publisher.publishToChannel === 'function') { + this.publisher.publishToChannel(message, channel); + } + } + + setEventEmitter (emitter) { + this.emitter = emitter; + } + + _onServerResponse(data) { + console.log(data); + + // Here this is the 'ws' instance + this.sendMessage(data); + } +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js new file mode 100644 index 0000000000000000000000000000000000000000..dcfbdb3876eb7726d2637b1e770ee5ac448a394e --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js @@ -0,0 +1,104 @@ +/* + * Lucas Fialho Zawacki + * Paulo Renato Lanzarin + * (C) Copyright 2017 Bigbluebutton + * + */ + +'use strict'; + +// const express = require('express'); +// const session = require('express-session') +// const wsModule = require('./websocket'); + +const http = require('http'); +const fs = require('fs'); +const EventEmitter = require('events'); +const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw'); +const C = require('../bbb/messages/Constants'); + +// Global variables +module.exports = class ConnectionManager { + + constructor (settings, logger) { + this._logger = logger; + this._screenshareSessions = {}; + + this._setupBBB(); + + this._emitter = this._setupEventEmitter(); + this._adapters = []; + } + + setHttpServer(httpServer) { + this.httpServer = httpServer; + } + + listen(callback) { + this.httpServer.listen(callback); + } + + addAdapter(adapter) { + adapter.setEventEmitter(this._emitter); + this._adapters.push(adapter); + } + + _setupEventEmitter() { + let self = this; + let emitter = new EventEmitter(); + + emitter.on(C.WEBSOCKET_MESSAGE, (data) => { + console.log(" [ConnectionManager] RECEIVED DATA FROM WEBSOCKET"); + switch (data.type) { + case "screenshare": + self._bbbGW.publish(JSON.stringify(data), C.TO_SCREENSHARE); + break; + + case "video": + self._bbbGW.publish(JSON.stringify(data), C.TO_VIDEO); + break; + + case "audio": + self._bbbGW.publish(JSON.stringify(data), C.TO_AUDIO); + break; + + case "default": + // TODO handle API error message; + } + }); + + return emitter; + } + + async _setupBBB() { + this._bbbGW = new BigBlueButtonGW(); + + try { + const screenshare = await this._bbbGW.addSubscribeChannel(C.FROM_SCREENSHARE); + const video = await this._bbbGW.addSubscribeChannel(C.FROM_VIDEO); + const audio = await this._bbbGW.addSubscribeChannel(C.FROM_AUDIO); + + screenshare.on(C.REDIS_MESSAGE, (data) => { + console.log(" [ConnectionManager] RECEIVED DATA FROM REDIS"); + this._emitter.emit('response', data); + }); + + video.on(C.REDIS_MESSAGE, (data) => { + console.log(" [ConnectionManager] RECEIVED DATA FROM REDIS"); + this._emitter.emit('response', data); + }); + + console.log(' [ConnectionManager] Successfully subscribed to processes redis channels'); + } + catch (err) { + console.log(' [ConnectionManager] ' + err); + this._stopAll; + } + } + + _stopSession(sessionId) { + } + + _stopAll() { + } +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js b/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js new file mode 100644 index 0000000000000000000000000000000000000000..8ec5a30fac3abeb8eda002809665d17662757929 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js @@ -0,0 +1,30 @@ +"use strict"; + +const http = require("http"); +const fs = require("fs"); +const config = require('config'); + +module.exports = class HttpServer { + + constructor() { + //const privateKey = fs.readFileSync('sslcert/server.key', 'utf8'); + //const certificate = fs.readFileSync('sslcert/server.crt', 'utf8'); + //const credentials = {key: privateKey, cert: certificate}; + + this.port = config.get('clientPort'); + + this.server = http.createServer((req,res) => { + // + }); + } + + getServerObject() { + return this.server; + } + + listen(callback) { + console.log(' [HttpServer] Listening in port ' + this.port); + this.server.listen(this.port, callback); + } + +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js b/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js new file mode 100644 index 0000000000000000000000000000000000000000..84022e268838c571893d3bf0c2b6f5d092584dbf --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js @@ -0,0 +1,81 @@ +const Joi = require('joi'); + +let instance = null; + +module.exports = class MessageParser { + constructor() { + if(!instance){ + instance = this; + } + return instance; + } + + static const schema { + startScreenshare: Joi.object().keys({ + sdpOffer : Joi.string().required(), + vh: Joi.number().required(), + vw: Joi.number().required() + }), + + startVideo: Joi.object().keys({ + internalMeetingId: joi.string().required(), + callerName : Joi.string().required(), + }), + + startAudio: Joi.object().keys({ + internalMeetingId: joi.string().required(), + callerName : Joi.string().required(), + }), + + playStart: Joi.object().keys({ + }), + + playStop: Joi.object().keys.({ + }), + + stop: Joi.object().keys({ + }), + + onIceCandidate: Joi.object().keys({ + internalMeetingId: joi.string().required(), + candidate: Joi.object().required(), + }), + } + + static const messageTemplate Joi.object().keys({ + id: Joi.string().required(), + type: joi.string().required(), + role: joi.string().required(), + }) + + static const validateMessage (msg) { + let res = Joi.validate(msg, messageTemplate, {allowUnknown: true}); + + if (!res.error) { + res = Joi.validate(msg, schema[msg.id]); + } + + return res; + } + + _parse (message) { + let parsed = { id: '' }; + + try { + parsed = JSON.parse(message); + } catch (e) { + console.error(e); + } + + let res = validateMessage(parsed); + + if (res.error) { + parsed.validMessage = false; + parsed.errors = res.error; + } else { + parsed.validMessage = true; + } + + return parsed; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js new file mode 100644 index 0000000000000000000000000000000000000000..6c109baf75ac71b9c54a12f1ccca3f988a46ff90 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js @@ -0,0 +1,34 @@ +'use strict'; + +// incomplete + +module.exports = class RedisConnectionManager { + + constructor(options) { + + this._client = redis.createClient({options}); + this._pubchannel = options.pubchannel; + this._subchannel = optiosn.subchannel; + + if (options.pubchannel) { + this._client.on() + } + + if (options.subchannel) { + this._client.on() + } + + this._client.on() + // pub + + } + + setEventEmitter(emitter) { + this.emitter = emitter; + } + + _onMessage() { + + } + +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js new file mode 100644 index 0000000000000000000000000000000000000000..d83b475169a11e7a486356e0e0601e037d75442e --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js @@ -0,0 +1,121 @@ +'use strict'; + +const ws = require('ws'); +const C = require('../bbb/messages/Constants'); + +ws.prototype.setErrorCallback = function(callback) { + + this._errorCallback = callback; +}; + +ws.prototype.sendMessage = function(json) { + + let websocket = this; + + if (this._closeCode === 1000) { + console.log("Websocket closed, not sending"); + this._errorCallback("Error: not opened"); + } + + return this.send(JSON.stringify(json), function(error) { + if(error) { + console.log('server: Websocket error "' + error + '" on message "' + json.id + '"'); + + websocket._errorCallback(error); + } + }); + +}; + +module.exports = class WebsocketConnectionManager { + constructor (server, path) { + this.wss = new ws.Server({ + server, + path + }); + + this.wss.on ('connection', (ws) => { + let self = this; + + ws.on('message', (data) => { + let message = {}; + + try { + message = JSON.parse(data); + } catch(e) { + console.error(" [WebsocketConnectionManager] JSON message parse error " + e); + message = {}; + } + + // Test for empty or invalid JSON + if (Object.getOwnPropertyNames(message).length !== 0) { + if (message.callerName && !ws.connectionId) { + ws.connectionId = data.callerName; + } + + this.emitter.emit(C.WEBSOCKET_MESSAGE, message); + } + }); + + //ws.on('message', this._onMessage.bind(this)); + ws.setErrorCallback(this._onError.bind(this)); + + ws.on('close', this._onClose); + ws.on('error', this._onError); + + // TODO: should we delete this listener after websocket dies? + this.emitter.on('response', (data) => { + console.log(' [WebsocketConnectionManager] Receiving event '); + console.log(data); + if (ws.connectionId == data.callerName) { + ws.sendMessage(data); + } + }); + }); + } + + setEventEmitter (emitter) { + console.log(emitter); + this.emitter = emitter; + } + + _onServerResponse (data) { + + console.log(' [WebsocketConnectionManager] Receiving event '); + console.log(data); + + // Here this is the 'ws' instance + this.sendMessage(data); + } + + _onMessage (data) { + + let message = {}; + + try { + message = JSON.parse(data); + } catch(e) { + console.error(" [WebsocketConnectionManager] JSON message parse error " + e); + message = {}; + } + + // Test for empty or invalid JSON + if (Object.getOwnPropertyNames(message).length !== 0) { + this.emitter.emit(C.WEBSOCKET_MESSAGE, message); + } + } + + _onError (err) { + console.log(' [WebsocketConnectionManager] Connection error'); + + } + + _onClose (err) { + console.log(' [WebsocketConnectionManager] Closed Connection'); + } + + _stop () { + + } + +} diff --git a/labs/kurento-screenshare/lib/h264-sdp.js b/labs/bbb-webrtc-sfu/lib/h264-sdp.js similarity index 100% rename from labs/kurento-screenshare/lib/h264-sdp.js rename to labs/bbb-webrtc-sfu/lib/h264-sdp.js diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js b/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..ee03958e42a0d3d54b74e7c76a2a1fef19234b7a --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js @@ -0,0 +1,12 @@ +const MCSApiStub = require('./media/MCSApiStub'); + +process.on('uncaughtException', function (error) { + console.log(error.stack); +}); + +process.on('disconnect',function() { + console.log("Parent exited!"); + process.kill(); +}); + +core = new MCSApiStub(); diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js new file mode 100644 index 0000000000000000000000000000000000000000..94f7acc9dc9f89aa14094232af1bb300e47468e7 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js @@ -0,0 +1,86 @@ +/* + * (C) Copyright 2016 Mconf Tecnologia (http://mconf.com/) + */ + +/** + * @classdesc + * Message constants for the communication with BigBlueButton + * @constructor + */ + +'use strict' + +exports.ALL = 'ALL' + +exports.LOG_LEVEL = {} +exports.LOG_LEVEL.DEBUG = 0 +exports.LOG_LEVEL.INFO = 1 +exports.LOG_LEVEL.WARN = 2 +exports.LOG_LEVEL.ERROR = 3 +exports.LOG_LEVEL.OFF = 100 + +exports.STATUS = {} +exports.STATUS.STARTED = "STARTED" +exports.STATUS.STOPPED = "STOPPED" +exports.STATUS.RUNNING = "RUNNING'" +exports.STATUS.STARTING = "STARTING" +exports.STATUS.STOPPING = "STOPPING" +exports.STATUS.RESTARTING = "RESTARTING" + +exports.USERS = {} +exports.USERS.SFU = "SFU" +exports.USERS.MCU = "MCU" + +exports.MEDIA_TYPE = {} +exports.MEDIA_TYPE.WEBRTC = "WebRtcEndpoint" +exports.MEDIA_TYPE.RTP= "RtpEndpoint" +exports.MEDIA_TYPE.URI = "PlayerEndpoint" + +// Observer Constants +exports.EVENT = {} +exports.EVENT.DIAL_EVENT = "BRIDGE_DIAL" +exports.EVENT.HANGUP_EVENT = "BRIDGE_HANGUP" +exports.EVENT.SESSION_ID_EVENT = "SESSION_ID" +exports.EVENT.AUDIO_SESSION_TERMINATED = "AUDIO_SESSION_TERMINATED" + +// Media server state changes +exports.EVENT.NEW_SESSION = "NewSession" +exports.EVENT.MEDIA_STATE = {}; +exports.EVENT.MEDIA_STATE.MEDIA_EVENT = "MediaEvent" +exports.EVENT.MEDIA_STATE.CHANGED = "MediaStateChanged" +exports.EVENT.MEDIA_STATE.FLOW_OUT = "MediaFlowOutStateChange" +exports.EVENT.MEDIA_STATE.FLOW_IN = "MediaFlowInStateChange" +exports.EVENT.MEDIA_STATE.ENDOFSTREAM = "EndOfStream" +exports.EVENT.MEDIA_STATE.ICE = "OnIceCandidate" + + + +// RTP params +exports.SDP = {}; +exports.SDP.PARAMS = "params" +exports.SDP.MEDIA_DESCRIPTION = "media_description" +exports.SDP.LOCAL_IP_ADDRESS = "local_ip_address" +exports.SDP.LOCAL_VIDEO_PORT = "local_video_port" +exports.SDP.DESTINATION_IP_ADDRESS = "destination_ip_address" +exports.SDP.DESTINATION_VIDEO_PORT = "destination_video_port" +exports.SDP.REMOTE_VIDEO_PORT = "remote_video_port" +exports.SDP.CODEC_NAME = "codec_name" +exports.SDP.CODEC_ID = "codec_id" +exports.SDP.CODEC_RATE = "codec_rate" +exports.SDP.RTP_PROFILE = "rtp_profile" +exports.SDP.SEND_RECEIVE = "send_receive" +exports.SDP.FRAME_RATE = "frame_rate" + +// Strings +exports.STRING = {} +exports.STRING.ANONYMOUS = "ANONYMOUS" +exports.STRING.FS_USER_AGENT_STRING = "Freeswitch_User_Agent" +exports.STRING.XML_MEDIA_FAST_UPDATE = '<?xml version=\"1.0\" encoding=\"utf-8\" ?>' + + '<media_control>' + + '<vc_primitive>' + + '<to_encoder>' + + '<picture_fast_update>' + + '</picture_fast_update>' + + '</to_encoder>' + + '</vc_primitive>' + + '</media_control>' diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js new file mode 100644 index 0000000000000000000000000000000000000000..19a5ed1e41e2fb0470bf7d4bf44be0af62cb5348 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js @@ -0,0 +1,146 @@ +'use strict' + +var config = require('config'); +var C = require('../constants/Constants'); +// EventEmitter +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var MediaController = require('./MediaController.js'); + +let instance = null; + +module.exports = class MCSApiStub extends EventEmitter{ + constructor() { + if(!instance) { + super(); + this.listener = new EventEmitter(); + this._mediaController = new MediaController(this.listener); + instance = this; + } + + return instance; + } + + async join (room, type, params) { + let self = this; + try { + const answer = await this._mediaController.join(room, type, params); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + Promise.reject(err); + } + } + + // Not yet implemented in MediaController, should be simple nonetheless + async leave (room, userId) { + try { + const answer = await this._mediaController.leave(room, userId); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + async publishnsubscribe (user, sourceId, sdp, params) { + try { + const answer = await this._mediaController.publishnsubscribe(user, sourceId, sdp, params); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + async publish (user, room, type, params) { + try { + this.listener.once(C.EVENT.NEW_SESSION+user, (event) => { + let sessionId = event; + this.listener.on(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, (event) => { + this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, event); + }); + }); + const answer = await this._mediaController.publish(user, room, type, params); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + async unpublish (user, mediaId) { + try { + const answer = await this._mediaController.unpublish(user, mediaId); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + async subscribe (user, sourceId, type, params) { + try { + this.listener.once(C.EVENT.NEW_SESSION+user, (event) => { + let sessionId = event; + this.listener.on(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, (event) => { + this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+sessionId, event); + }); + }); + + const answer = await this._mediaController.subscribe(user, sourceId, type, params); + + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + async unsubscribe (user, sdp, params) { + try { + await this._mediaController.unsubscribe(user, mediaId); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + async onEvent (eventName, mediaId) { + try { + const eventTag = this._mediaController.onEvent(eventName, mediaId); + this._mediaController.on(eventTag, (event) => { + this.emit(eventTag, event); + }); + + return Promise.resolve(eventTag); + } + catch (err) { + console.log(err); + return Promise.reject(); + } + } + + async addIceCandidate (mediaId, candidate) { + try { + console.log(" [api] Adding ice candidate for => " + mediaId); + const ack = await this._mediaController.addIceCandidate(mediaId, candidate); + return Promise.resolve(ack); + } + catch (err) { + console.log(err); + Promise.reject(); + } + } + setStrategy (strategy) { + // TODO + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js new file mode 100644 index 0000000000000000000000000000000000000000..de9b888cfef941ce1412f9c9c073f2bd1d25344c --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js @@ -0,0 +1,309 @@ +'use strict' + +const config = require('config'); +const C = require('../constants/Constants'); + +// Model +const SfuUser = require('../model/SfuUser'); +const Room = require('../model/Room.js'); + +const EventEmitter = require('events').EventEmitter; + +/* PRIVATE ELEMENTS */ +/** + * Deep copy a javascript Object + * @param {Object} object The object to be copied + * @return {Object} A deep copy of the given object + */ +function copy(object) { + return JSON.parse(JSON.stringify(object)); +} + +function getPort(min_port, max_port) { + return Math.floor((Math.random()*(max_port - min_port +1)+ min_port)); +} + +function getVideoPort() { + return getPort(config.get('sip.min_video_port'), config.get('sip.max_video_port')); +} + +/* PUBLIC ELEMENTS */ + +let instance = null; + + +module.exports = class MediaController { + constructor(emitter) { + if (!instance) { + this.emitter = emitter; + this._rooms = {}; + this._users = {}; + this._mediaSessions = {}; + instance = this; + } + + return instance; + } + + start (_kurentoClient, _kurentoToken, callback) { + var self = this; + return callback(null); + } + + stop (callback) { + var self = this; + self.stopAllMedias(function (e) { + if (e) { + callback(e); + } + self._rooms = {}; + }); + } + + getVideoPort () { + return getPort(config.get('sip.min_video_port'), config.get('sip.max_video_port')); + } + + getRoom (roomId) { + return this._rooms[roomdId]; + } + + async join (roomId, type, params) { + console.log("[mcs] Join room => " + roomId + ' as ' + type); + try { + let session; + const room = await this.createRoomMCS(roomId); + const user = await this.createUserMCS(roomId, type, params); + let userId = user.id; + room.setUser(user); + if (params.sdp) { + session = user.addSdp(params.sdp); + } + if (params.uri) { + session = user.addUri(params.sdp); + } + + console.log("[mcs] Resolving user " + userId); + return Promise.resolve(userId); + } + catch (err) { + console.log("[mcs] JOIN ERROR " + err); + return Promise.reject(new Error(err)); + } + } + + async publishnsubscribe (userId, sourceId, sdp, params) { + console.log("[mcs] pns"); + let type = params.type; + try { + user = this.getUserMCS(userId); + let userId = user.id; + let session = user.addSdp(sdp, type); + let sessionId = session.id; + + if (typeof this._mediaSessions[session.id] == 'undefined' || + !this._mediaSessions[session.id]) { + this._mediaSessions[session.id] = {}; + } + + this._mediaSessions[session.id] = session; + + const answer = await user.startSession(session.id); + await user.connect(sourceId, session.id); + + console.log("[mcs] user with sdp session " + session.id); + return Promise.resolve({userId, sessionId}); + } + catch (err) { + console.log("[mcs] PUBLISHNSUBSCRIBE ERROR " + err); + return Promise.reject(new Error(err)); + } + } + + async publish (userId, roomId, type, params) { + console.log("[mcs] publish"); + let session; + // TODO handle mediaType + let mediaType = params.mediaType; + let answer; + + try { + console.log(" [mcs] Fetching user => " + userId); + + const user = await this.getUserMCS(userId); + + console.log(" [mcs] Fetched user => " + user); + + switch (type) { + case "RtpEndpoint": + case "WebRtcEndpoint": + session = user.addSdp(params.descriptor, type); + + answer = await user.startSession(session.id); + break; + case "URI": + session = user.addUri(params.descriptor, type); + + answer = await user.startSession(session.id); + break; + + default: return Promise.reject(new Error("[mcs] Invalid media type")); + } + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + + if (typeof this._mediaSessions[session.id] == 'undefined' || + !this._mediaSessions[session.id]) { + this._mediaSessions[session.id] = {}; + } + + this._mediaSessions[session.id] = session; + let sessionId = session.id; + + return Promise.resolve({answer, sessionId}); + } + + async subscribe (userId, type, sourceId, params) { + console.log(" [mcs] subscribe"); + let session; + // TODO handle mediaType + let mediaType = params.mediaType; + let answer; + let sourceSession = this._mediaSessions[sourceId]; + + if (typeof sourceSession === 'undefined') { + return Promise.reject(new Error(" [mcs] Media session " + sourceId + " was not found")); + } + + try { + console.log(" [mcs] Fetching user => " + userId); + + const user = await this.getUserMCS(userId); + + console.log(" [mcs] Fetched user => " + user); + + switch (type) { + case "RtpEndpoint": + case "WebRtcEndpoint": + session = user.addSdp(params.descriptor, type); + + answer = await user.startSession(session.id); + await sourceSession.connect(session._mediaElement); + + break; + case "URI": + session = user.addUri(params.descriptor, type); + answer = await user.startSession(session.id); + await sourceSession.connect(session._mediaElement); + + break; + + default: return Promise.reject(new Error("[mcs] Invalid media type")); + } + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + + if (typeof this._mediaSessions[session.id] == 'undefined' || + !this._mediaSessions[session.id]) { + this._mediaSessions[session.id] = {}; + } + + this._mediaSessions[session.id] = session; + let sessionId = session.id; + + return Promise.resolve({answer, sessionId}); + } + + async unpublish (userId, mediaId) { + try { + const user = this.getUserMCS(userId); + const answer = await user.unpublish(mediaId); + this._mediaSessions[mediaId] = null; + return Promise.resolve(answer); + } + catch (err) { + return Promise.reject(new Error(err)); + } + } + + async unsubscribe (userId, mediaId) { + try { + const user = this.getUserMCS(userId); + const answer = await user.unsubscribe(mediaId); + return Promise.resolve(); + this._mediaSessions[mediaId] = null; + } + catch (err) { + return Promise.reject(new Error(err)); + } + } + + async addIceCandidate (mediaId, candidate) { + let session = this._mediaSessions[mediaId]; + if (typeof session === 'undefined') { + return Promise.reject(new Error(" [mcs] Media session " + mediaId + " was not found")); + } + try { + console.log(" [mcs] Adding ICE candidate for => " + mediaId); + const ack = await session.addIceCandidate(candidate); + return Promise.resolve(ack); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + /** + * Creates an empty {Room} room and indexes it + * @param {String} roomId + */ + async createRoomMCS (roomId) { + let self = this; + + console.log(" [media] Creating new room with ID " + roomId); + + if(!self._rooms[roomId]) { + self._rooms[roomId] = new Room(roomId); + } + + return Promise.resolve(self._rooms[roomId]); + } + + /** + * Creates an {User} of type @type + * @param {String} roomId + */ + createUserMCS (roomId, type, params) { + let self = this; + let user; + console.log(" [media] Creating a new user[" + type + "]"); + + switch (type) { + case C.USERS.SFU: + user = new SfuUser(roomId, type, this.emitter, params.userAgentString, params.sdp); + break; + case C.USERS.MCU: + console.log(" [media] createUserMCS MCU TODO"); + break; + default: + console.log(" [controller] Unrecognized user type"); + } + + if(!self._users[user.id]) { + self._users[user.id] = user; + } + + return Promise.resolve(user); + } + + getUserMCS (userId) { + return this._users[userId]; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js new file mode 100644 index 0000000000000000000000000000000000000000..7ba91f197ee19ae540f2eba3ef64ccd6be02b5b9 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js @@ -0,0 +1,272 @@ +'use strict' + +const C = require('../constants/Constants.js'); +const config = require('config'); +const mediaServerClient = require('kurento-client'); +const util = require('util'); +const EventEmitter = require('events').EventEmitter; + +let instance = null; + +/* Public members */ +module.exports = class MediaServer extends EventEmitter { + constructor(serverUri) { + if(!instance){ + super(); + this._serverUri = serverUri; + this._mediaPipelines = {}; + this._mediaElements= {}; + this._mediaServer; + instance = this; + } + + return instance; + } + + async init () { + if (typeof this._mediaServer === 'undefined' || !this._mediaServer) { + this._mediaServer = await this._getMediaServerClient(this._serverUri); + } + } + + _getMediaServerClient (serverUri) { + return new Promise((resolve, reject) => { + mediaServerClient(serverUri, (error, client) => { + if (error) { + reject(error); + } + console.log(" [media] Retrieved media server client => " + client); + resolve(client); + }); + }); + } + + _getMediaPipeline (conference) { + return new Promise((resolve, reject) => { + if (this._mediaPipelines[conference]) { + console.log(' [media] Pipeline already exists. ' + JSON.stringify(this._mediaPipelines, null, 2)); + resolve(this._mediaPipelines[conference]); + } + else { + this._mediaServer.create('MediaPipeline', (error, pipeline) => { + if (error) { + console.log(error); + reject(error); + } + this._mediaPipelines[conference] = pipeline; + resolve(pipeline); + }); + } + }); + } + + _releasePipeline (pipelineId) { + let mediaPipeline = this._mediaPipelines[pipelineId]; + + if (typeof mediaPipeline !== 'undefined' && typeof mediaPipeline.release === 'function') { + mediaElement.release(); + } + } + + _createElement (pipeline, type) { + return new Promise((resolve, reject) => { + pipeline.create(type, (error, mediaElement) => { + if (error) { + return reject(error); + } + console.log(" [MediaController] Created [" + type + "] media element: " + mediaElement.id); + this._mediaElements[mediaElement.id] = mediaElement; + return resolve(mediaElement); + }); + }); + } + + + async createMediaElement (conference, type) { + try { + const pipeline = await this._getMediaPipeline(conference); + const mediaElement = await this._createElement(pipeline, type); + return Promise.resolve(mediaElement.id); + } + catch (err) { + return Promise.reject(new Error(err)); + } + } + + async connect (sourceId, sinkId, type) { + let source = this._mediaElements[sourceId]; + let sink = this._mediaElements[sinkId]; + + if (source && sink) { + return new Promise((resolve, reject) => { + switch (type) { + case 'ALL': + source.connect(sink, (error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + break; + + + case 'AUDIO': + case 'VIDEO': + source.connect(sink, (error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + break; + + default: return reject("[mcs] Invalid connect type"); + } + }); + } + else { + return Promise.reject("Failed to connect " + type + ": " + sourceId + " to " + sinkId); + } + } + + stop (elementId) { + let mediaElement = this._mediaElements[elementId]; + // TODO remove event listeners + if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') { + mediaElement.release(); + } + } + + + addIceCandidate (elementId, candidate) { + let mediaElement = this._mediaElements[elementId]; + let kurentoCandidate = mediaServerClient.getComplexType('IceCandidate')(candidate); + + if (typeof mediaElement !== 'undefined' && typeof mediaElement.addIceCandidate === 'function' && + typeof candidate !== 'undefined') { + mediaElement.addIceCandidate(candidate); + console.log(" [media] Added ICE candidate for => " + elementId); + return Promise.resolve(); + } + else { + return Promise.reject(new Error("Candidate could not be parsed or media element does not exist")); + } + } + + gatherCandidates (elementId) { + console.log(' [media] Gathering ICE candidates for ' + elementId); + let mediaElement = this._mediaElements[elementId]; + + return new Promise((resolve, reject) => { + if (typeof mediaElement !== 'undefined' && typeof mediaElement.gatherCandidates === 'function') { + mediaElement.gatherCandidates((error) => { + if (error) { + return reject(new Error(error)); + } + console.log(' [media] Triggered ICE gathering for ' + elementId); + return resolve(); + }); + } + else { + return reject(" [MediaController/gatherCandidates] There is no element " + elementId); + } + }); + } + + setInputBandwidth (elementId, min, max) { + let mediaElement = this._mediaElements[elementId]; + + if (typeof mediaElement !== 'undefined') { + endpoint.setMinVideoRecvBandwidth(min); + endpoint.setMaxVideoRecvBandwidth(max); + } else { + return (" [MediaController/setInputBandwidth] There is no element " + elementId); + } + } + + setOutputBandwidth (endpoint, min, max) { + let mediaElement = this._mediaElements[elementId]; + + if (typeof mediaElement !== 'undefined') { + endpoint.setMinVideoSendBandwidth(min); + endpoint.setMaxVideoSendBandwidth(max); + } else { + return (" [MediaController/setOutputBandwidth] There is no element " + elementId ); + } + } + + setOutputBitrate (endpoint, min, max) { + let mediaElement = this._mediaElements[elementId]; + + if (typeof mediaElement !== 'undefined') { + endpoint.setMinOutputBitrate(min); + endpoint.setMaxOutputBitrate(max); + } else { + return (" [MediaController/setOutputBitrate] There is no element " + elementId); + } + } + + processOffer (elementId, sdpOffer) { + let mediaElement = this._mediaElements[elementId]; + + return new Promise((resolve, reject) => { + if (typeof mediaElement !== 'undefined' && typeof mediaElement.processOffer === 'function') { + mediaElement.processOffer(sdpOffer, (error, answer) => { + if (error) { + return reject(error); + } + return resolve(answer); + }); + } + else { + return reject(" [MediaController/processOffer] There is no element " + elementId); + } + }); + } + + trackMediaState (elementId, type) { + switch (type) { + case C.MEDIA_TYPE.URI: + this.addMediaEventListener(C.EVENT.MEDIA_STATE.ENDOFSTREAM, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.CHANGED, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_IN, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_OUT, elementId); + break; + + case C.MEDIA_TYPE.WEBRTC: + this.addMediaEventListener(C.EVENT.MEDIA_STATE.CHANGED, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_IN, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_OUT, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.ICE, elementId); + break; + + case C.MEDIA_TYPE.RTP: + this.addMediaEventListener(C.EVENT.MEDIA_STATE.CHANGED, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_IN, elementId); + this.addMediaEventListener(C.EVENT.MEDIA_STATE.FLOW_OUT, elementId); + break; + + default: return; + } + return; + } + + addMediaEventListener (eventTag, elementId) { + let mediaElement = this._mediaElements[elementId]; + // TODO event type validator + if (typeof mediaElement !== 'undefined' && mediaElement) { + console.log(' [media] Adding media state listener [' + eventTag + '] for ' + elementId); + mediaElement.on(eventTag, (event) => { + if (eventTag === C.EVENT.MEDIA_STATE.ICE) { + console.log(" [media] Relaying ICE for MediaState" + elementId); + event.candidate = mediaServerClient.getComplexType('IceCandidate')(event.candidate); + } + this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+elementId , {eventTag, event}); + }); + } + } + + notifyMediaState (elementId, eventTag, event) { + this.emit(C.MEDIA_STATE.MEDIA_EVENT , {elementId, eventTag, event}); + } +}; diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js new file mode 100644 index 0000000000000000000000000000000000000000..8bcc8a7d8e61b170c0a74315504f3b9ef64490c8 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js @@ -0,0 +1,43 @@ +/** + * @classdesc + * Model class for rooms + */ + +'use strict' + +module.exports = class Room { + constructor(id) { + this._id = id; + this._users = {}; + this._mcuUsers = {}; + } + + getUser (id) { + return this._users[id]; + } + + getMcuUser (id) { + return this._mcuUsers[id]; + } + + setUser (user) { + if (typeof this._users[user.id] == 'undefined' || + !this._users[user.id]) { + this._users[user.id] = {}; + } + this._users[user.id] = user; + } + + destroyUser(user) { + let _user = this._users[user.id]; + _user.destroy(); + delete this._users[user.id]; + } + + destroyMcuUser (user) { + let _user = this._mcuUsers[user.id]; + _user.destroy(); + delete this._mcuUsers[user.id]; + } + +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js new file mode 100644 index 0000000000000000000000000000000000000000..22ba6c247ecb14800124c3b3d62cbd208440d853 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js @@ -0,0 +1,117 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const C = require('../constants/Constants'); +const SdpWrapper = require('../utils/SdpWrapper'); +const uuidv4 = require('uuid/v4'); +const EventEmitter = require('events').EventEmitter; +const MediaServer = require('../media/media-server'); +const config = require('config'); +const kurentoUrl = config.get('kurentoUrl'); + +module.exports = class SdpSession { + constructor(emitter, sdp = null, room, type = 'WebRtcEndpoint') { + this.id = uuidv4(); + this.room = room; + this.emitter = emitter; + this._status = C.STATUS.STOPPED; + this._type = type; + // {SdpWrapper} SdpWrapper + this._sdp; + if (sdp && type) { + this.setSdp(sdp, type); + } + this._MediaServer = new MediaServer(kurentoUrl); + this._mediaElement; + } + + async setSdp (sdp, type) { + this._sdp = new SdpWrapper(sdp, type); + await this._sdp.processSdp(); + } + + async start (sdpId) { + this._status = C.STATUS.STARTING; + try { + const client = await this._MediaServer.init(); + + console.log("[SdpSession] start/cme"); + this._mediaElement = await this._MediaServer.createMediaElement(this.room, this._type); + console.log("[SdpSession] start/po " + this._mediaElement); + + this._MediaServer.trackMediaState(this._mediaElement, this._type); + this._MediaServer.on(C.EVENT.MEDIA_STATE.MEDIA_EVENT+this._mediaElement, (event) => { + setTimeout(() => { + console.log(" [SdpSession] Relaying EVENT MediaState" + this.id); + event.id = this.id; + this.emitter.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+this.id, event); + }, 50); + }); + + const answer = await this._MediaServer.processOffer(this._mediaElement, this._sdp.getMainDescription()); + + if (this._type === 'WebRtcEndpoint') { + this._MediaServer.gatherCandidates(this._mediaElement); + } + + return Promise.resolve(answer); + } + catch (err) { + this.handleError(err); + return Promise.reject(err); + } + } + + // TODO move to parent Session + async stop () { + this._status = C.STATUS.STOPPING; + try { + await this._MediaServer.stop(this.id); + this._status = C.STATUS.STOPPED; + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(err); + } + } + + + // TODO move to parent Session + // TODO handle connection type + async connect (sinkId) { + try { + console.log(" [SdpSession] Connecting " + this._mediaElement + " => " + sinkId); + await this._MediaServer.connect(this._mediaElement, sinkId, 'ALL'); + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + return Promise.reject(err); + } + } + + async addIceCandidate (candidate) { + try { + console.log(" [SdpSession] Adding ICE candidate for => " + this._mediaElement); + await this._MediaServer.addIceCandidate(this._mediaElement, candidate); + Promise.resolve(); + } + catch (err) { + Promise.reject(err); + } + } + + addMediaEventListener (type, mediaId) { + this._MediaServer.addMediaEventListener (type, mediaId); + } + + handleError (err) { + console.log(err); + this._status = C.STATUS.STOPPED; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js new file mode 100644 index 0000000000000000000000000000000000000000..94301309cacedc06c9c4ef260d7ddc8b454401a3 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js @@ -0,0 +1,164 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const User = require('./User'); +const C = require('../constants/Constants'); +const SdpWrapper = require('../utils/SdpWrapper'); +const SdpSession = require('../model/SdpSession'); +const UriSession = require('../model/UriSession'); + +module.exports = class SfuUser extends User { + constructor(_roomId, type, emitter, userAgentString = C.STRING.ANONYMOUS, sdp = null, uri = null) { + super(_roomId); + // {SdpWrapper} SdpWrapper + this._sdp; + // {Object} hasAudio, hasVideo, hasContent + this._mediaSessions = {} + this.userAgentString; + this.emitter = emitter; + if (sdp) { + this.addSdp(sdp); + } + if (uri) { + this.addUri(uri); + } + } + + async addUri (uri, type) { + // TODO switch from type to children UriSessions (RTSP|HTTP|etc) + let session = new UriSession(uri, type); + + if (typeof this._mediaSessions[session.id] == 'undefined' || + !this._mediaSessions[session.id]) { + this._mediaSessions[session.id] = {}; + } + this._mediaSessions[session.id] = session; + try { + await this.startSession(session.id); + Promise.resolve(session.id); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + addSdp (sdp, type) { + // TODO switch from type to children SdpSessions (WebRTC|SDP) + let session = new SdpSession(this.emitter, sdp, this.roomId, type); + this.emitter.emit(C.EVENT.NEW_SESSION+this.id, session.id); + + if (typeof this._mediaSessions[session.id] == 'undefined' || + !this._mediaSessions[session.id]) { + this._mediaSessions[session.id] = {}; + } + this._mediaSessions[session.id] = session; + console.log("[SfuUser] Added SDP " + session.id); + + return session; + } + + async startSession (sessionId) { + console.log("[SfuUser] starting session " + sessionId); + let session = this._mediaSessions[sessionId]; + + try { + const answer = await session.start(); + console.log("WELL"); + console.log(answer); + return Promise.resolve(answer); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + async subscribe (sdp, mediaId) { + let session = await this.addSdp(sdp); + try { + await this.startSession(session.id); + await this.connect(session.id, mediaId); + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async publish (sdp, mediaId) { + let session = await this.addSdp(sdp); + try { + await this.startSession(session.id); + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async unsubscribe (sdp, mediaId) { + try { + await this.stopSession(mediaId); + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async unpublish (sdp, mediaId) { + try { + await this.stopSession(mediaId); + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async stopSession (sdpId) { + let session = this._mediaSessions[sdpId]; + + try { + await session.stop(); + this._mediaSessions[sdpId] = null; + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async connect (sourceId, sinkId) { + let session = this._mediaSessions[sourceId]; + if(session) { + try { + console.log(" [SfuUser] Connecting sessions " + sourceId + "=>" + sinkId); + await session.connect(sinkId); + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + else { + return Promise.reject(new Error(" [SfuUser] Source session " + sourceId + " not found")); + } + } + + handleError (err) { + console.log(err); + this._status = C.STATUS.STOPPED; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js new file mode 100644 index 0000000000000000000000000000000000000000..74b7795bcc8b8c75918b965355f7ba3716985bf9 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js @@ -0,0 +1,73 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const C = require('../constants/Constants'); +const uuidv4 = require('uuid/v4'); +const EventEmitter = require('events').EventEmitter; +const MediaServer = require('../media/media-server'); + +module.exports = class UriSession extends EventEmitter { + constructor(uri = null) { + super(); + this.id = uuidv4(); + this._status = C.STATUS.STOPPED; + this._uri; + if (uri) { + this.setUri(uri); + } + } + + setUri (uri) { + this._uri = uri; + } + + async start () { + this._status = C.STATUS.STARTING; + try { + const mediaElement = await MediaServer.createMediaElement(this.id, C.MEDIA_TYPE.URI); + console.log("start/cme"); + await MediaServer.play(this.id); + this._status = C.STATUS.STARTED; + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + // TODO move to parent Session + async stop () { + this._status = C.STATUS.STOPPING; + try { + await MediaServer.stop(this.id); + this._status = C.STATUS.STOPPED; + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + // TODO move to parent Session + async connect (sinkId) { + try { + await MediaServer.connect(this.id, sinkId); + return Promise.resolve() + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + handleError (err) { + console.log(err); + this._status = C.STATUS.STOPPED; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js new file mode 100644 index 0000000000000000000000000000000000000000..b3d073a33788ec9c3978d23f81ecc737a5ba751d --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js @@ -0,0 +1,18 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const uuidv4 = require('uuid/v4'); +const User = require('./User'); +const C = require('../constants/Constants.js'); + +module.exports = class User { + constructor(roomId, type, userAgentString = C.STRING.ANONYMOUS) { + this.roomId = roomId; + this.id = uuidv4(); + this.userAgentString = userAgentString; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..eea0d58895e7424e145df30a93d7a20fe6db1a29 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js @@ -0,0 +1,256 @@ +/** + * @classdesc + * Utils class for manipulating SDP + */ + +'use strict' + +var config = require('config'); +var transform = require('sdp-transform'); + +module.exports = class SdpWrapper { + constructor(sdp) { + this._plainSdp = sdp; + this._jsonSdp = transform.parse(sdp); + this._mediaLines = {}; + this._mediaCapabilities = {}; + this._profileThreshold = "ffffff"; + } + + setSdp (sdp) { + this._plainSdp = sdp; + this._jsonSdp = transform.parse(sdp); + } + + getPlainSdp () { + return this._plainSdp; + } + + getJsonSdp () { + return this._jsonSdp; + } + + removeFmtp () { + return this._plainSdp.replace(/(a=fmtp:).*/g, ''); + } + + replaceServerIpv4 (ipv4) { + return this._plainSdp.replace(/(IP4\s[0-9.]*)/g, 'IP4 ' + ipv4); + } + + getCallId () { + return this._plainSdp.match(/(call-id|i):\s(.*)/i)[2]; + } + + /** + * Given a SDP, test if there is more than on video description + * @param {string} sdp The Session Descriptor + * @return {boolean} true if there is more than one video description, else false + */ + hasAudio () { + return /(m=audio)/i.test(this._plainSdp); + } + + /** + * Given a SDP, test if there is a video description in it + * @param {string} sdp The Session Descriptor + * @return {boolean} true if there is a video description, else false + */ + hasVideo (sdp) { + return /(m=video)/i.test(sdp); + } + + /** + * Given a SDP, test if there is more than on video description + * @param {string} sdp The Session Descriptor + * @return {boolean} true if there is more than one video description, else false + */ + hasMultipleVideo (sdp) { + return /(m=video)([\s\S]*\1){1,}/i.test(sdp); + } + + /** + * Given a SDP, return its Session Description + * @param {string} sdp The Session Descriptor + * @return {string} Session description (SDP until the first media line) + */ + getSessionDescription (sdp) { + return sdp.match(/[\s\S]+?(?=m=audio|m=video)/i); + } + + removeSessionDescription (sdp) { + return sdp.match(/(?=[\s\S]+?)(m=audio[\s\S]+|m=video[\s\S]+)/i)[1]; + } + + getVideoParameters (sdp) { + var res = transform.parse(sdp); + console.log(" [sdp] getVideoParameters => " + JSON.stringify(res, null, 2)); + var params = {}; + params.fmtp = ""; + params.codecId = 96; + var pt = 0; + for(var ml of res.media) { + if(ml.type == 'video') { + if (typeof ml.fmtp[0] != 'undefined' && ml.fmtp) { + params.codecId = ml.fmtp[0].payload; + params.fmtp = ml.fmtp[0].config; + console.log(" [sdp] getVideoParameters fmtp => " + JSON.stringify(params)); + return params; + } + } + } + return params; + } + + /** + * Given a SDP, return its Content Description + * @param {string} sdp The Session Descriptor + * @return {string} Content Description (SDP after first media description) + */ + getContentDescription (sdp) { + var res = transform.parse(sdp); + res.media = res.media.filter(function (ml) { return ml.type == "video" }); + var mangledSdp = transform.write(res); + if(typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") { + return mangledSdp; + } + else + return sdp; + } + + /** + * Given a SDP, return its first Media Description + * @param {string} sdp The Session Descriptor + * @return {string} Content Description (SDP after first media description) + */ + getAudioDescription (sdp) { + var res = transform.parse(sdp); + res.media = res.media.filter(function (ml) { return ml.type == "audio" }); + // Hack: Some devices (Snom, Pexip) send crypto with RTP/AVP + // That is forbidden according to RFC3711 and FreeSWITCH rebukes it + res = this.removeTransformCrypto(res); + var mangledSdp = transform.write(res); + this.getSessionDescription(mangledSdp); + if(typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") { + return mangledSdp; + } + else { + return sdp; + } + } + + /** + * Given a SDP, return its first Media Description + * @param {string} sdp The Session Descriptor + * @return {string} Content Description (SDP after first media description) + */ + getMainDescription () { + var res = transform.parse(this._plainSdp); + // Filter should also carry && ml.invalid[0].value != 'content:slides'; + // when content is enabled + res.media = res.media.filter(function (ml) { return ml.type == "video"}); //&& ml.invalid[0].value != 'content:slides'}); + var mangledSdp = transform.write(res); + if (typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") { + console.log(" [sdp] MAIN VIDEO SDP => " + mangledSdp); + return mangledSdp; + } + else { + return sdp; + } + } + + /** + * Given a JSON SDP, remove associated crypto 'a=' lines from media lines + * WARNING: HACK MADE FOR FreeSWITCH ~1.4 COMPATIBILITY + * @param {Object} sdp The Session Descriptor JSON + * @return {Object} JSON SDP without crypto lines + */ + removeTransformCrypto (sdp) { + for(var ml of sdp.media) { + delete ml['crypto']; + } + return sdp; + } + + removeHighQualityFmtps (sdp) { + let res = transform.parse(sdp); + let maxProfileLevel = config.get('kurento.maximum_profile_level_hex'); + let pt = 0; + let idx = 0; + for(var ml of res.media) { + if(ml.type == 'video') { + for(var fmtp of ml.fmtp) { + let fmtpConfig = transform.parseParams(fmtp.config); + let profileId = fmtpConfig['profile-level-id']; + if(typeof profileId !== 'undefined' && parseInt(profileId, 16) > parseInt(maxProfileLevel, 16)) { + console.log(" [sdp] Filtering profile " + parseInt(profileId, 16) + ". Higher than max "+ parseInt(maxProfileLevel, 16)); + pt = fmtp.payload; + delete ml.fmtp[idx]; + ml.rtp = ml.rtp.filter((rtp) => { return rtp.payload != pt}); + } + else { + // Remove fmtp further specifications + //let configProfile = "profile-level-id="+profileId; + //fmtp.config = configProfile; + } + idx++; + } + } + } + var mangledSdp = transform.write(res); + return mangledSdp; + } + + async processSdp () { + let description = this._plainSdp; + //if(config.get('kurento.force_low_resolution')) { + // description = this.removeFmtp(description); + //} + + description = description.toString().replace(/telephone-event/, "TELEPHONE-EVENT"); + + this._mediaCapabilities.hasVideo = this.hasVideo(description); + this._mediaCapabilities.hasAudio = this.hasAudio(description); + this._mediaCapabilities.hasContent = this.hasMultipleVideo(description); + this.sdpSessionDescription = this.getSessionDescription(description); + this.audioSdp = this.getAudioDescription(description); + this.mainVideoSdp = this.getMainDescription(description); + //this.mainVideoSdp = this.removeHighQualityFmtps(this.mainVideoSdp); + this.contentVideoSdp = this.getContentDescription(description); + + return; + } + + /* DEVELOPMENT METHODS */ + _disableMedia (sdp) { + return sdp.replace(/(m=application\s)\d*/g, "$10"); + }; + + /** + * Given a SDP, add Floor Control response + * @param {string} sdp The Session Descriptor + * @return {string} A new Session Descriptor with Floor Control + */ + _addFloorControl (sdp) { + return sdp.replace(/a=inactive/i, 'a=sendrecv\r\na=floorctrl:c-only\r\na=setup:active\r\na=connection:new'); + } + + /** + * Given a SDP, add Floor Control response to reinvite + * @param {string} sdp The Session Descriptor + * @return {string} A new Session Descriptor with Floor Control Id + */ + _addFloorId (sdp) { + sdp = sdp.replace(/(a=floorctrl:c-only)/i, '$1\r\na=floorid:1 m-stream:3'); + return sdp.replace(/(m=video.*)([\s\S]*?m=video.*)([\s\S]*)/i, '$1\r\na=content:main\r\na=label:1$2\r\na=content:slides\r\na=label:3$3'); + } + + /** + * Given the string representation of a Session Descriptor, remove it's video + * @param {string} sdp The Session Descriptor + * @return {string} A new Session Descriptor without the video + */ + _removeVideoSdp (sdp) { + return sdp.replace(/(m=video[\s\S]+)/g,''); + }; +}; diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..11b06b0bcf49d721be79203de85fe307962b74a7 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js @@ -0,0 +1,37 @@ +/** + * @classdesc + * Utils class for SDP generation + */ + +module.exports.generateSdp = function(remote_ip_address, remote_video_port) { + return "v=0\r\n" + + "o=- 0 0 IN IP4 " + remote_ip_address + "\r\n" + + "s=No Name\r\n" + + "c=IN IP4 " + remote_ip_address + "\r\n" + + "t=0 0\r\n" + + "m=video " + remote_video_port + " RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=ftmp:96 packetization-mode=0\r\n"; +} + +/** + * Generates a video SDP given the media specs + * @param {string} sourceIpAddress The source IP address of the media + * @param {string} sourceVideoPort The source video port of the media + * @param {string} codecId The ID of the codec + * @param {string} sendReceive The SDP flag of the media flow + * direction, 'sendonly', 'recvonly' or 'sendrecv' + * @param {String} rtpProfile The RTP profile of the RTP Endpoint + * @param {String} codecName The name of the codec used for the RTP + * Endpoint + * @param {String} codecRate The codec rate + * @return {string} The Session Descriptor for the media + */ +module.exports.generateVideoSdp = function (sourceIpAddress, sourceVideoPort, codecId, sendReceive, rtpProfile, codecName, codecRate, fmtp) { + return 'm=video ' + sourceVideoPort + ' ' + rtpProfile + ' ' + codecId + '\r\n' + + 'a=' + sendReceive + '\r\n' + + 'c=IN IP4 ' + sourceIpAddress + '\r\n' + + 'a=rtpmap:' + codecId + ' ' + codecName + '/' + codecRate + '\r\n' + + 'a=fmtp:' + codecId + ' ' + fmtp + '\r\n'; +}; + diff --git a/labs/kurento-screenshare/lib/media-controller.js b/labs/bbb-webrtc-sfu/lib/media-controller.js similarity index 100% rename from labs/kurento-screenshare/lib/media-controller.js rename to labs/bbb-webrtc-sfu/lib/media-controller.js diff --git a/labs/kurento-screenshare/lib/media-handler.js b/labs/bbb-webrtc-sfu/lib/media-handler.js similarity index 100% rename from labs/kurento-screenshare/lib/media-handler.js rename to labs/bbb-webrtc-sfu/lib/media-handler.js diff --git a/labs/kurento-screenshare/lib/ConnectionManager.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js similarity index 70% rename from labs/kurento-screenshare/lib/ConnectionManager.js rename to labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js index 2dbef1e75d32f7759337ef0f56a3b11ced7d4ed6..1748ac0fbfbb4815c9de207616c8a5767e208fdb 100644 --- a/labs/kurento-screenshare/lib/ConnectionManager.js +++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js @@ -5,26 +5,28 @@ * */ -'use strict' +"use strict"; +const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw'); const cookieParser = require('cookie-parser') const express = require('express'); const session = require('express-session') -const wsModule = require('./websocket'); +const wsModule = require('../websocket'); const http = require('http'); const fs = require('fs'); -const BigBlueButtonGW = require('./bbb/pubsub/bbb-gw'); +const MediaController = require('../media-controller'); var Screenshare = require('./screenshare'); -var C = require('./bbb/messages/Constants'); - +var C = require('../bbb/messages/Constants'); // Global variables -module.exports = class ConnectionManager { +module.exports = class ScreenshareManager { constructor (settings, logger) { this._logger = logger; this._clientId = 0; this._app = express(); + + this._sessions = {}; this._screenshareSessions = {}; this._setupExpressSession(); @@ -79,6 +81,7 @@ module.exports = class ConnectionManager { let connectionId; let request = webSocket.upgradeReq; let sessionId; + let callerName; let response = { writeHead : {} }; @@ -95,7 +98,16 @@ module.exports = class ConnectionManager { webSocket.on('close', function() { console.log('Connection ' + connectionId + ' closed'); - self._stopSession(sessionId); + console.log(webSocket.presenter); + + if (webSocket.presenter && self._screenshareSessions[sessionId]) { // if presenter // FIXME (this conditional was added to prevent screenshare stop when an iOS user quits) + console.log(" [CM] Stopping presenter " + sessionId); + self._stopSession(sessionId); + } + if (webSocket.viewer && typeof webSocket.session !== 'undefined') { + console.log(" [CM] Stopping viewer " + webSocket.viewerId); + webSocket.session._stopViewer(webSocket.viewerId); + } }); webSocket.on('message', function(_message) { @@ -103,9 +115,9 @@ module.exports = class ConnectionManager { let session; // The sessionId is voiceBridge for screensharing sessions sessionId = message.voiceBridge; - if(self._screenshareSessions[sessionId]) { session = self._screenshareSessions[sessionId]; + webSocket.session = session; } switch (message.id) { @@ -114,7 +126,14 @@ module.exports = class ConnectionManager { // Checking if there's already a Screenshare session started // because we shouldn't overwrite it + webSocket.presenter = true; + if (!self._screenshareSessions[message.voiceBridge]) { + self._screenshareSessions[message.voiceBridge] = {} + self._screenshareSessions[message.voiceBridge] = session; + } + + //session.on('message', self._assembleSessionMessage.bind(self)); if(session) { break; } @@ -147,11 +166,23 @@ module.exports = class ConnectionManager { break; case 'viewer': - console.log('Viewer message => [' + message.id + '] connection [' + connectionId + '][' + message.presenterId + '][' + message.sessionId + '][' + message.callerName + ']'); - + console.log("[viewer] Session output \n " + session); + + webSocket.viewer = true; + webSocket.viewerId = message.callerName; + + if (message.sdpOffer && message.voiceBridge) { + if (session) { + session._startViewer(webSocket, message.voiceBridge, message.sdpOffer, message.callerName, self._screenshareSessions[message.voiceBridge]._presenterEndpoint); + } else { + webSocket.sendMessage("voiceBridge not recognized"); + webSocket.sendMessage(Object.keys(self._screenshareSessions)); + webSocket.sendMessage(message.voiceBridge); + } + } break; - case 'stop': + case 'stop': console.log('[' + message.id + '] connection ' + connectionId); if (session) { @@ -163,6 +194,7 @@ module.exports = class ConnectionManager { case 'onIceCandidate': if (session) { + console.log(" [CM] What the fluff is happening"); session._onIceCandidate(message.candidate); } else { console.log(" [iceCandidate] Why is there no session on ICE CANDIDATE?"); @@ -176,6 +208,16 @@ module.exports = class ConnectionManager { })); break; + + case 'viewerIceCandidate': + console.log("[viewerIceCandidate] Session output => " + session); + if (session) { + session._onViewerIceCandidate(message.candidate, message.callerName); + } else { + console.log("[iceCandidate] Why is there no session on ICE CANDIDATE?"); + } + break; + default: webSocket.sendMessage({ id : 'error', message : 'Invalid message ' + message }); break; @@ -203,4 +245,4 @@ module.exports = class ConnectionManager { setTimeout(process.exit, 1000); } -} +}; diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..e8755e9024cb276cb52ecc0eacdae81e711f094c --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js @@ -0,0 +1,12 @@ +const ScreenshareManager = require('./ScreenshareManager'); + +process.on('uncaughtException', function (error) { + console.log(error.stack); +}); + +process.on('disconnect',function() { + console.log("Parent exited!"); + process.kill(); +}); + +c = new ScreenshareManager(); diff --git a/labs/kurento-screenshare/lib/screenshare.js b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js similarity index 66% rename from labs/kurento-screenshare/lib/screenshare.js rename to labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js index 411fbd54a9ac3cf994f7e0f703ed7c3f32f8718e..75cfccf742da95673e571168d7fba305d29669ba 100644 --- a/labs/kurento-screenshare/lib/screenshare.js +++ b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js @@ -8,13 +8,13 @@ 'use strict' // Imports -const C = require('./bbb/messages/Constants'); -const MediaHandler = require('./media-handler'); -const Messaging = require('./bbb/messages/Messaging'); +const C = require('../bbb/messages/Constants'); +const MediaHandler = require('../media-handler'); +const Messaging = require('../bbb/messages/Messaging'); const moment = require('moment'); -const h264_sdp = require('./h264-sdp'); +const h264_sdp = require('../h264-sdp'); const now = moment(); -const MediaController = require('./media-controller'); +const MediaController = require('../media-controller'); // Global stuff var sharedScreens = {}; @@ -44,6 +44,8 @@ module.exports = class Screenshare { this._vw = vw; this._vh = vh; this._candidatesQueue = []; + this._viewersEndpoint = []; + this._viewersCandidatesQueue = []; } // TODO isolate ICE @@ -51,12 +53,93 @@ module.exports = class Screenshare { let candidate = kurento.getComplexType('IceCandidate')(_candidate); if (this._presenterEndpoint) { + console.log(" [screenshare] Adding ICE candidate to presenter"); this._presenterEndpoint.addIceCandidate(candidate); } else { this._candidatesQueue.push(candidate); } }; + + _onViewerIceCandidate(_candidate, callerName) { + console.log("onviewericecandidate callerName = " + callerName); + let candidate = kurento.getComplexType('IceCandidate')(_candidate); + + if (this._viewersEndpoint[callerName]) { + this._viewersEndpoint[callerName].addIceCandidate(candidate); + } + else { + if (!this._viewersCandidatesQueue[callerName]) { + this._viewersCandidatesQueue[callerName] = []; + } + this._viewersCandidatesQueue[callerName].push(candidate); + } + } + + _startViewer(ws, voiceBridge, sdp, callerName, presenterEndpoint, callback) { + let self = this; + let _callback = function(){}; + console.log("startviewer callerName = " + callerName); + self._viewersCandidatesQueue[callerName] = []; + + console.log("VIEWER VOICEBRIDGE: "+self._voiceBridge); + + MediaController.createMediaElement(voiceBridge, C.WebRTC, function(error, webRtcEndpoint) { + if (error) { + console.log("Media elements error" + error); + return _callback(error); + } + + self._viewersEndpoint[callerName] = webRtcEndpoint; + + // QUEUES UP ICE CANDIDATES IF NEGOTIATION IS NOT YET READY + while(self._viewersCandidatesQueue[callerName].length) { + let candidate = self._viewersCandidatesQueue[callerName].shift(); + MediaController.addIceCandidate(self._viewersEndpoint[callerName].id, candidate); + } + // CONNECTS TWO MEDIA ELEMENTS + MediaController.connectMediaElements(presenterEndpoint.id, self._viewersEndpoint[callerName].id, C.VIDEO, function(error) { + if (error) { + console.log("Media elements CONNECT error " + error); + //pipeline.release(); + return _callback(error); + } + }); + + // ICE NEGOTIATION WITH THE ENDPOINT + self._viewersEndpoint[callerName].on('OnIceCandidate', function(event) { + let candidate = kurento.getComplexType('IceCandidate')(event.candidate); ws.sendMessage({ id : 'iceCandidate', candidate : candidate }); + }); + + sdp = h264_sdp.transform(sdp); + // PROCESS A SDP OFFER + MediaController.processOffer(webRtcEndpoint.id, sdp, function(error, webRtcSdpAnswer) { + if (error) { + console.log(" [webrtc] processOffer error => " + error + " for SDP " + sdp); + //pipeline.release(); + return _callback(error); + } + ws.sendMessage({id: "viewerResponse", sdpAnswer: webRtcSdpAnswer, response: "accepted"}); + console.log(" Sent sdp message to client with callerName:" + callerName); + + MediaController.gatherCandidates(webRtcEndpoint.id, function(error) { + if (error) { + return _callback(error); + } + + self._viewersEndpoint[callerName].on('MediaFlowInStateChange', function(event) { + if (event.state === 'NOT_FLOWING') { + console.log(" NOT FLOWING "); + } + else if (event.state === 'FLOWING') { + console.log(" FLOWING "); + } + }); + }); + }); + }); + } + _startPresenter(id, ws, sdpOffer, callback) { let self = this; @@ -65,6 +148,7 @@ module.exports = class Screenshare { // Force H264 on Firefox and Chrome sdpOffer = h264_sdp.transform(sdpOffer); console.log("Starting presenter for " + sdpOffer); + console.log("PRESENTER VOICEBRIDGE: " + self._voiceBridge); MediaController.createMediaElement(self._voiceBridge, C.WebRTC, function(error, webRtcEndpoint) { if (error) { console.log("Media elements error" + error); @@ -160,7 +244,6 @@ module.exports = class Screenshare { } else { console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE"); } - if (this._ffmpegRtpEndpoint) { MediaController.releaseMediaElement(this._ffmpegRtpEndpoint.id); this._ffmpegRtpEndpoint = null; @@ -196,6 +279,7 @@ module.exports = class Screenshare { } _onRtpMediaFlowing(meetingId, rtpParams) { + console.log(" [screenshare] Media FLOWING for meeting => " + meetingId); let self = this; let strm = Messaging.generateStartTranscoderRequestMessage(meetingId, meetingId, rtpParams); @@ -218,7 +302,8 @@ module.exports = class Screenshare { }; _stopRtmpBroadcast (meetingId) { - var self = this; + console.log(" [screenshare] _stopRtmpBroadcast for meeting => " + meetingId); + let self = this; if(self._meetingId === meetingId) { // TODO correctly assemble this timestamp let timestamp = now.format('hhmmss'); @@ -229,6 +314,7 @@ module.exports = class Screenshare { } _startRtmpBroadcast (meetingId, output) { + console.log(" [screenshare] _startRtmpBroadcast for meeting => " + meetingId); var self = this; if(self._meetingId === meetingId) { // TODO correctly assemble this timestamp @@ -245,5 +331,17 @@ module.exports = class Screenshare { console.log(" [screenshare] TODO RTP NOT_FLOWING"); }; + _stopViewer(id) { + let viewer = this._viewersEndpoint[id]; + console.log(' [stop] Releasing endpoints for ' + id); + if (viewer) { + MediaController.releaseMediaElement(viewer.id); + this._viewersEndpoint[viewer.id] = null; + } else { + console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE"); + } + + delete this._viewersCandidatesQueue[id]; + }; }; diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoManager.js b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js new file mode 100755 index 0000000000000000000000000000000000000000..5bd40e44e9ca44704d12588ab9ba01a97a4bcc44 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js @@ -0,0 +1,159 @@ +/* + * Lucas Fialho Zawacki + * (C) Copyright 2017 Bigbluebutton + * + */ + +var cookieParser = require('cookie-parser') +var express = require('express'); +var session = require('express-session') +var ws = require('./websocket'); +var http = require('http'); +var fs = require('fs'); + +var Video = require('./video'); + +// Global variables +var app = express(); +var sessions = {}; + +/* + * Management of sessions + */ +app.use(cookieParser()); + +var sessionHandler = session({ + secret : 'Shawarma', rolling : true, resave : true, saveUninitialized : true +}); + +app.use(sessionHandler); + +/* + * Server startup + */ +var server = http.createServer(app).listen(3002, function() { + console.log(' [*] Running bbb-html5 kurento video service.'); +}); + +var wss = new ws.Server({ + server : server, + path : '/html5video' +}); + +var clientId = 0; + +wss.on('connection', function(ws) { + var sessionId; + var request = ws.upgradeReq; + var response = { + writeHead : {} + }; + + sessionHandler(request, response, function(err) { + sessionId = request.session.id + "_" + clientId++; + + if (!sessions[sessionId]) { + sessions[sessionId] = {}; + } + + console.log('Connection received with sessionId ' + sessionId); + }); + + ws.on('error', function(error) { + console.log('Connection ' + sessionId + ' error'); + // stop(sessionId); + }); + + ws.on('close', function() { + console.log('Connection ' + sessionId + ' closed'); + stopSession(sessionId); + }); + + ws.on('message', function(_message) { + var message = JSON.parse(_message); + + var video; + if (message.cameraId && sessions[sessionId][message.cameraId]) { + video = sessions[sessionId][message.cameraId]; + } + + switch (message.id) { + + case 'start': + + console.log('[' + message.id + '] connection ' + sessionId); + + var video = new Video(ws, message.cameraId, message.cameraShared); + sessions[sessionId][message.cameraId] = video; + + video.start(message.sdpOffer, function(error, sdpAnswer) { + if (error) { + return ws.sendMessage({id : 'error', message : error }); + } + + ws.sendMessage({id : 'startResponse', cameraId: message.cameraId, sdpAnswer : sdpAnswer}); + }); + + break; + + case 'stop': + + console.log('[' + message.id + '] connection ' + sessionId); + + if (video) { + video.stop(sessionId); + } else { + console.log(" [stop] Why is there no video on STOP?"); + } + break; + + case 'onIceCandidate': + + if (video) { + video.onIceCandidate(message.candidate); + } else { + console.log(" [iceCandidate] Why is there no video on ICE CANDIDATE?"); + } + break; + + default: + ws.sendMessage({ id : 'error', message : 'Invalid message ' + message }); + break; + } + + }); +}); + +var stopSession = function(sessionId) { + + console.log(' [>] Stopping session ' + sessionId); + + var videoIds = Object.keys(sessions[sessionId]); + + for (var i = 0; i < videoIds.length; i++) { + + var video = sessions[sessionId][videoIds[i]]; + video.stop(); + + delete sessions[sessionId][videoIds[i]]; + } + + delete sessions[sessionId]; +} + +var stopAll = function() { + + console.log('\n [x] Stopping everything! '); + + var sessionIds = Object.keys(sessions); + + for (var i = 0; i < sessionIds.length; i++) { + + stopSession(sessionIds[i]); + } + + setTimeout(process.exit, 1000); +} + +process.on('SIGTERM', stopAll); +process.on('SIGINT', stopAll); diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..af1f3b35daa3ad3748175c80254626e2bfe6f113 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js @@ -0,0 +1,10 @@ +const VideoManager = require('./VideoManager'); + +process.on('uncaughtException', function (error) { + console.log(error.stack); +}); + +process.on('disconnect',function() { + console.log("Parent exited!"); + process.kill(); +}); diff --git a/labs/bbb-webrtc-sfu/lib/video/video.js b/labs/bbb-webrtc-sfu/lib/video/video.js new file mode 100644 index 0000000000000000000000000000000000000000..f1544911343e948aad620811b224e40bcf57cf48 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/video/video.js @@ -0,0 +1,152 @@ +'use strict'; +// Global stuff +var sharedWebcams = {}; + +const kurento = require('kurento-client'); +const config = require('config'); +const kurentoUrl = config.get('kurentoUrl'); +const MCSApi = require('../mcs-core/lib/media/MCSApiStub'); + +if (config.get('acceptSelfSignedCertificate')) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED=0; +} + +module.exports = class Video { + constructor(_ws, _id, _shared) { + this.mcs = new MCSApi(); + this.ws = _ws; + this.id = _id; + this.meetingId = _id; + this.shared = _shared; + this.webRtcEndpoint = null; + this.mediaId = null; + + this.candidatesQueue = []; + } + + onIceCandidate (_candidate) { + if (this.mediaId) { + try { + this.flushCandidatesQueue(); + this.mcs.addIceCandidate(this.mediaId, _candidate); + } + catch (err) { + console.log(err); + } + } + else { + this.candidatesQueue.push(_candidate); + } + }; + + flushCandidatesQueue () { + if (this.mediaId) { + try { + while(this.candidatesQueue.length) { + let candidate = this.candidatesQueue.shift(); + this.mcs.addIceCandidate(this.mediaId, candidate); + } + } + catch (err) { + console.log(err); + } + } + } + + mediaState (event) { + let msEvent = event.event; + + switch (event.eventTag) { + + case "OnIceCandidate": + console.log(" [video] Sending ICE candidate to user => " + this.id); + let candidate = msEvent.candidate; + this.ws.sendMessage({ id : 'iceCandidate', cameraId: this.id, candidate : candidate }); + break; + + case "MediaStateChanged": + break; + + case "MediaFlowOutStateChange": + case "MediaFlowInStateChange": + console.log(' [video] ' + msEvent.type + '[' + msEvent.state + ']' + ' for endpoint ' + this.id); + + if (msEvent.state === 'NOT_FLOWING') { + this.ws.sendMessage({ id : 'playStop', cameraId : this.id }); + } + else if (msEvent.state === 'FLOWING') { + this.ws.sendMessage({ id : 'playStart', cameraId : this.id }); + } + + break; + + default: console.log(" [video] Unrecognized event"); + } + } + + async start (sdpOffer, callback) { + console.log(" [video] start"); + let sdpAnswer; + + try { + this.userId = await this.mcs.join(this.meetingId, 'SFU', {}); + console.log(" [video] Join returned => " + this.userId); + + if (this.shared) { + const ret = await this.mcs.publish(this.userId, this.meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer}); + this.mediaId = ret.sessionId; + sharedWebcams[this.id] = this.mediaId; + sdpAnswer = ret.answer; + this.flushCandidatesQueue(); + this.mcs.on('MediaEvent' + this.mediaId, this.mediaState.bind(this)); + + console.log(" [video] Publish returned => " + this.mediaId); + + return callback(null, sdpAnswer); + } + else { + const ret = await this.mcs.subscribe(this.userId, 'WebRtcEndpoint', sharedWebcams[this.id], {descriptor: sdpOffer}); + + this.mediaId = ret.sessionId; + sdpAnswer = ret.answer; + this.flushCandidatesQueue(); + this.mcs.on('MediaEvent' + this.mediaId, this.mediaState.bind(this)); + + console.log(" [video] Subscribe returned => " + this.mediaId); + + return callback(null, sdpAnswer); + } + } + catch (err) { + console.log(" [video] MCS returned error => " + err); + return callback(err); + } + }; + + stop () { + + //console.log(' [stop] Releasing webrtc endpoint for ' + id); + + //if (webRtcEndpoint) { + // webRtcEndpoint.release(); + // webRtcEndpoint = null; + //} else { + // console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE"); + //} + + //if (shared) { + // console.log(' [stop] Webcam is shared, releasing ' + id); + + // if (mediaPipelines[id]) { + // mediaPipelines[id].release(); + // } else { + // console.log(" [mediaPipeline] PLEASE DONT TRY STOPPING THINGS TWICE"); + // } + + // delete mediaPipelines[id]; + // delete sharedWebcams[id]; + //} + + //delete this.candidatesQueue; + }; +}; diff --git a/labs/kurento-screenshare/lib/websocket.js b/labs/bbb-webrtc-sfu/lib/video/websocket.js similarity index 100% rename from labs/kurento-screenshare/lib/websocket.js rename to labs/bbb-webrtc-sfu/lib/video/websocket.js diff --git a/labs/bbb-webrtc-sfu/lib/websocket.js b/labs/bbb-webrtc-sfu/lib/websocket.js new file mode 100644 index 0000000000000000000000000000000000000000..c4fe9f6f18b220e8b4c43be58ec75004a6430124 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/websocket.js @@ -0,0 +1,18 @@ +/* + * Simple wrapper around the ws library + * + */ + +var ws = require('ws'); + +ws.prototype.sendMessage = function(json) { + + return this.send(JSON.stringify(json), function(error) { + if(error) + console.log(' [server] Websocket error "' + error + '" on message "' + json.id + '"'); + }); + +}; + + +module.exports = ws; \ No newline at end of file diff --git a/labs/bbb-webrtc-sfu/package-lock.json b/labs/bbb-webrtc-sfu/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..a6f53daa3c4629ec0783a636917c82c37edaad1c --- /dev/null +++ b/labs/bbb-webrtc-sfu/package-lock.json @@ -0,0 +1,689 @@ +{ + "name": "bbb-screenshare-video-kurento-bridge", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz", + "integrity": "sha1-5fHzkoxtlf2WVYw27D2dDeSm7Oo=", + "requires": { + "mime-types": "2.1.17", + "negotiator": "0.5.3" + } + }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "async": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.0.1.tgz", + "integrity": "sha1-twnMAoCpw28J9FNr6CPIOKkEniU=", + "requires": { + "lodash": "4.17.4" + } + }, + "backoff": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.3.0.tgz", + "integrity": "sha1-7nx+OAk/kuRyhZ22NedlJFT8Ieo=" + }, + "base64-url": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-url/-/base64-url-1.2.1.tgz", + "integrity": "sha1-GZ/WYXAqDnt9yubgaYuwicUvbXg=" + }, + "bindings": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", + "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" + }, + "bufferutil": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-1.2.1.tgz", + "integrity": "sha1-N75dNuHgZJIiHmjUdLGsWOUQy9c=", + "requires": { + "bindings": "1.2.1", + "nan": "2.7.0" + } + }, + "commander": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz", + "integrity": "sha1-0SG7roYNmZKj1Re6lvVliOR8Z4E=" + }, + "config": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/config/-/config-1.28.1.tgz", + "integrity": "sha1-diXSoeTJDxMdinM0eYLZPDhzKC0=", + "dev": true, + "requires": { + "json5": "0.4.0", + "os-homedir": "1.0.2" + } + }, + "content-disposition": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz", + "integrity": "sha1-QoT+auBjCHRjnkToCkGMKTQTXp4=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "crc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.2.1.tgz", + "integrity": "sha1-XZyPt3okXNXsopHl0tAFM0urAII=" + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "depd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.0.1.tgz", + "integrity": "sha1-gK7GTJ1tl+ZcwqnKqTwKpqv3Oqo=" + }, + "destroy": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz", + "integrity": "sha1-tDO0ck5x/YVR2YhRdIUcX8N34sk=" + }, + "double-ended-queue": { + "version": "2.1.0-0", + "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", + "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" + }, + "ee-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.0.tgz", + "integrity": "sha1-ag18YiHkkP7v2S7D9EHJzozQl/Q=" + }, + "error-tojson": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/error-tojson/-/error-tojson-0.0.1.tgz", + "integrity": "sha1-p7GqlP/ADpB4wuuibiBL2Hzyy7k=" + }, + "es6-promise": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz", + "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng==" + }, + "escape-html": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.1.tgz", + "integrity": "sha1-GBoobq05ejmpKFfPsdQwUuNWv/A=" + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "etag": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.6.0.tgz", + "integrity": "sha1-i8ssavElTEgd/IuZfJBu9ORCwgc=", + "requires": { + "crc": "3.2.1" + } + }, + "express": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.12.4.tgz", + "integrity": "sha1-j+wlECVbxrLlgQfEgjnA+jB8GqI=", + "requires": { + "accepts": "1.2.13", + "content-disposition": "0.5.0", + "content-type": "1.0.4", + "cookie": "0.1.2", + "cookie-signature": "1.0.6", + "debug": "2.2.0", + "depd": "1.0.1", + "escape-html": "1.0.1", + "etag": "1.6.0", + "finalhandler": "0.3.6", + "fresh": "0.2.4", + "merge-descriptors": "1.0.0", + "methods": "1.1.2", + "on-finished": "2.2.1", + "parseurl": "1.3.2", + "path-to-regexp": "0.1.3", + "proxy-addr": "1.0.10", + "qs": "2.4.2", + "range-parser": "1.0.3", + "send": "0.12.3", + "serve-static": "1.9.3", + "type-is": "1.6.15", + "utils-merge": "1.0.0", + "vary": "1.0.1" + }, + "dependencies": { + "cookie": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", + "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=" + } + } + }, + "express-session": { + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.10.4.tgz", + "integrity": "sha1-BOHZLgBZOJPh92Vp6zrWMRPa+Uw=", + "requires": { + "cookie": "0.1.2", + "cookie-signature": "1.0.6", + "crc": "3.2.1", + "debug": "2.1.3", + "depd": "1.0.1", + "on-headers": "1.0.1", + "parseurl": "1.3.2", + "uid-safe": "1.1.0", + "utils-merge": "1.0.0" + }, + "dependencies": { + "cookie": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.2.tgz", + "integrity": "sha1-cv7D0k5Io0Mgc9kMEmQgBQYQBLE=" + }, + "debug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz", + "integrity": "sha1-zoqxte6PvuK/o7Yzyrk9NmtjQY4=", + "requires": { + "ms": "0.7.0" + } + }, + "ms": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.0.tgz", + "integrity": "sha1-hlvpTC5zl62KV9pqYzpuLzB5i4M=" + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "finalhandler": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.3.6.tgz", + "integrity": "sha1-2vnEFhsbBuABRmsUEd/baXO+E4s=", + "requires": { + "debug": "2.2.0", + "escape-html": "1.0.1", + "on-finished": "2.2.1" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.2.4.tgz", + "integrity": "sha1-NYJJkgbJcjcUGQ7ddLRgT+tKYUw=" + }, + "hoek": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz", + "integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.5.tgz", + "integrity": "sha1-X6eM8wG4JceKvDBC2BJyMEnqI8c=" + }, + "isbuffer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/isbuffer/-/isbuffer-0.0.0.tgz", + "integrity": "sha1-OMFG2d9Si4v5sHAcPUPPEt8/w5s=" + }, + "isemail": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.0.0.tgz", + "integrity": "sha512-rz0ng/c+fX+zACpLgDB8fnUQ845WSU06f4hlhk4K8TJxmR6f5hyvitu9a9JdMD7aq/P4E0XdG1uaab2OiXgHlA==", + "requires": { + "punycode": "2.1.0" + } + }, + "joi": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.0.1.tgz", + "integrity": "sha512-ChTMfmbIg5yrN9pUdeaLL8vzylMQhUteXiXa1MWINsMUs3jTQ8I87lUZwR5GdfCLJlpK04U7UgrxgmU8Zp7PhQ==", + "requires": { + "hoek": "5.0.2", + "isemail": "3.0.0", + "topo": "3.0.0" + } + }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, + "json5": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", + "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=", + "dev": true + }, + "kurento-client": { + "version": "git+https://github.com/Kurento/kurento-client-js.git#efb160e85a4b1f376307fe1979c9fbcb5f978393", + "requires": { + "async": "2.0.1", + "error-tojson": "0.0.1", + "es6-promise": "4.1.1", + "extend": "3.0.1", + "inherits": "2.0.3", + "kurento-client-core": "github:Kurento/kurento-client-core-js#2160f8e6938f138b52b72a5c5c354d1e5fce1ca0", + "kurento-client-elements": "github:Kurento/kurento-client-elements-js#cbd1ff67fbf0faddc9f6f266bb33e449bc9e1f81", + "kurento-client-filters": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0", + "kurento-jsonrpc": "github:Kurento/kurento-jsonrpc-js#827827bbeb557e1c1901f5a562c4c700b9a51401", + "minimist": "1.2.0", + "promise": "7.1.1", + "promisecallback": "0.0.4", + "reconnect-ws": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a" + } + }, + "kurento-client-core": { + "version": "github:Kurento/kurento-client-core-js#2160f8e6938f138b52b72a5c5c354d1e5fce1ca0" + }, + "kurento-client-elements": { + "version": "github:Kurento/kurento-client-elements-js#cbd1ff67fbf0faddc9f6f266bb33e449bc9e1f81" + }, + "kurento-client-filters": { + "version": "github:Kurento/kurento-client-filters-js#51308da53e432a2db9559dcdb308d87951417bf0" + }, + "kurento-jsonrpc": { + "version": "github:Kurento/kurento-jsonrpc-js#827827bbeb557e1c1901f5a562c4c700b9a51401", + "requires": { + "bufferutil": "1.2.1", + "inherits": "2.0.3", + "utf-8-validate": "1.2.2", + "ws": "1.1.5" + }, + "dependencies": { + "ws": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.1.5.tgz", + "integrity": "sha512-o3KqipXNUdS7wpQzBHSe180lBGO60SoK0yVo3CYJgb2MkobuWuBX6dhkYP5ORCLd55y+SaflMOV5fqAB53ux4w==", + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + } + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz", + "integrity": "sha1-IWnPdTjhsMyH+4jhUC2EdLv3mGQ=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + }, + "mime-types": { + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "requires": { + "mime-db": "1.30.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "moment": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.1.tgz", + "integrity": "sha1-VtoaLRy/AdOLfhr8McELz6GSkWc=" + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, + "native-or-bluebird": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/native-or-bluebird/-/native-or-bluebird-1.1.2.tgz", + "integrity": "sha1-OSHhECMtHreQ89rGG7NwUxx9NW4=" + }, + "negotiator": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz", + "integrity": "sha1-Jp1cR2gQ7JLtvntsLygxY4T5p+g=" + }, + "on-finished": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.2.1.tgz", + "integrity": "sha1-XIXBzDYpn3gCllP2Z/J7a5nrwCk=", + "requires": { + "ee-first": "1.1.0" + } + }, + "on-headers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", + "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" + }, + "options": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=" + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.3.tgz", + "integrity": "sha1-IbmrgidCed4lsVbqCP0SylG4rss=" + }, + "promise": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz", + "integrity": "sha1-SJZUxpJha4qlWwck+oCbt9tJxb8=", + "requires": { + "asap": "2.0.6" + } + }, + "promisecallback": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/promisecallback/-/promisecallback-0.0.4.tgz", + "integrity": "sha1-uTTxPATkQ2IrTWbeTkLqX2zmbnQ=" + }, + "proxy-addr": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.10.tgz", + "integrity": "sha1-DUCoL4Afw1VWfS7LZe/j8HfxIcU=", + "requires": { + "forwarded": "0.1.2", + "ipaddr.js": "1.0.5" + } + }, + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + }, + "qs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.4.2.tgz", + "integrity": "sha1-9854jld33wtQENp/fE5zujJHD1o=" + }, + "range-parser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz", + "integrity": "sha1-aHKCNTXGkuLCoBA4Jq/YLC4P8XU=" + }, + "reconnect-core": { + "version": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734", + "requires": { + "backoff": "2.3.0" + } + }, + "reconnect-ws": { + "version": "github:KurentoForks/reconnect-ws#f287385d75861654528c352e60221f95c9209f8a", + "requires": { + "reconnect-core": "github:KurentoForks/reconnect-core#921d43e91578abb2fb2613f585c010c1939cf734", + "websocket-stream": "0.5.1" + } + }, + "redis": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", + "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", + "requires": { + "double-ended-queue": "2.1.0-0", + "redis-commands": "1.3.1", + "redis-parser": "2.6.0" + } + }, + "redis-commands": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.3.1.tgz", + "integrity": "sha1-gdgm9F+pyLIBH0zXoP5ZfSQdRCs=" + }, + "redis-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", + "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + }, + "sdp-transform": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz", + "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY=" + }, + "send": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/send/-/send-0.12.3.tgz", + "integrity": "sha1-zRLcWP3iHk+RkCs5sv2gWnptm9w=", + "requires": { + "debug": "2.2.0", + "depd": "1.0.1", + "destroy": "1.0.3", + "escape-html": "1.0.1", + "etag": "1.6.0", + "fresh": "0.2.4", + "mime": "1.3.4", + "ms": "0.7.1", + "on-finished": "2.2.1", + "range-parser": "1.0.3" + } + }, + "serve-static": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.9.3.tgz", + "integrity": "sha1-X42gcyOtOF/z3FQfGnkXsuQ261c=", + "requires": { + "escape-html": "1.0.1", + "parseurl": "1.3.2", + "send": "0.12.3", + "utils-merge": "1.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "tinycolor": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tinycolor/-/tinycolor-0.0.1.tgz", + "integrity": "sha1-MgtaUtg6u1l42Bo+iH1K77FaYWQ=" + }, + "topo": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", + "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", + "requires": { + "hoek": "5.0.2" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.17" + } + }, + "uid-safe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-1.1.0.tgz", + "integrity": "sha1-WNbF2r+N+9jVKDSDmAbAP9YUMjI=", + "requires": { + "base64-url": "1.2.1", + "native-or-bluebird": "1.1.2" + } + }, + "ultron": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=" + }, + "utf-8-validate": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-1.2.2.tgz", + "integrity": "sha1-i7hxpHQeCFxwSHynrNvX1tNgKes=", + "requires": { + "bindings": "1.2.1", + "nan": "2.4.0" + }, + "dependencies": { + "nan": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "integrity": "sha1-+zxZ1F/k7/4hXwuJD4rfbrMtIjI=" + } + } + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "vary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz", + "integrity": "sha1-meSYFWaihhGN+yuBc1ffeZM3bRA=" + }, + "websocket-stream": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/websocket-stream/-/websocket-stream-0.5.1.tgz", + "integrity": "sha1-YizR8FZvuEzgpNb4VFJvPcTXDkg=", + "requires": { + "isbuffer": "0.0.0", + "through": "2.3.8", + "ws": "0.4.32" + }, + "dependencies": { + "nan": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-1.0.0.tgz", + "integrity": "sha1-riT4hQgY1mL8q1rPfzuVv6oszzg=" + }, + "ws": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/ws/-/ws-0.4.32.tgz", + "integrity": "sha1-eHphVEFPPJntg8V3IVOyD+sM7DI=", + "requires": { + "commander": "2.1.0", + "nan": "1.0.0", + "options": "0.0.6", + "tinycolor": "0.0.1" + } + } + } + }, + "ws": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-1.0.1.tgz", + "integrity": "sha1-fQsqLljN3YGQOcKcneZQReGzEOk=", + "requires": { + "options": "0.0.6", + "ultron": "1.0.2" + } + } + } +} diff --git a/labs/kurento-screenshare/package.json b/labs/bbb-webrtc-sfu/package.json similarity index 58% rename from labs/kurento-screenshare/package.json rename to labs/bbb-webrtc-sfu/package.json index 187a3ab8a3f2e70c689e740178834ba1fadb5146..63bf2947e1efd675d6c585ee335dca30e8b95c92 100644 --- a/labs/kurento-screenshare/package.json +++ b/labs/bbb-webrtc-sfu/package.json @@ -1,20 +1,21 @@ { - "name": "bbb-screenshare-video-kurento-bridge", - "version": "1.0.0", + "name": "bbb-webrtc-sfu", + "version": "0.0.1", "private": true, "scripts": { - "start": "nodejs server.js", - "postinstall": "npm start" + "start": "node server.js" }, "dependencies": { "cookie-parser": "^1.3.5", "express": "~4.12.4", "express-session": "~1.10.3", - "ws": "~1.0.1", - "kurento-client": "6.6.0", + "kurento-client": "https://github.com/Kurento/kurento-client-js#master", + "moment": "*", "redis": "^2.6.2", "sdp-transform": "*", - "moment": "*" + "uuid": "^3.1.0", + "ws": "~1.0.1", + "joi": "*" }, "devDependencies": { "config": "^1.26.1", diff --git a/labs/bbb-webrtc-sfu/server.js b/labs/bbb-webrtc-sfu/server.js new file mode 100755 index 0000000000000000000000000000000000000000..94b4ec868b85e7a6608680639cd620d4ce3f8dd9 --- /dev/null +++ b/labs/bbb-webrtc-sfu/server.js @@ -0,0 +1,67 @@ +/* + * Lucas Fialho Zawacki + * Paulo Renato Lanzarin + * (C) Copyright 2017 Bigbluebutton + * + */ + +const ConnectionManager = require('./lib/connection-manager/ConnectionManager'); +const HttpServer = require('./lib/connection-manager/HttpServer'); +//const server = new HttpServer(); +//const WebsocketConnectionManager = require('./lib/connection-manager/WebsocketConnectionManager'); +const cp = require('child_process'); + +let screenshareProc = cp.fork('./lib/screenshare/ScreenshareProcess', { + // Pass over all of the environment. + env: process.ENV, + // Share stdout/stderr, so we can hear the inevitable errors. + silent: false +}); + +let videoProc = cp.fork('./lib/video/VideoProcess.js', { + // Pass over all of the environment. + env: process.ENV, + // Share stdout/stderr, so we can hear the inevitable errors. + silent: false +}); + +let onMessage = function (message) { + console.log('event','child message',this.pid,message); +}; + +let onError = function(e) { + console.log('event','child error',this.pid,e); +}; + +let onDisconnect = function(e) { + console.log(e); + console.log('event','child disconnect',this.pid,'killing...'); + this.kill(); +}; + +screenshareProc.on('message',onMessage); +screenshareProc.on('error',onError); +screenshareProc.on('disconnect',onDisconnect); + +videoProc.on('message',onMessage); +videoProc.on('error',onError); +videoProc.on('disconnect',onDisconnect); + +//const CM = new ConnectionManager(screenshareProc, videoProc); + +//let websocketManager = new WebsocketConnectionManager(server.getServerObject(), '/kurento-screenshare'); + +process.on('SIGTERM', process.exit) +process.on('SIGINT', process.exit) +process.on('uncaughtException', function (error) { + console.log(error.stack); + process.exit('1'); +}); + + +//CM.setHttpServer(server); +//CM.addAdapter(websocketManager); +// +//CM.listen(() => { +// console.log(" [SERVER] Server started"); +//}); diff --git a/labs/kurento-screenshare/.gitignore b/labs/kurento-screenshare/.gitignore deleted file mode 100644 index 40b878db5b1c97fc77049537a71bb2e249abe5dc..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ \ No newline at end of file diff --git a/labs/kurento-screenshare/config/default.yml b/labs/kurento-screenshare/config/default.yml deleted file mode 100644 index 46b6e2629ecf6e34603f5a34c1288266d0583918..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/config/default.yml +++ /dev/null @@ -1,8 +0,0 @@ -kurentoUrl: "KURENTOURL" -kurentoIp: "KURENTOIP" -localIpAddress: "HOST" -acceptSelfSignedCertificate: false -redisHost : "127.0.0.1" -redisPort : "6379" -minVideoPort: 30000 -maxVideoPort: 33000 diff --git a/labs/kurento-screenshare/keys/README.md b/labs/kurento-screenshare/keys/README.md deleted file mode 100644 index 5bc681a1c8d2ece88651b6ee63d410536eae50f6..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This folder contains a dummy self-signed certificate only for demo purposses, -**DON'T USE IT IN PRODUCTION**. diff --git a/labs/kurento-screenshare/keys/server.crt b/labs/kurento-screenshare/keys/server.crt deleted file mode 100644 index 65e608dad5d9fb19f68ac486e6189dfc67dcd2ff..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/server.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBjCCAe4CCQCuf5QfyX2oDDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTE0MDkyOTA5NDczNVoXDTE1MDkyOTA5NDczNVowRTELMAkG -A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5lFi3pBYWIY6kTN/iUaxJLROFo -FhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP1UitWzVO6pVvBaIt5IKlhhfm -YA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5VjKYc3OtEhcG8dgLAnOjbbk2Hr -8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo9gs56urvVDWG4rhdGybj1uwU -ZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0NjaU+MpSdEbB82z4b2NiN8Wq+ -rFA/JbvyeoWWHMoa7wkVs1MCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYLRwV9fo -AOhJfeK199Tv6oXoNSSSe10pVLnYxPcczCVQ4b9SomKFJFbmwtPVGi6w3m+8mV7F -9I2WKyeBHzmzfW2utZNupVybxgzEjuFLOVytSPdsB+DcJomOi8W/Cf2Vk8Wykb/t -Ctr1gfOcI8rwEGKxm279spBs0u1snzoLyoimbMbiXbC82j1IiN3Jus08U07m/j7N -hRBCpeHjUHT3CRpvYyTRnt+AyBd8BiyJB7nWmcNI1DksXPfehd62MAFS9e1ZE+dH -Aavg/U8VpS7pcCQcPJvIJ2hehrt8L6kUk3YUYqZ0OeRZK27f2R5+wFlDF33esm3N -dCSsLJlXyqAQFg== ------END CERTIFICATE----- diff --git a/labs/kurento-screenshare/keys/server.csr b/labs/kurento-screenshare/keys/server.csr deleted file mode 100644 index 6615b130471ce23cf8d980df5a308694ed06695b..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/server.csr +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx -ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5l -Fi3pBYWIY6kTN/iUaxJLROFoFhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP -1UitWzVO6pVvBaIt5IKlhhfmYA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5Vj -KYc3OtEhcG8dgLAnOjbbk2Hr8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo -9gs56urvVDWG4rhdGybj1uwUZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0N -jaU+MpSdEbB82z4b2NiN8Wq+rFA/JbvyeoWWHMoa7wkVs1MCAwEAAaAAMA0GCSqG -SIb3DQEBCwUAA4IBAQBMszYHMpklgTF/3h1zAzKXUD9NrtZp8eWhL06nwVjQX8Ai -EaCUiW0ypstokWcH9+30chd2OD++67NbxYUEucH8HrKpOoy6gs5L/mqgQ9Npz3OT -TB1HI4kGtpVuUQ5D7L0596tKzMX/CgW/hRcHWl+PDkwGhQs1qZcJ8QN+YP6AkRrO -5sDdDB/BLrB9PtBQbPrYIQcHQ7ooYWz/G+goqRxzZ6rt0aU2uAB6l7c82ADLAqFJ -qlw+xqVzEETVfqM5TXKK/wV3hgm4oSX5Q4SHLKF94ODOkWcnV4nfIKz7y+5XcQ3p -PrGimI1br07okC5rO9cgLCR0Ks20PPFcM0FvInW/ ------END CERTIFICATE REQUEST----- diff --git a/labs/kurento-screenshare/keys/server.key b/labs/kurento-screenshare/keys/server.key deleted file mode 100644 index a69a0a279daf6a68b9eff057204cd05af1b27a5a..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAwk7I4cn6slYkRDs/uQqhZrfV9/uEo1nEXqxgTmUWLekFhYhj -qRM3+JRrEktE4WgWGgL8z9JNjvqsivKLHjvi//pxGgbw34ZAESfggA/VSK1bNU7q -lW8Foi3kgqWGF+ZgDUgzB4J3Te8txodN102YUMFOSztAPB96dNpHlWMphzc60SFw -bx2AsCc6NtuTYevzC16vYh42CSHJrfPKhedMtPybwgyDaZBomzrZeWj2Cznq6u9U -NYbiuF0bJuPW7BRmINjI/gIUJQdLpRW3Xa8AM/y+NvCayzZJwawh/Q2NpT4ylJ0R -sHzbPhvY2I3xar6sUD8lu/J6hZYcyhrvCRWzUwIDAQABAoIBACwt56TW3MZxqZtN -8WYsUZheUispJ/ZQMcLo5JjOiSV1Jwk+gpJtyTse291z+bxagzP02/CQu4u32UVa -cmE0cp+LHO4zB8964dREwdm8P91fdS6Au/uwG5LNZniCFCQZAFvkv52Ef4XbzQen -uf4rKWerHBck6K0C5z/sZXxE6KtScE2ZLUmkhO0nkHM6MA6gFk2OMnB+oDTOWWPt -1mlreQlzuMYG/D4axviRYrOSYCE5Qu1SOw/DEOLQqqeBjQrKtAyOlFHZsIR6lBfe -KHMChPUcYIwaowt2DcqH/A+AFXRtaifa6DvH8Yul+2vAp47UEpaenVfM5bpN33XV -EzerjtECgYEA+xiXzblek67iQgRpc9eHSoqs4iRLhae8s8kpAG51Jz46Je+Dmium -XV769oiUGUxBeoUb7ryW+4MOzHJaA1BfGejQSvwLIB9e4cnikqnAArcqbcAcOCL1 -aYYDiSmSmN/AokNZlPKEBFXP9bzXrU9smQJWNTHlcRl7JXfnwF+jwNsCgYEAxhpE -SBr9vlUVHNh/S6C5i80NIYg6jCy2FgsmuzEqmcqV0pTyzegmq8bru+QmuvoUj2o4 -nVv4J9d1fLF6ECUVk9aK8UdJOOB6hAfurOdJCArgrsY/9t4uDzXfbPCdfSNQITE0 -XgeNGQX1EzvwwkBmyZKk0kLIr3syP8ZCWfXDROkCgYBR+dF1pJMv++R6UR5sZ20P -9P5ERj0xwXVl7MKqFWXCDhrFz9BTQPTrftrIKgbPy4mFCnf4FTHlov/t11dzxYWG -2+9Ey8yGDDfZ1yNVZn39ZPdBJXsRCLi+XrZAzYXCyyoEz6ArdJGNKMbgH2r6dfeq -bIzgiQ2zQvJlZSQQNiksCQKBgCgwzAmU8EXdHRttEOZXBU3HnBJhgP9PUuHGAWWY -4/uvjhXbAiekIbRX9xt3fiQQ+HrgIfxK3F246K0TlKAR5f7IWAf7Xm+bmz+OHG4X -vklTa6IJtpBvIwkS9PE1H75zm54gTW+GOKoK+12bm4zNZA0hIy9FPVHcvKUTpAJ8 -SdGBAoGAHLtJnB1NO4EgO6WtLQMXt7HrIbup8eZi8/82gC3422C+ooKIrYQ07qSw -nBOO/G0OB4yd6vCE2x5+TWSSCYGgG5A8aIv5qP76RP4hovGHxG/y2tfotw5UuOrh -nFWlTP4Urs8PeykvK9ao8r/T8BnPIC16U6ENYvAc0mRlFA2j1GA= ------END RSA PRIVATE KEY----- diff --git a/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js b/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js deleted file mode 100644 index da92167e4169e8a03ee3d3cb3e03b78405c1049f..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @classdesc - * Redis wrapper class for connecting to Redis channels - */ - -/* Modules */ - -var redis = require('redis'); -var config = require('config'); -var Constants = require('../messages/Constants.js'); -var util = require('util'); -const EventEmitter = require('events').EventEmitter; -const _retryThreshold = 1000 * 60 * 60; -const _maxRetries = 10; - - -/* Public members */ - -var RedisWrapper = function(subpattern) { - // Redis PubSub client holders - this.redisCli = null; - this.redisPub = null; - // Pub and Sub channels/patterns - this.subpattern = subpattern; - EventEmitter.call(this); -} - -util.inherits(RedisWrapper, EventEmitter); - -RedisWrapper.prototype.startRedis = function(callback) { - var self = this; - if (this.redisCli) { - console.log(" [RedisWrapper] Redis Client already exists"); - callback(false, this); - } - - var options = { - host : config.get('redisHost'), - port : config.get('redisPort'), - //password: config.get('redis.password') - retry_strategy: redisRetry - }; - - this.redisCli = redis.createClient(options); - this.redisPub = redis.createClient(options); - - console.log(" [RedisWrapper] Trying to subscribe to redis channel"); - - this.redisCli.on("psubscribe", function (channel, count) { - console.log(" [RedisWrapper] Successfully subscribed to pattern [" + channel + "]"); - }); - - this.redisCli.on("pmessage", self.onMessage.bind(self)); - this.redisCli.psubscribe(this.subpattern); - - console.log(" [RedisWrapper] Started Redis client at " + options.host + ":" + options.port + - " for subscription pattern: " + this.subpattern); - - callback(false, this); -}; - -RedisWrapper.prototype.stopRedis = function(callback) { - if (this.redisCli){ - this.redisCli.quit(); - } - callback(false); -}; - -RedisWrapper.prototype.publishToChannel = function(message, channel) { - if(this.redisPub) { - console.log(" [RedisWrapper] Sending message to channel [" + channel + "]: " + message); - this.redisPub.publish(channel, message); - } -}; - -RedisWrapper.prototype.onMessage = function(pattern, channel, message) { - console.log(" [RedisWrapper] Message received from channel [" + channel + "] : " + message); - // use event emitter to throw new message - this.emit(Constants.REDIS_MESSAGE, message); -} - -/* Private members */ - -function redisRetry(options) { - if (options.error && options.error.code === 'ECONNREFUSED') { - return new Error('The server refused the connection'); - } - if (options.total_retry_time > _retryThreshold) { - return new Error('Retry time exhausted'); - } - if (options.times_connected > _maxRetries) { - return undefined; - } - return Math.max(options.attempt * 100, 3000); -}; - -module.exports = RedisWrapper; diff --git a/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js b/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js deleted file mode 100644 index 1abbb25aa010d9dc2a18d09edb2064c8516d9463..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @classdesc - * BigBlueButton redis gateway for bbb-screenshare node app - */ - -/* Modules */ - -var C = require('../messages/Constants.js'); -var RedisWrapper = require('./RedisWrapper.js'); -var config = require('config'); -var util = require('util'); -var EventEmitter = require('events').EventEmitter; - -/* Public members */ - -var BigBlueButtonGW = function () { - this.redisClients = null - EventEmitter.call(this); -}; - -util.inherits(BigBlueButtonGW, EventEmitter); - -BigBlueButtonGW.prototype.addSubscribeChannel = function (channel, callback) { - var self = this; - - if (this.redisClients === null) { - this.redisClients = {}; - } - - if (this.redisClients[channel]) { - return callback(null, this.redisClients[channel]); - } - - var wrobj = new RedisWrapper(channel); - this.redisClients[channel] = {}; - this.redisClients[channel] = wrobj; - wrobj.startRedis(function(error, redisCli) { - if(error) { - console.log(" [BigBlueButtonGW] Could not start redis client for channel " + channel); - return callback(error); - } - - console.log(" [BigBlueButtonGW] Added redis client to this.redisClients[" + channel + "]"); - wrobj.on(C.REDIS_MESSAGE, self.incomingMessage.bind(self)); - - return callback(null, wrobj); - }); -}; - -/** - * Capture messages from subscribed channels and emit an event with it's - * identifier and payload. Check Constants.js for the identifiers. - * - * @param {Object} message Redis message - */ -BigBlueButtonGW.prototype.incomingMessage = function (message) { - var msg = JSON.parse(message); - - // Trying to parse both message types, 1x and 2x - if (msg.header) { - var header = msg.header; - var payload = msg.payload; - } - else if (msg.core) { - var header = msg.core.header; - var payload = msg.core.body; - } - - if (header){ - switch (header.name) { - // interoperability with 1.1 - case C.START_TRANSCODER_REPLY: - this.emit(C.START_TRANSCODER_REPLY, payload); - break; - case C.STOP_TRANSCODER_REPLY: - this.emit(C.STOP_TRANSCODER_REPLY, payload); - break; - // 2x messages - case C.START_TRANSCODER_RESP_2x: - payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x]; - - this.emit(C.START_TRANSCODER_RESP_2x, payload); - break; - case C.STOP_TRANSCODER_RESP_2x: - payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x]; - this.emit(C.STOP_TRANSCODER_RESP_2x, payload); - break; - - default: - console.log(" [BigBlueButtonGW] Unknown Redis message with ID =>" + header.name); - } - } -}; - -BigBlueButtonGW.prototype.publish = function (message, channel, callback) { - for(var client in this.redisClients) { - if(typeof this.redisClients[client].publishToChannel === 'function') { - this.redisClients[client].publishToChannel(message, channel); - return callback(null); - } - } - return callback("Client not found"); -}; - -module.exports = BigBlueButtonGW; diff --git a/labs/kurento-screenshare/server.js b/labs/kurento-screenshare/server.js deleted file mode 100755 index f645ac88a52ffa328944f6f775b868bda3b0ae34..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/server.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Paulo Renato Lanzarin - * (C) Copyright 2017 Bigbluebutton - * - */ - -const ConnectionManager = require('./lib/ConnectionManager'); -const CM = new ConnectionManager(); - -process.on('SIGTERM', CM._stopAll.bind(CM)); -process.on('SIGINT', CM._stopAll.bind(CM));