Merge branch 'node-bbb-apps-packaging' into bbb-webrtc-sfu
Conflicts: bigbluebutton-html5/imports/startup/client/base.jsx bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx bigbluebutton-html5/imports/ui/components/app/container.jsx bigbluebutton-html5/imports/ui/components/screenshare/service.js bigbluebutton-html5/imports/ui/components/video-dock/component.jsx bigbluebutton-html5/imports/ui/components/video-dock/container.jsx bigbluebutton-html5/private/locales/en.json bigbluebutton-html5/server/main.js
This commit is contained in:
commit
f43b77c19f
@ -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
|
||||
|
@ -49,6 +49,7 @@
|
||||
uri="rtmp://HOST/screenshare"
|
||||
showButton="true"
|
||||
enablePause="true"
|
||||
tryKurentoWebRTC="false"
|
||||
tryWebRTCFirst="false"
|
||||
chromeExtensionLink=""
|
||||
chromeExtensionKey=""
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
49
bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
Executable file
49
bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js
Executable file
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
2
bigbluebutton-html5/imports/api/video/server/index.js
Normal file
2
bigbluebutton-html5/imports/api/video/server/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import './eventHandlers';
|
||||
import './methods';
|
7
bigbluebutton-html5/imports/api/video/server/methods.js
Normal file
7
bigbluebutton-html5/imports/api/video/server/methods.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import userShareWebcam from './methods/userShareWebcam';
|
||||
import userUnshareWebcam from './methods/userUnshareWebcam';
|
||||
|
||||
Meteor.methods({
|
||||
userShareWebcam, userUnshareWebcam,
|
||||
});
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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 }) => {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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 />,
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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 />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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));
|
@ -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);
|
@ -0,0 +1,6 @@
|
||||
kurento:
|
||||
wsUrl: 'HOST'
|
||||
chromeExtensionKey: 'KEY'
|
||||
chromeExtensionLink: 'LINK'
|
||||
enableScreensharing: false
|
||||
enableVideo: false
|
@ -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"
|
||||
}
|
||||
|
91
bigbluebutton-html5/public/js/adjust-videos.js
Normal file
91
bigbluebutton-html5/public/js/adjust-videos.js
Normal file
@ -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;
|
||||
})();
|
32
bigbluebutton-html5/public/js/bower.json
Normal file
32
bigbluebutton-html5/public/js/bower.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
15
labs/bbb-webrtc-sfu/config/default.example.yml
Normal file
15
labs/bbb-webrtc-sfu/config/default.example.yml
Normal file
@ -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"
|
@ -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",
|
@ -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;
|
119
labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js
Normal file
119
labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
121
labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
Normal file
121
labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
104
labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js
Normal file
104
labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js
Normal file
@ -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() {
|
||||
}
|
||||
}
|
30
labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js
Normal file
30
labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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 () {
|
||||
|
||||
}
|
||||
|
||||
}
|
12
labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js
Normal file
12
labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js
Normal file
@ -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();
|
86
labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js
Normal file
86
labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js
Normal file
@ -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>'
|
146
labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js
Normal file
146
labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js
Normal file
@ -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
|
||||
}
|
||||
}
|
309
labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js
Normal file
309
labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js
Normal file
@ -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];
|
||||
}
|
||||
}
|
272
labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js
Normal file
272
labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js
Normal file
@ -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});
|
||||
}
|
||||
};
|
43
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js
Normal file
43
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js
Normal file
@ -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];
|
||||
}
|
||||
|
||||
}
|
117
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
Normal file
117
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
164
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js
Normal file
164
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
73
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js
Normal file
73
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
18
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js
Normal file
18
labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js
Normal file
@ -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;
|
||||
}
|
||||
}
|
256
labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
Normal file
256
labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js
Normal file
@ -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,'');
|
||||
};
|
||||
};
|
37
labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js
Normal file
37
labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js
Normal file
@ -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';
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
12
labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js
Normal file
12
labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js
Normal file
@ -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();
|
@ -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];
|
||||
};
|
||||
};
|
159
labs/bbb-webrtc-sfu/lib/video/VideoManager.js
Executable file
159
labs/bbb-webrtc-sfu/lib/video/VideoManager.js
Executable file
@ -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);
|
10
labs/bbb-webrtc-sfu/lib/video/VideoProcess.js
Normal file
10
labs/bbb-webrtc-sfu/lib/video/VideoProcess.js
Normal file
@ -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();
|
||||
});
|
152
labs/bbb-webrtc-sfu/lib/video/video.js
Normal file
152
labs/bbb-webrtc-sfu/lib/video/video.js
Normal file
@ -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;
|
||||
};
|
||||
};
|
18
labs/bbb-webrtc-sfu/lib/websocket.js
Normal file
18
labs/bbb-webrtc-sfu/lib/websocket.js
Normal file
@ -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;
|
689
labs/bbb-webrtc-sfu/package-lock.json
generated
Normal file
689
labs/bbb-webrtc-sfu/package-lock.json
generated
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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",
|
67
labs/bbb-webrtc-sfu/server.js
Executable file
67
labs/bbb-webrtc-sfu/server.js
Executable file
@ -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");
|
||||
//});
|
1
labs/kurento-screenshare/.gitignore
vendored
1
labs/kurento-screenshare/.gitignore
vendored
@ -1 +0,0 @@
|
||||
node_modules/
|
@ -1,8 +0,0 @@
|
||||
kurentoUrl: "KURENTOURL"
|
||||
kurentoIp: "KURENTOIP"
|
||||
localIpAddress: "HOST"
|
||||
acceptSelfSignedCertificate: false
|
||||
redisHost : "127.0.0.1"
|
||||
redisPort : "6379"
|
||||
minVideoPort: 30000
|
||||
maxVideoPort: 33000
|
@ -1,2 +0,0 @@
|
||||
This folder contains a dummy self-signed certificate only for demo purposses,
|
||||
**DON'T USE IT IN PRODUCTION**.
|
@ -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-----
|
@ -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-----
|
@ -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-----
|
@ -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;
|
@ -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;
|
@ -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));
|
Loading…
Reference in New Issue
Block a user