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:
prlanzarin 2017-11-11 03:41:37 +00:00
commit f43b77c19f
94 changed files with 4544 additions and 479 deletions

View File

@ -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

View File

@ -49,6 +49,7 @@
uri="rtmp://HOST/screenshare"
showButton="true"
enablePause="true"
tryKurentoWebRTC="false"
tryWebRTCFirst="false"
chromeExtensionLink=""
chromeExtensionKey=""

View File

@ -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>

View File

@ -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);
});
};

View File

@ -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>

View File

@ -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;

View 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();
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}

View File

@ -0,0 +1,2 @@
import './eventHandlers';
import './methods';

View File

@ -0,0 +1,7 @@
import { Meteor } from 'meteor/meteor';
import userShareWebcam from './methods/userShareWebcam';
import userUnshareWebcam from './methods/userUnshareWebcam';
Meteor.methods({
userShareWebcam, userUnshareWebcam,
});

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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 }) => {

View File

@ -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>

View File

@ -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>

View File

@ -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));

View File

@ -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';

View File

@ -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 />,
};

View File

@ -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 {

View File

@ -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 />
);
}
}

View File

@ -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,
};

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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));

View File

@ -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);

View File

@ -0,0 +1,6 @@
kurento:
wsUrl: 'HOST'
chromeExtensionKey: 'KEY'
chromeExtensionLink: 'LINK'
enableScreensharing: false
enableVideo: false

View File

@ -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"
}

View 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;
})();

View 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"
}
}

View 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"

View File

@ -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",

View File

@ -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;

View 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);
}
}

View 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);
}
}

View 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() {
}
}

View 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);
}
}

View File

@ -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;
}
}

View File

@ -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() {
}
}

View File

@ -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 () {
}
}

View 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();

View 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>'

View 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
}
}

View 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];
}
}

View 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});
}
};

View 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];
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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,'');
};
};

View 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';
};

View File

@ -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);
}
}
};

View 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();

View File

@ -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];
};
};

View 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);

View 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();
});

View 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;
};
};

View 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
View 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"
}
}
}
}

View File

@ -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
View 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");
//});

View File

@ -1 +0,0 @@
node_modules/

View File

@ -1,8 +0,0 @@
kurentoUrl: "KURENTOURL"
kurentoIp: "KURENTOIP"
localIpAddress: "HOST"
acceptSelfSignedCertificate: false
redisHost : "127.0.0.1"
redisPort : "6379"
minVideoPort: 30000
maxVideoPort: 33000

View File

@ -1,2 +0,0 @@
This folder contains a dummy self-signed certificate only for demo purposses,
**DON'T USE IT IN PRODUCTION**.

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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;

View File

@ -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;

View File

@ -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));