diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala index ad5e4c5f64cf5520b61f66a409315d4a791565d9..ef46d2b8dc88b6a46d754b9767db312307175827 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/apps/users/UserBroadcastCamStartMsgHdlr.scala @@ -16,7 +16,7 @@ trait UserBroadcastCamStartMsgHdlr { val envelope = BbbCoreEnvelope(UserBroadcastCamStartedEvtMsg.NAME, routing) val header = BbbClientMsgHeader(UserBroadcastCamStartedEvtMsg.NAME, props.meetingProp.intId, msg.header.userId) - val body = UserBroadcastCamStartedEvtMsgBody(msg.header.userId, msg.body.stream) + val body = UserBroadcastCamStartedEvtMsgBody(msg.header.userId, msg.body.stream, msg.body.isHtml5Client) val event = UserBroadcastCamStartedEvtMsg(header, body) val msgEvent = BbbCommonEnvCoreMsg(envelope, event) outGW.send(msgEvent) diff --git a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala index 5ae7d4d228c9d5151b778f4ede4633c40c91d228..c1d5fcff1be79e96dcd5f3182ff1487239155618 100755 --- a/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala +++ b/bbb-common-message/src/main/scala/org/bigbluebutton/common2/msgs/WebcamsMsgs.scala @@ -3,15 +3,15 @@ package org.bigbluebutton.common2.msgs object UserBroadcastCamStartedEvtMsg { val NAME = "UserBroadcastCamStartedEvtMsg" } case class UserBroadcastCamStartedEvtMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStartedEvtMsgBody) extends BbbCoreMsg -case class UserBroadcastCamStartedEvtMsgBody(userId: String, stream: String) +case class UserBroadcastCamStartedEvtMsgBody(userId: String, stream: String, isHtml5Client: Boolean = false) object UserBroadcastCamStartMsg { val NAME = "UserBroadcastCamStartMsg" } case class UserBroadcastCamStartMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStartMsgBody) extends StandardMsg -case class UserBroadcastCamStartMsgBody(stream: String) +case class UserBroadcastCamStartMsgBody(stream: String, isHtml5Client: Boolean = false) object UserBroadcastCamStopMsg { val NAME = "UserBroadcastCamStopMsg" } case class UserBroadcastCamStopMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStopMsgBody) extends StandardMsg -case class UserBroadcastCamStopMsgBody(stream: String) +case class UserBroadcastCamStopMsgBody(stream: String, isHtml5Client: Boolean = false) object UserBroadcastCamStoppedEvtMsg { val NAME = "UserBroadcastCamStoppedEvtMsg" } case class UserBroadcastCamStoppedEvtMsg(header: BbbClientMsgHeader, body: UserBroadcastCamStoppedEvtMsgBody) extends BbbCoreMsg diff --git a/bigbluebutton-client/resources/config.xml.template b/bigbluebutton-client/resources/config.xml.template index cc0b56fd9c4d63dc83160cbf101713d2e93f0e76..4d43ee876264fd7a29ae485b80518446b4ddf012 100755 --- a/bigbluebutton-client/resources/config.xml.template +++ b/bigbluebutton-client/resources/config.xml.template @@ -48,6 +48,7 @@ uri="rtmp://HOST/screenshare" showButton="true" enablePause="true" + tryKurentoWebRTC="false" tryWebRTCFirst="false" chromeExtensionLink="" chromeExtensionKey="" diff --git a/bigbluebutton-client/resources/prod/BigBlueButton.html b/bigbluebutton-client/resources/prod/BigBlueButton.html index 3084a19be3d5a7046dcc2b85c99e92861ba8f699..ecd8710c8d5107671da8ae4674687292d4098fca 100755 --- a/bigbluebutton-client/resources/prod/BigBlueButton.html +++ b/bigbluebutton-client/resources/prod/BigBlueButton.html @@ -142,8 +142,8 @@ <script src="lib/verto-min.js" language="javascript"></script> <script src="lib/verto_extension.js" language="javascript"></script> - <script src="lib/kurento-utils.min.js" language="javascript"></script> <script src="lib/kurento-extension.js" language="javascript"></script> + <script src="lib/kurento-utils.js" language="javascript"></script> <script src="lib/bbb_api_bridge.js?v=VERSION" language="javascript"></script> <script src="lib/sip.js?v=VERSION" language="javascript"></script> diff --git a/bigbluebutton-client/resources/prod/lib/kurento-extension.js b/bigbluebutton-client/resources/prod/lib/kurento-extension.js index cd9c5fab9026142ecae3133278762eabdf33fd9b..3aa4e9243994311f35964cb79379d1a53cc74f74 100644 --- a/bigbluebutton-client/resources/prod/lib/kurento-extension.js +++ b/bigbluebutton-client/resources/prod/lib/kurento-extension.js @@ -1,6 +1,7 @@ var isFirefox = typeof window.InstallTrigger !== 'undefined'; var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; var isChrome = !!window.chrome && !isOpera; +var isSafari = navigator.userAgent.indexOf("Safari") >= 0 && !isChrome; var kurentoHandler = null; Kurento = function ( @@ -20,7 +21,7 @@ Kurento = function ( this.screenConstraints = {}; this.mediaCallback = null; - this.voiceBridge = voiceBridge; + this.voiceBridge = voiceBridge + '-SCREENSHARE'; this.internalMeetingId = internalMeetingId; this.vid_width = window.screen.width; @@ -33,9 +34,8 @@ Kurento = function ( this.caller_id_name = conferenceUsername; this.caller_id_number = conferenceUsername; - this.pingInterval; - this.kurentoPort = "kurento-screenshare"; + this.kurentoPort = "bbb-webrtc-sfu"; this.hostName = window.location.hostname; this.socketUrl = 'wss://' + this.hostName + '/' + this.kurentoPort; @@ -43,6 +43,7 @@ Kurento = function ( if (chromeExtension != null) { this.chromeExtension = chromeExtension; + window.chromeExtension = chromeExtension; } if (onFail != null) { @@ -57,21 +58,44 @@ 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.ws !== null) { + this.kurentoScreenshare.ws.onclose = function(){}; + this.kurentoScreenshare.ws.close(); } - if(kurentoHandler.ws !== null) { - kurentoHandler.ws.onclose = function(){}; - kurentoHandler.ws.close(); + + 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.ws !== null) { + this.kurentoVideo.ws.onclose = function(){}; + this.kurentoVideo.ws.close(); } - kurentoHandler.disposeScreenShare(); - this.kurentoScreenShare = null; - kurentoHandler = null; + + this.kurentoVideo.disposeScreenShare(); + this.kurentoVideo = null; + } + + if (this.kurentoVideo) { + this.kurentoVideo = null; } }; @@ -79,24 +103,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 +133,18 @@ 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.mediaCallback(); - }; + }.bind(self); } else console.log("this browser does not support websockets"); @@ -135,15 +155,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); - break; - case 'pong': + this.webRtcPeer.addIceCandidate(parsedMessage.candidate); break; default: console.error('Unrecognized message', parsedMessage); @@ -156,21 +177,33 @@ Kurento.prototype.setRenderTag = function (tag) { Kurento.prototype.presenterResponse = function (message) { if (message.response != 'accepted') { - var errorMsg = message.message ? message.message : 'Unknow error'; - console.warn('Call not accepted for the following reason: ' + errorMsg); + var errorMsg = message.message ? message.message : 'Unknown error'; + console.warn('Call not accepted for the following reason: ' + JSON.stringify(errorMsg, null, 2)); 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 +211,105 @@ 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, + role: 'presenter', + 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', + role: 'presenter', 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', + role: 'viewer', + type: 'screenshare', + voiceBridge: self.voiceBridge, + candidate : candidate, + callerName: self.caller_id_name + } + this.sendMessage(message); } Kurento.prototype.setWatchVideo = function (tag) { @@ -276,60 +325,50 @@ 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, + role: 'viewer', + 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() { - var message = { - id : 'ping', - type: 'screenshare', - internalMeetingId: kurentoHandler.internalMeetingId, - voiceBridge: kurentoHandler.voiceBridge, - callerName : kurentoHandler.caller_id_name, - }; - - kurentoHandler.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 +399,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 +415,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 +475,22 @@ window.kurentoWatchVideo = function () { window.kurentoInitialize(); window.kurentoManager.joinWatchVideo.apply(window.kurentoManager, arguments); }; + +window.kurentoExitVideo = function () { + window.kurentoInitialize(); + window.kurentoManager.exitVideo(); +} + +window.getChromeScreenConstraints = function(callback, extensionId) { + chrome.runtime.sendMessage(extensionId, { + getStream: true, + sources: [ + "window", + "screen", + "tab" + ]}, + function(response) { + console.log(response); + callback(response); + }); +};; diff --git a/bigbluebutton-client/resources/prod/lib/kurento-utils.js b/bigbluebutton-client/resources/prod/lib/kurento-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..d171093e8784054a9f42264a0dccb960c5c25d10 --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/kurento-utils.js @@ -0,0 +1,4362 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kurentoUtils = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +var freeice = require('freeice'); +var inherits = require('inherits'); +var UAParser = require('ua-parser-js'); +var uuid = require('uuid'); +var hark = require('hark'); +var EventEmitter = require('events').EventEmitter; +var recursive = require('merge').recursive.bind(undefined, true); +var sdpTranslator = require('sdp-translator'); +var logger = window.Logger || console; +try { + require('kurento-browser-extensions'); +} catch (error) { + if (typeof getScreenConstraints === 'undefined') { + logger.warn('screen sharing is not available'); + getScreenConstraints = function getScreenConstraints(sendSource, callback) { + callback(new Error('This library is not enabled for screen sharing')); + }; + } +} +var MEDIA_CONSTRAINTS = { + audio: true, + video: { + width: 640, + framerate: 15 + } + }; +var ua = window && window.navigator ? window.navigator.userAgent : ''; +var parser = new UAParser(ua); +var browser = parser.getBrowser(); +var usePlanB = false; +if (browser.name === 'Chrome' || browser.name === 'Chromium') { + logger.debug(browser.name + ': using SDP PlanB'); + usePlanB = true; +} +function noop(error) { + if (error) + logger.error(error); +} +function trackStop(track) { + track.stop && track.stop(); +} +function streamStop(stream) { + stream.getTracks().forEach(trackStop); +} +var dumpSDP = function (description) { + if (typeof description === 'undefined' || description === null) { + return ''; + } + return 'type: ' + description.type + '\r\n' + description.sdp; +}; +function bufferizeCandidates(pc, onerror) { + var candidatesQueue = []; + pc.addEventListener('signalingstatechange', function () { + if (this.signalingState === 'stable') { + while (candidatesQueue.length) { + var entry = candidatesQueue.shift(); + this.addIceCandidate(entry.candidate, entry.callback, entry.callback); + } + } + }); + return function (candidate, callback) { + callback = callback || onerror; + switch (pc.signalingState) { + case 'closed': + callback(new Error('PeerConnection object is closed')); + break; + case 'stable': + if (pc.remoteDescription) { + pc.addIceCandidate(candidate, callback, callback); + break; + } + default: + candidatesQueue.push({ + candidate: candidate, + callback: callback + }); + } + }; +} +function removeFIDFromOffer(sdp) { + var n = sdp.indexOf('a=ssrc-group:FID'); + if (n > 0) { + return sdp.slice(0, n); + } else { + return sdp; + } +} +function getSimulcastInfo(videoStream) { + var videoTracks = videoStream.getVideoTracks(); + if (!videoTracks.length) { + logger.warn('No video tracks available in the video stream'); + return ''; + } + var lines = [ + 'a=x-google-flag:conference', + 'a=ssrc-group:SIM 1 2 3', + 'a=ssrc:1 cname:localVideo', + 'a=ssrc:1 msid:' + videoStream.id + ' ' + videoTracks[0].id, + 'a=ssrc:1 mslabel:' + videoStream.id, + 'a=ssrc:1 label:' + videoTracks[0].id, + 'a=ssrc:2 cname:localVideo', + 'a=ssrc:2 msid:' + videoStream.id + ' ' + videoTracks[0].id, + 'a=ssrc:2 mslabel:' + videoStream.id, + 'a=ssrc:2 label:' + videoTracks[0].id, + 'a=ssrc:3 cname:localVideo', + 'a=ssrc:3 msid:' + videoStream.id + ' ' + videoTracks[0].id, + 'a=ssrc:3 mslabel:' + videoStream.id, + 'a=ssrc:3 label:' + videoTracks[0].id + ]; + lines.push(''); + return lines.join('\n'); +} +function WebRtcPeer(mode, options, callback) { + if (!(this instanceof WebRtcPeer)) { + return new WebRtcPeer(mode, options, callback); + } + WebRtcPeer.super_.call(this); + if (options instanceof Function) { + callback = options; + options = undefined; + } + options = options || {}; + callback = (callback || noop).bind(this); + var self = this; + var localVideo = options.localVideo; + var remoteVideo = options.remoteVideo; + var videoStream = options.videoStream; + var audioStream = options.audioStream; + var mediaConstraints = options.mediaConstraints; + var connectionConstraints = options.connectionConstraints; + var pc = options.peerConnection; + var sendSource = options.sendSource || 'webcam'; + var dataChannelConfig = options.dataChannelConfig; + var useDataChannels = options.dataChannels || false; + var dataChannel; + var guid = uuid.v4(); + var configuration = recursive({ iceServers: freeice() }, options.configuration); + var onicecandidate = options.onicecandidate; + if (onicecandidate) + this.on('icecandidate', onicecandidate); + var oncandidategatheringdone = options.oncandidategatheringdone; + if (oncandidategatheringdone) { + this.on('candidategatheringdone', oncandidategatheringdone); + } + var simulcast = options.simulcast; + var multistream = options.multistream; + var interop = new sdpTranslator.Interop(); + var candidatesQueueOut = []; + var candidategatheringdone = false; + Object.defineProperties(this, { + 'peerConnection': { + get: function () { + return pc; + } + }, + 'id': { + value: options.id || guid, + writable: false + }, + 'remoteVideo': { + get: function () { + return remoteVideo; + } + }, + 'localVideo': { + get: function () { + return localVideo; + } + }, + 'dataChannel': { + get: function () { + return dataChannel; + } + }, + 'currentFrame': { + get: function () { + if (!remoteVideo) + return; + if (remoteVideo.readyState < remoteVideo.HAVE_CURRENT_DATA) + throw new Error('No video stream data available'); + var canvas = document.createElement('canvas'); + canvas.width = remoteVideo.videoWidth; + canvas.height = remoteVideo.videoHeight; + canvas.getContext('2d').drawImage(remoteVideo, 0, 0); + return canvas; + } + } + }); + if (!pc) { + pc = new RTCPeerConnection(configuration); + if (useDataChannels && !dataChannel) { + var dcId = 'WebRtcPeer-' + self.id; + var dcOptions = undefined; + if (dataChannelConfig) { + dcId = dataChannelConfig.id || dcId; + dcOptions = dataChannelConfig.options; + } + dataChannel = pc.createDataChannel(dcId, dcOptions); + if (dataChannelConfig) { + dataChannel.onopen = dataChannelConfig.onopen; + dataChannel.onclose = dataChannelConfig.onclose; + dataChannel.onmessage = dataChannelConfig.onmessage; + dataChannel.onbufferedamountlow = dataChannelConfig.onbufferedamountlow; + dataChannel.onerror = dataChannelConfig.onerror || noop; + } + } + } + pc.addEventListener('icecandidate', function (event) { + var candidate = event.candidate; + if (EventEmitter.listenerCount(self, 'icecandidate') || EventEmitter.listenerCount(self, 'candidategatheringdone')) { + if (candidate) { + var cand; + if (multistream && usePlanB) { + cand = interop.candidateToUnifiedPlan(candidate); + } else { + cand = candidate; + } + self.emit('icecandidate', cand); + candidategatheringdone = false; + } else if (!candidategatheringdone) { + self.emit('candidategatheringdone'); + candidategatheringdone = true; + } + } else if (!candidategatheringdone) { + candidatesQueueOut.push(candidate); + if (!candidate) + candidategatheringdone = true; + } + }); + pc.ontrack = options.onaddstream; + pc.onnegotiationneeded = options.onnegotiationneeded; + this.on('newListener', function (event, listener) { + if (event === 'icecandidate' || event === 'candidategatheringdone') { + while (candidatesQueueOut.length) { + var candidate = candidatesQueueOut.shift(); + if (!candidate === (event === 'candidategatheringdone')) { + listener(candidate); + } + } + } + }); + var addIceCandidate = bufferizeCandidates(pc); + this.addIceCandidate = function (iceCandidate, callback) { + var candidate; + if (multistream && usePlanB) { + candidate = interop.candidateToPlanB(iceCandidate); + } else { + candidate = new RTCIceCandidate(iceCandidate); + } + logger.debug('Remote ICE candidate received', iceCandidate); + callback = (callback || noop).bind(this); + addIceCandidate(candidate, callback); + }; + this.generateOffer = function (callback) { + callback = callback.bind(this); + var offerAudio = true; + var offerVideo = true; + if (mediaConstraints) { + offerAudio = typeof mediaConstraints.audio === 'boolean' ? mediaConstraints.audio : true; + offerVideo = typeof mediaConstraints.video === 'boolean' ? mediaConstraints.video : true; + } + var browserDependantConstraints = { + offerToReceiveAudio: mode !== 'sendonly' && offerAudio, + offerToReceiveVideo: mode !== 'sendonly' && offerVideo + }; + var constraints = browserDependantConstraints; + logger.debug('constraints: ' + JSON.stringify(constraints)); + pc.createOffer(constraints).then(function (offer) { + logger.debug('Created SDP offer'); + offer = mangleSdpToAddSimulcast(offer); + return pc.setLocalDescription(offer); + }).then(function () { + var localDescription = pc.localDescription; + logger.debug('Local description set', localDescription.sdp); + if (multistream && usePlanB) { + localDescription = interop.toUnifiedPlan(localDescription); + logger.debug('offer::origPlanB->UnifiedPlan', dumpSDP(localDescription)); + } + callback(null, localDescription.sdp, self.processAnswer.bind(self)); + }).catch(callback); + }; + this.getLocalSessionDescriptor = function () { + return pc.localDescription; + }; + this.getRemoteSessionDescriptor = function () { + return pc.remoteDescription; + }; + function setRemoteVideo() { + if (remoteVideo) { + var stream = pc.getRemoteStreams()[0]; + remoteVideo.pause(); + remoteVideo.srcObject = stream; + remoteVideo.load(); + logger.info('Remote URL:', remoteVideo.srcObject); + } + } + this.showLocalVideo = function () { + localVideo.srcObject = videoStream; + localVideo.muted = true; + }; + this.send = function (data) { + if (dataChannel && dataChannel.readyState === 'open') { + dataChannel.send(data); + } else { + logger.warn('Trying to send data over a non-existing or closed data channel'); + } + }; + this.processAnswer = function (sdpAnswer, callback) { + callback = (callback || noop).bind(this); + var answer = new RTCSessionDescription({ + type: 'answer', + sdp: sdpAnswer + }); + if (multistream && usePlanB) { + var planBAnswer = interop.toPlanB(answer); + logger.debug('asnwer::planB', dumpSDP(planBAnswer)); + answer = planBAnswer; + } + logger.debug('SDP answer received, setting remote description'); + if (pc.signalingState === 'closed') { + return callback('PeerConnection is closed'); + } + pc.setRemoteDescription(answer, function () { + setRemoteVideo(); + callback(); + }, callback); + }; + this.processOffer = function (sdpOffer, callback) { + callback = callback.bind(this); + var offer = new RTCSessionDescription({ + type: 'offer', + sdp: sdpOffer + }); + if (multistream && usePlanB) { + var planBOffer = interop.toPlanB(offer); + logger.debug('offer::planB', dumpSDP(planBOffer)); + offer = planBOffer; + } + logger.debug('SDP offer received, setting remote description'); + if (pc.signalingState === 'closed') { + return callback('PeerConnection is closed'); + } + pc.setRemoteDescription(offer).then(function () { + return setRemoteVideo(); + }).then(function () { + return pc.createAnswer(); + }).then(function (answer) { + answer = mangleSdpToAddSimulcast(answer); + logger.debug('Created SDP answer'); + return pc.setLocalDescription(answer); + }).then(function () { + var localDescription = pc.localDescription; + if (multistream && usePlanB) { + localDescription = interop.toUnifiedPlan(localDescription); + logger.debug('answer::origPlanB->UnifiedPlan', dumpSDP(localDescription)); + } + logger.debug('Local description set', localDescription.sdp); + callback(null, localDescription.sdp); + }).catch(callback); + }; + function mangleSdpToAddSimulcast(answer) { + if (simulcast) { + if (browser.name === 'Chrome' || browser.name === 'Chromium') { + logger.debug('Adding multicast info'); + answer = new RTCSessionDescription({ + 'type': answer.type, + 'sdp': removeFIDFromOffer(answer.sdp) + getSimulcastInfo(videoStream) + }); + } else { + logger.warn('Simulcast is only available in Chrome browser.'); + } + } + return answer; + } + function start() { + if (pc.signalingState === 'closed') { + callback('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'); + } + if (videoStream && localVideo) { + self.showLocalVideo(); + } + if (videoStream) { + pc.addStream(videoStream); + } + if (audioStream) { + pc.addStream(audioStream); + } + var browser = parser.getBrowser(); + if (mode === 'sendonly' && (browser.name === 'Chrome' || browser.name === 'Chromium') && browser.major === 39) { + mode = 'sendrecv'; + } + callback(); + } + if (mode !== 'recvonly' && !videoStream && !audioStream) { + function getMedia(constraints) { + if (constraints === undefined) { + constraints = MEDIA_CONSTRAINTS; + } + navigator.mediaDevices.getUserMedia(constraints).then(function (stream) { + videoStream = stream; + start(); + }).catch(callback); + } + if (sendSource === 'webcam') { + getMedia(mediaConstraints); + } else { + getScreenConstraints(sendSource, function (error, constraints_) { + if (error) + return callback(error); + constraints = [mediaConstraints]; + constraints.unshift(constraints_); + getMedia(recursive.apply(undefined, constraints)); + }, guid); + } + } else { + setTimeout(start, 0); + } + this.on('_dispose', function () { + if (localVideo) { + localVideo.pause(); + localVideo.src = ''; + localVideo.load(); + localVideo.muted = false; + } + if (remoteVideo) { + remoteVideo.pause(); + remoteVideo.src = ''; + remoteVideo.load(); + } + self.removeAllListeners(); + if (window.cancelChooseDesktopMedia !== undefined) { + window.cancelChooseDesktopMedia(guid); + } + }); +} +inherits(WebRtcPeer, EventEmitter); +function createEnableDescriptor(type) { + var method = 'get' + type + 'Tracks'; + return { + enumerable: true, + get: function () { + if (!this.peerConnection) + return; + var streams = this.peerConnection.getLocalStreams(); + if (!streams.length) + return; + for (var i = 0, stream; stream = streams[i]; i++) { + var tracks = stream[method](); + for (var j = 0, track; track = tracks[j]; j++) + if (!track.enabled) + return false; + } + return true; + }, + set: function (value) { + function trackSetEnable(track) { + track.enabled = value; + } + this.peerConnection.getLocalStreams().forEach(function (stream) { + stream[method]().forEach(trackSetEnable); + }); + } + }; +} +Object.defineProperties(WebRtcPeer.prototype, { + 'enabled': { + enumerable: true, + get: function () { + return this.audioEnabled && this.videoEnabled; + }, + set: function (value) { + this.audioEnabled = this.videoEnabled = value; + } + }, + 'audioEnabled': createEnableDescriptor('Audio'), + 'videoEnabled': createEnableDescriptor('Video') +}); +WebRtcPeer.prototype.getLocalStream = function (index) { + if (this.peerConnection) { + return this.peerConnection.getLocalStreams()[index || 0]; + } +}; +WebRtcPeer.prototype.getRemoteStream = function (index) { + if (this.peerConnection) { + return this.peerConnection.getRemoteStreams()[index || 0]; + } +}; +WebRtcPeer.prototype.dispose = function () { + logger.debug('Disposing WebRtcPeer'); + var pc = this.peerConnection; + var dc = this.dataChannel; + try { + if (dc) { + if (dc.signalingState === 'closed') + return; + dc.close(); + } + if (pc) { + if (pc.signalingState === 'closed') + return; + pc.getLocalStreams().forEach(streamStop); + pc.close(); + } + } catch (err) { + logger.warn('Exception disposing webrtc peer ' + err); + } + this.emit('_dispose'); +}; +function WebRtcPeerRecvonly(options, callback) { + if (!(this instanceof WebRtcPeerRecvonly)) { + return new WebRtcPeerRecvonly(options, callback); + } + WebRtcPeerRecvonly.super_.call(this, 'recvonly', options, callback); +} +inherits(WebRtcPeerRecvonly, WebRtcPeer); +function WebRtcPeerSendonly(options, callback) { + if (!(this instanceof WebRtcPeerSendonly)) { + return new WebRtcPeerSendonly(options, callback); + } + WebRtcPeerSendonly.super_.call(this, 'sendonly', options, callback); +} +inherits(WebRtcPeerSendonly, WebRtcPeer); +function WebRtcPeerSendrecv(options, callback) { + if (!(this instanceof WebRtcPeerSendrecv)) { + return new WebRtcPeerSendrecv(options, callback); + } + WebRtcPeerSendrecv.super_.call(this, 'sendrecv', options, callback); +} +inherits(WebRtcPeerSendrecv, WebRtcPeer); +function harkUtils(stream, options) { + return hark(stream, options); +} +exports.bufferizeCandidates = bufferizeCandidates; +exports.WebRtcPeerRecvonly = WebRtcPeerRecvonly; +exports.WebRtcPeerSendonly = WebRtcPeerSendonly; +exports.WebRtcPeerSendrecv = WebRtcPeerSendrecv; +exports.hark = harkUtils; +},{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid":23}],2:[function(require,module,exports){ +if (window.addEventListener) + module.exports = require('./index'); +},{"./index":3}],3:[function(require,module,exports){ +var WebRtcPeer = require('./WebRtcPeer'); +exports.WebRtcPeer = WebRtcPeer; +},{"./WebRtcPeer":1}],4:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + // At least give some kind of context to the user + var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); + err.context = er; + throw err; + } + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + } else if (isObject(handler)) { + args = Array.prototype.slice.call(arguments, 1); + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else if (listeners) { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.prototype.listenerCount = function(type) { + if (this._events) { + var evlistener = this._events[type]; + + if (isFunction(evlistener)) + return 1; + else if (evlistener) + return evlistener.length; + } + return 0; +}; + +EventEmitter.listenerCount = function(emitter, type) { + return emitter.listenerCount(type); +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],5:[function(require,module,exports){ +/* jshint node: true */ +'use strict'; + +var normalice = require('normalice'); + +/** + # freeice + + The `freeice` module is a simple way of getting random STUN or TURN server + for your WebRTC application. The list of servers (just STUN at this stage) + were sourced from this [gist](https://gist.github.com/zziuni/3741933). + + ## Example Use + + The following demonstrates how you can use `freeice` with + [rtc-quickconnect](https://github.com/rtc-io/rtc-quickconnect): + + <<< examples/quickconnect.js + + As the `freeice` module generates ice servers in a list compliant with the + WebRTC spec you will be able to use it with raw `RTCPeerConnection` + constructors and other WebRTC libraries. + + ## Hey, don't use my STUN/TURN server! + + If for some reason your free STUN or TURN server ends up in the + list of servers ([stun](https://github.com/DamonOehlman/freeice/blob/master/stun.json) or + [turn](https://github.com/DamonOehlman/freeice/blob/master/turn.json)) + that is used in this module, you can feel + free to open an issue on this repository and those servers will be removed + within 24 hours (or sooner). This is the quickest and probably the most + polite way to have something removed (and provides us some visibility + if someone opens a pull request requesting that a server is added). + + ## Please add my server! + + If you have a server that you wish to add to the list, that's awesome! I'm + sure I speak on behalf of a whole pile of WebRTC developers who say thanks. + To get it into the list, feel free to either open a pull request or if you + find that process a bit daunting then just create an issue requesting + the addition of the server (make sure you provide all the details, and if + you have a Terms of Service then including that in the PR/issue would be + awesome). + + ## I know of a free server, can I add it? + + Sure, if you do your homework and make sure it is ok to use (I'm currently + in the process of reviewing the terms of those STUN servers included from + the original list). If it's ok to go, then please see the previous entry + for how to add it. + + ## Current List of Servers + + * current as at the time of last `README.md` file generation + + ### STUN + + <<< stun.json + + ### TURN + + <<< turn.json + +**/ + +var freeice = module.exports = function(opts) { + // if a list of servers has been provided, then use it instead of defaults + var servers = { + stun: (opts || {}).stun || require('./stun.json'), + turn: (opts || {}).turn || require('./turn.json') + }; + + var stunCount = (opts || {}).stunCount || 2; + var turnCount = (opts || {}).turnCount || 0; + var selected; + + function getServers(type, count) { + var out = []; + var input = [].concat(servers[type]); + var idx; + + while (input.length && out.length < count) { + idx = (Math.random() * input.length) | 0; + out = out.concat(input.splice(idx, 1)); + } + + return out.map(function(url) { + //If it's a not a string, don't try to "normalice" it otherwise using type:url will screw it up + if ((typeof url !== 'string') && (! (url instanceof String))) { + return url; + } else { + return normalice(type + ':' + url); + } + }); + } + + // add stun servers + selected = [].concat(getServers('stun', stunCount)); + + if (turnCount) { + selected = selected.concat(getServers('turn', turnCount)); + } + + return selected; +}; + +},{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){ +module.exports=[ + "stun.l.google.com:19302", + "stun1.l.google.com:19302", + "stun2.l.google.com:19302", + "stun3.l.google.com:19302", + "stun4.l.google.com:19302", + "stun.ekiga.net", + "stun.ideasip.com", + "stun.schlund.de", + "stun.stunprotocol.org:3478", + "stun.voiparound.com", + "stun.voipbuster.com", + "stun.voipstunt.com", + "stun.voxgratia.org", + "stun.services.mozilla.com" +] + +},{}],7:[function(require,module,exports){ +module.exports=[] + +},{}],8:[function(require,module,exports){ +var WildEmitter = require('wildemitter'); + +function getMaxVolume (analyser, fftBins) { + var maxVolume = -Infinity; + analyser.getFloatFrequencyData(fftBins); + + for(var i=4, ii=fftBins.length; i < ii; i++) { + if (fftBins[i] > maxVolume && fftBins[i] < 0) { + maxVolume = fftBins[i]; + } + }; + + return maxVolume; +} + + +var audioContextType = window.AudioContext || window.webkitAudioContext; +// use a single audio context due to hardware limits +var audioContext = null; +module.exports = function(stream, options) { + var harker = new WildEmitter(); + + + // make it not break in non-supported browsers + if (!audioContextType) return harker; + + //Config + var options = options || {}, + smoothing = (options.smoothing || 0.1), + interval = (options.interval || 50), + threshold = options.threshold, + play = options.play, + history = options.history || 10, + running = true; + + //Setup Audio Context + if (!audioContext) { + audioContext = new audioContextType(); + } + var sourceNode, fftBins, analyser; + + analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = smoothing; + fftBins = new Float32Array(analyser.fftSize); + + if (stream.jquery) stream = stream[0]; + if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) { + //Audio Tag + sourceNode = audioContext.createMediaElementSource(stream); + if (typeof play === 'undefined') play = true; + threshold = threshold || -50; + } else { + //WebRTC Stream + sourceNode = audioContext.createMediaStreamSource(stream); + threshold = threshold || -50; + } + + sourceNode.connect(analyser); + if (play) analyser.connect(audioContext.destination); + + harker.speaking = false; + + harker.setThreshold = function(t) { + threshold = t; + }; + + harker.setInterval = function(i) { + interval = i; + }; + + harker.stop = function() { + running = false; + harker.emit('volume_change', -100, threshold); + if (harker.speaking) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + }; + harker.speakingHistory = []; + for (var i = 0; i < history; i++) { + harker.speakingHistory.push(0); + } + + // Poll the analyser node to determine if speaking + // and emit events if changed + var looper = function() { + setTimeout(function() { + + //check if stop has been called + if(!running) { + return; + } + + var currentVolume = getMaxVolume(analyser, fftBins); + + harker.emit('volume_change', currentVolume, threshold); + + var history = 0; + if (currentVolume > threshold && !harker.speaking) { + // trigger quickly, short history + for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history >= 2) { + harker.speaking = true; + harker.emit('speaking'); + } + } else if (currentVolume < threshold && harker.speaking) { + for (var i = 0; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history == 0) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + } + harker.speakingHistory.shift(); + harker.speakingHistory.push(0 + (currentVolume > threshold)); + + looper(); + }, interval); + }; + looper(); + + + return harker; +} + +},{"wildemitter":24}],9:[function(require,module,exports){ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],10:[function(require,module,exports){ +// Does nothing at all. + +},{}],11:[function(require,module,exports){ +/*! + * @name JavaScript/NodeJS Merge v1.2.0 + * @author yeikos + * @repository https://github.com/yeikos/js.merge + + * Copyright 2014 yeikos - MIT license + * https://raw.github.com/yeikos/js.merge/master/LICENSE + */ + +;(function(isNode) { + + /** + * Merge one or more objects + * @param bool? clone + * @param mixed,... arguments + * @return object + */ + + var Public = function(clone) { + + return merge(clone === true, false, arguments); + + }, publicName = 'merge'; + + /** + * Merge two or more objects recursively + * @param bool? clone + * @param mixed,... arguments + * @return object + */ + + Public.recursive = function(clone) { + + return merge(clone === true, true, arguments); + + }; + + /** + * Clone the input removing any reference + * @param mixed input + * @return mixed + */ + + Public.clone = function(input) { + + var output = input, + type = typeOf(input), + index, size; + + if (type === 'array') { + + output = []; + size = input.length; + + for (index=0;index<size;++index) + + output[index] = Public.clone(input[index]); + + } else if (type === 'object') { + + output = {}; + + for (index in input) + + output[index] = Public.clone(input[index]); + + } + + return output; + + }; + + /** + * Merge two objects recursively + * @param mixed input + * @param mixed extend + * @return mixed + */ + + function merge_recursive(base, extend) { + + if (typeOf(base) !== 'object') + + return extend; + + for (var key in extend) { + + if (typeOf(base[key]) === 'object' && typeOf(extend[key]) === 'object') { + + base[key] = merge_recursive(base[key], extend[key]); + + } else { + + base[key] = extend[key]; + + } + + } + + return base; + + } + + /** + * Merge two or more objects + * @param bool clone + * @param bool recursive + * @param array argv + * @return object + */ + + function merge(clone, recursive, argv) { + + var result = argv[0], + size = argv.length; + + if (clone || typeOf(result) !== 'object') + + result = {}; + + for (var index=0;index<size;++index) { + + var item = argv[index], + + type = typeOf(item); + + if (type !== 'object') continue; + + for (var key in item) { + + var sitem = clone ? Public.clone(item[key]) : item[key]; + + if (recursive) { + + result[key] = merge_recursive(result[key], sitem); + + } else { + + result[key] = sitem; + + } + + } + + } + + return result; + + } + + /** + * Get type of variable + * @param mixed input + * @return string + * + * @see http://jsperf.com/typeofvar + */ + + function typeOf(input) { + + return ({}).toString.call(input).slice(8, -1).toLowerCase(); + + } + + if (isNode) { + + module.exports = Public; + + } else { + + window[publicName] = Public; + + } + +})(typeof module === 'object' && module && typeof module.exports === 'object' && module.exports); +},{}],12:[function(require,module,exports){ +/** + # normalice + + Normalize an ice server configuration object (or plain old string) into a format + that is usable in all browsers supporting WebRTC. Primarily this module is designed + to help with the transition of the `url` attribute of the configuration object to + the `urls` attribute. + + ## Example Usage + + <<< examples/simple.js + +**/ + +var protocols = [ + 'stun:', + 'turn:' +]; + +module.exports = function(input) { + var url = (input || {}).url || input; + var protocol; + var parts; + var output = {}; + + // if we don't have a string url, then allow the input to passthrough + if (typeof url != 'string' && (! (url instanceof String))) { + return input; + } + + // trim the url string, and convert to an array + url = url.trim(); + + // if the protocol is not known, then passthrough + protocol = protocols[protocols.indexOf(url.slice(0, 5))]; + if (! protocol) { + return input; + } + + // now let's attack the remaining url parts + url = url.slice(5); + parts = url.split('@'); + + output.username = input.username; + output.credential = input.credential; + // if we have an authentication part, then set the credentials + if (parts.length > 1) { + url = parts[1]; + parts = parts[0].split(':'); + + // add the output credential and username + output.username = parts[0]; + output.credential = (input || {}).credential || parts[1] || ''; + } + + output.url = protocol + url; + output.urls = [ output.url ]; + + return output; +}; + +},{}],13:[function(require,module,exports){ +var grammar = module.exports = { + v: [{ + name: 'version', + reg: /^(\d*)$/ + }], + o: [{ //o=- 20518 0 IN IP4 203.0.113.1 + // NB: sessionId will be a String in most cases because it is huge + name: 'origin', + reg: /^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/, + names: ['username', 'sessionId', 'sessionVersion', 'netType', 'ipVer', 'address'], + format: "%s %s %d %s IP%d %s" + }], + // default parsing of these only (though some of these feel outdated) + s: [{ name: 'name' }], + i: [{ name: 'description' }], + u: [{ name: 'uri' }], + e: [{ name: 'email' }], + p: [{ name: 'phone' }], + z: [{ name: 'timezones' }], // TODO: this one can actually be parsed properly.. + r: [{ name: 'repeats' }], // TODO: this one can also be parsed properly + //k: [{}], // outdated thing ignored + t: [{ //t=0 0 + name: 'timing', + reg: /^(\d*) (\d*)/, + names: ['start', 'stop'], + format: "%d %d" + }], + c: [{ //c=IN IP4 10.47.197.26 + name: 'connection', + reg: /^IN IP(\d) (\S*)/, + names: ['version', 'ip'], + format: "IN IP%d %s" + }], + b: [{ //b=AS:4000 + push: 'bandwidth', + reg: /^(TIAS|AS|CT|RR|RS):(\d*)/, + names: ['type', 'limit'], + format: "%s:%s" + }], + m: [{ //m=video 51744 RTP/AVP 126 97 98 34 31 + // NB: special - pushes to session + // TODO: rtp/fmtp should be filtered by the payloads found here? + reg: /^(\w*) (\d*) ([\w\/]*)(?: (.*))?/, + names: ['type', 'port', 'protocol', 'payloads'], + format: "%s %d %s %s" + }], + a: [ + { //a=rtpmap:110 opus/48000/2 + push: 'rtp', + reg: /^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/, + names: ['payload', 'codec', 'rate', 'encoding'], + format: function (o) { + return (o.encoding) ? + "rtpmap:%d %s/%s/%s": + o.rate ? + "rtpmap:%d %s/%s": + "rtpmap:%d %s"; + } + }, + { + //a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 + //a=fmtp:111 minptime=10; useinbandfec=1 + push: 'fmtp', + reg: /^fmtp:(\d*) ([\S| ]*)/, + names: ['payload', 'config'], + format: "fmtp:%d %s" + }, + { //a=control:streamid=0 + name: 'control', + reg: /^control:(.*)/, + format: "control:%s" + }, + { //a=rtcp:65179 IN IP4 193.84.77.194 + name: 'rtcp', + reg: /^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/, + names: ['port', 'netType', 'ipVer', 'address'], + format: function (o) { + return (o.address != null) ? + "rtcp:%d %s IP%d %s": + "rtcp:%d"; + } + }, + { //a=rtcp-fb:98 trr-int 100 + push: 'rtcpFbTrrInt', + reg: /^rtcp-fb:(\*|\d*) trr-int (\d*)/, + names: ['payload', 'value'], + format: "rtcp-fb:%d trr-int %d" + }, + { //a=rtcp-fb:98 nack rpsi + push: 'rtcpFb', + reg: /^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/, + names: ['payload', 'type', 'subtype'], + format: function (o) { + return (o.subtype != null) ? + "rtcp-fb:%s %s %s": + "rtcp-fb:%s %s"; + } + }, + { //a=extmap:2 urn:ietf:params:rtp-hdrext:toffset + //a=extmap:1/recvonly URI-gps-string + push: 'ext', + reg: /^extmap:([\w_\/]*) (\S*)(?: (\S*))?/, + names: ['value', 'uri', 'config'], // value may include "/direction" suffix + format: function (o) { + return (o.config != null) ? + "extmap:%s %s %s": + "extmap:%s %s"; + } + }, + { + //a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 + push: 'crypto', + reg: /^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/, + names: ['id', 'suite', 'config', 'sessionConfig'], + format: function (o) { + return (o.sessionConfig != null) ? + "crypto:%d %s %s %s": + "crypto:%d %s %s"; + } + }, + { //a=setup:actpass + name: 'setup', + reg: /^setup:(\w*)/, + format: "setup:%s" + }, + { //a=mid:1 + name: 'mid', + reg: /^mid:([^\s]*)/, + format: "mid:%s" + }, + { //a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a + name: 'msid', + reg: /^msid:(.*)/, + format: "msid:%s" + }, + { //a=ptime:20 + name: 'ptime', + reg: /^ptime:(\d*)/, + format: "ptime:%d" + }, + { //a=maxptime:60 + name: 'maxptime', + reg: /^maxptime:(\d*)/, + format: "maxptime:%d" + }, + { //a=sendrecv + name: 'direction', + reg: /^(sendrecv|recvonly|sendonly|inactive)/ + }, + { //a=ice-lite + name: 'icelite', + reg: /^(ice-lite)/ + }, + { //a=ice-ufrag:F7gI + name: 'iceUfrag', + reg: /^ice-ufrag:(\S*)/, + format: "ice-ufrag:%s" + }, + { //a=ice-pwd:x9cml/YzichV2+XlhiMu8g + name: 'icePwd', + reg: /^ice-pwd:(\S*)/, + format: "ice-pwd:%s" + }, + { //a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 + name: 'fingerprint', + reg: /^fingerprint:(\S*) (\S*)/, + names: ['type', 'hash'], + format: "fingerprint:%s %s" + }, + { + //a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host + //a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 + //a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 + //a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 + //a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 + push:'candidates', + reg: /^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/, + names: ['foundation', 'component', 'transport', 'priority', 'ip', 'port', 'type', 'raddr', 'rport', 'tcptype', 'generation'], + format: function (o) { + var str = "candidate:%s %d %s %d %s %d typ %s"; + + str += (o.raddr != null) ? " raddr %s rport %d" : "%v%v"; + + // NB: candidate has three optional chunks, so %void middles one if it's missing + str += (o.tcptype != null) ? " tcptype %s" : "%v"; + + if (o.generation != null) { + str += " generation %d"; + } + return str; + } + }, + { //a=end-of-candidates (keep after the candidates line for readability) + name: 'endOfCandidates', + reg: /^(end-of-candidates)/ + }, + { //a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... + name: 'remoteCandidates', + reg: /^remote-candidates:(.*)/, + format: "remote-candidates:%s" + }, + { //a=ice-options:google-ice + name: 'iceOptions', + reg: /^ice-options:(\S*)/, + format: "ice-options:%s" + }, + { //a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 + push: "ssrcs", + reg: /^ssrc:(\d*) ([\w_]*):(.*)/, + names: ['id', 'attribute', 'value'], + format: "ssrc:%d %s:%s" + }, + { //a=ssrc-group:FEC 1 2 + push: "ssrcGroups", + reg: /^ssrc-group:(\w*) (.*)/, + names: ['semantics', 'ssrcs'], + format: "ssrc-group:%s %s" + }, + { //a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV + name: "msidSemantic", + reg: /^msid-semantic:\s?(\w*) (\S*)/, + names: ['semantic', 'token'], + format: "msid-semantic: %s %s" // space after ":" is not accidental + }, + { //a=group:BUNDLE audio video + push: 'groups', + reg: /^group:(\w*) (.*)/, + names: ['type', 'mids'], + format: "group:%s %s" + }, + { //a=rtcp-mux + name: 'rtcpMux', + reg: /^(rtcp-mux)/ + }, + { //a=rtcp-rsize + name: 'rtcpRsize', + reg: /^(rtcp-rsize)/ + }, + { // any a= that we don't understand is kepts verbatim on media.invalid + push: 'invalid', + names: ["value"] + } + ] +}; + +// set sensible defaults to avoid polluting the grammar with boring details +Object.keys(grammar).forEach(function (key) { + var objs = grammar[key]; + objs.forEach(function (obj) { + if (!obj.reg) { + obj.reg = /(.*)/; + } + if (!obj.format) { + obj.format = "%s"; + } + }); +}); + +},{}],14:[function(require,module,exports){ +var parser = require('./parser'); +var writer = require('./writer'); + +exports.write = writer; +exports.parse = parser.parse; +exports.parseFmtpConfig = parser.parseFmtpConfig; +exports.parsePayloads = parser.parsePayloads; +exports.parseRemoteCandidates = parser.parseRemoteCandidates; + +},{"./parser":15,"./writer":16}],15:[function(require,module,exports){ +var toIntIfInt = function (v) { + return String(Number(v)) === v ? Number(v) : v; +}; + +var attachProperties = function (match, location, names, rawName) { + if (rawName && !names) { + location[rawName] = toIntIfInt(match[1]); + } + else { + for (var i = 0; i < names.length; i += 1) { + if (match[i+1] != null) { + location[names[i]] = toIntIfInt(match[i+1]); + } + } + } +}; + +var parseReg = function (obj, location, content) { + var needsBlank = obj.name && obj.names; + if (obj.push && !location[obj.push]) { + location[obj.push] = []; + } + else if (needsBlank && !location[obj.name]) { + location[obj.name] = {}; + } + var keyLocation = obj.push ? + {} : // blank object that will be pushed + needsBlank ? location[obj.name] : location; // otherwise, named location or root + + attachProperties(content.match(obj.reg), keyLocation, obj.names, obj.name); + + if (obj.push) { + location[obj.push].push(keyLocation); + } +}; + +var grammar = require('./grammar'); +var validLine = RegExp.prototype.test.bind(/^([a-z])=(.*)/); + +exports.parse = function (sdp) { + var session = {} + , media = [] + , location = session; // points at where properties go under (one of the above) + + // parse lines we understand + sdp.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function (l) { + var type = l[0]; + var content = l.slice(2); + if (type === 'm') { + media.push({rtp: [], fmtp: []}); + location = media[media.length-1]; // point at latest media line + } + + for (var j = 0; j < (grammar[type] || []).length; j += 1) { + var obj = grammar[type][j]; + if (obj.reg.test(content)) { + return parseReg(obj, location, content); + } + } + }); + + session.media = media; // link it up + return session; +}; + +var fmtpReducer = function (acc, expr) { + var s = expr.split('='); + if (s.length === 2) { + acc[s[0]] = toIntIfInt(s[1]); + } + return acc; +}; + +exports.parseFmtpConfig = function (str) { + return str.split(/\;\s?/).reduce(fmtpReducer, {}); +}; + +exports.parsePayloads = function (str) { + return str.split(' ').map(Number); +}; + +exports.parseRemoteCandidates = function (str) { + var candidates = []; + var parts = str.split(' ').map(toIntIfInt); + for (var i = 0; i < parts.length; i += 3) { + candidates.push({ + component: parts[i], + ip: parts[i + 1], + port: parts[i + 2] + }); + } + return candidates; +}; + +},{"./grammar":13}],16:[function(require,module,exports){ +var grammar = require('./grammar'); + +// customized util.format - discards excess arguments and can void middle ones +var formatRegExp = /%[sdv%]/g; +var format = function (formatStr) { + var i = 1; + var args = arguments; + var len = args.length; + return formatStr.replace(formatRegExp, function (x) { + if (i >= len) { + return x; // missing argument + } + var arg = args[i]; + i += 1; + switch (x) { + case '%%': + return '%'; + case '%s': + return String(arg); + case '%d': + return Number(arg); + case '%v': + return ''; + } + }); + // NB: we discard excess arguments - they are typically undefined from makeLine +}; + +var makeLine = function (type, obj, location) { + var str = obj.format instanceof Function ? + (obj.format(obj.push ? location : location[obj.name])) : + obj.format; + + var args = [type + '=' + str]; + if (obj.names) { + for (var i = 0; i < obj.names.length; i += 1) { + var n = obj.names[i]; + if (obj.name) { + args.push(location[obj.name][n]); + } + else { // for mLine and push attributes + args.push(location[obj.names[i]]); + } + } + } + else { + args.push(location[obj.name]); + } + return format.apply(null, args); +}; + +// RFC specified order +// TODO: extend this with all the rest +var defaultOuterOrder = [ + 'v', 'o', 's', 'i', + 'u', 'e', 'p', 'c', + 'b', 't', 'r', 'z', 'a' +]; +var defaultInnerOrder = ['i', 'c', 'b', 'a']; + + +module.exports = function (session, opts) { + opts = opts || {}; + // ensure certain properties exist + if (session.version == null) { + session.version = 0; // "v=0" must be there (only defined version atm) + } + if (session.name == null) { + session.name = " "; // "s= " must be there if no meaningful name set + } + session.media.forEach(function (mLine) { + if (mLine.payloads == null) { + mLine.payloads = ""; + } + }); + + var outerOrder = opts.outerOrder || defaultOuterOrder; + var innerOrder = opts.innerOrder || defaultInnerOrder; + var sdp = []; + + // loop through outerOrder for matching properties on session + outerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in session && session[obj.name] != null) { + sdp.push(makeLine(type, obj, session)); + } + else if (obj.push in session && session[obj.push] != null) { + session[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + + // then for each media line, follow the innerOrder + session.media.forEach(function (mLine) { + sdp.push(makeLine('m', grammar.m[0], mLine)); + + innerOrder.forEach(function (type) { + grammar[type].forEach(function (obj) { + if (obj.name in mLine && mLine[obj.name] != null) { + sdp.push(makeLine(type, obj, mLine)); + } + else if (obj.push in mLine && mLine[obj.push] != null) { + mLine[obj.push].forEach(function (el) { + sdp.push(makeLine(type, obj, el)); + }); + } + }); + }); + }); + + return sdp.join('\r\n') + '\r\n'; +}; + +},{"./grammar":13}],17:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = function arrayEquals(array) { + // if the other array is a falsy value, return + if (!array) + return false; + + // compare lengths - can save a lot of time + if (this.length != array.length) + return false; + + for (var i = 0, l = this.length; i < l; i++) { + // Check if we have nested arrays + if (this[i] instanceof Array && array[i] instanceof Array) { + // recurse into the nested arrays + if (!arrayEquals.apply(this[i], [array[i]])) + return false; + } else if (this[i] != array[i]) { + // Warning - two different object instances will never be equal: + // {x:20} != {x:20} + return false; + } + } + return true; +}; + + +},{}],18:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +exports.Interop = require('./interop'); + +},{"./interop":19}],19:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* global RTCSessionDescription */ +/* global RTCIceCandidate */ +/* jshint -W097 */ +"use strict"; + +var transform = require('./transform'); +var arrayEquals = require('./array-equals'); + +function Interop() { + + /** + * This map holds the most recent Unified Plan offer/answer SDP that was + * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and + * the SDP string as values. + * + * @type {{}} + */ + this.cache = { + mlB2UMap : {}, + mlU2BMap : {} + }; +} + +module.exports = Interop; + +/** + * Changes the candidate args to match with the related Unified Plan + */ +Interop.prototype.candidateToUnifiedPlan = function(candidate) { + var cand = new RTCIceCandidate(candidate); + + cand.sdpMLineIndex = this.cache.mlB2UMap[cand.sdpMLineIndex]; + /* TODO: change sdpMid to (audio|video)-SSRC */ + + return cand; +}; + +/** + * Changes the candidate args to match with the related Plan B + */ +Interop.prototype.candidateToPlanB = function(candidate) { + var cand = new RTCIceCandidate(candidate); + + if (cand.sdpMid.indexOf('audio') === 0) { + cand.sdpMid = 'audio'; + } else if (cand.sdpMid.indexOf('video') === 0) { + cand.sdpMid = 'video'; + } else { + throw new Error('candidate with ' + cand.sdpMid + ' not allowed'); + } + + cand.sdpMLineIndex = this.cache.mlU2BMap[cand.sdpMLineIndex]; + + return cand; +}; + +/** + * Returns the index of the first m-line with the given media type and with a + * direction which allows sending, in the last Unified Plan description with + * type "answer" converted to Plan B. Returns {null} if there is no saved + * answer, or if none of its m-lines with the given type allow sending. + * @param type the media type ("audio" or "video"). + * @returns {*} + */ +Interop.prototype.getFirstSendingIndexFromAnswer = function(type) { + if (!this.cache.answer) { + return null; + } + + var session = transform.parse(this.cache.answer); + if (session && session.media && Array.isArray(session.media)){ + for (var i = 0; i < session.media.length; i++) { + if (session.media[i].type == type && + (!session.media[i].direction /* default to sendrecv */ || + session.media[i].direction === 'sendrecv' || + session.media[i].direction === 'sendonly')){ + return i; + } + } + } + + return null; +}; + +/** + * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A + * PeerConnection wrapper transforms the SDP to Plan B before passing it to the + * application. + * + * @param desc + * @returns {*} + */ +Interop.prototype.toPlanB = function(desc) { + var self = this; + //#region Preliminary input validation. + + if (typeof desc !== 'object' || desc === null || + typeof desc.sdp !== 'string') { + console.warn('An empty description was passed as an argument.'); + return desc; + } + + // Objectify the SDP for easier manipulation. + var session = transform.parse(desc.sdp); + + // If the SDP contains no media, there's nothing to transform. + if (typeof session.media === 'undefined' || + !Array.isArray(session.media) || session.media.length === 0) { + console.warn('The description has no media.'); + return desc; + } + + // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B + // SDP has a video, an audio and a data "channel" at most. + if (session.media.length <= 3 && session.media.every(function(m) { + return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; + })) { + console.warn('This description does not look like Unified Plan.'); + return desc; + } + + //#endregion + + // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443 + var sdp = desc.sdp; + var rewrite = false; + for (var i = 0; i < session.media.length; i++) { + var uLine = session.media[i]; + uLine.rtp.forEach(function(rtp) { + if (rtp.codec === 'NULL') + { + rewrite = true; + var offer = transform.parse(self.cache.offer); + rtp.codec = offer.media[i].rtp[0].codec; + } + }); + } + if (rewrite) { + sdp = transform.write(session); + } + + // Unified Plan SDP is our "precious". Cache it for later use in the Plan B + // -> Unified Plan transformation. + this.cache[desc.type] = sdp; + + //#region Convert from Unified Plan to Plan B. + + // We rebuild the session.media array. + var media = session.media; + session.media = []; + + // Associative array that maps channel types to channel objects for fast + // access to channel objects by their type, e.g. type2bl['audio']->channel + // obj. + var type2bl = {}; + + // Used to build the group:BUNDLE value after the channels construction + // loop. + var types = []; + + media.forEach(function(uLine) { + // rtcp-mux is required in the Plan B SDP. + if ((typeof uLine.rtcpMux !== 'string' || + uLine.rtcpMux !== 'rtcp-mux') && + uLine.direction !== 'inactive') { + throw new Error('Cannot convert to Plan B because m-lines ' + + 'without the rtcp-mux attribute were found.'); + } + + // If we don't have a channel for this uLine.type OR the selected is + // inactive, then select this uLine as the channel basis. + if (typeof type2bl[uLine.type] === 'undefined' || + type2bl[uLine.type].direction === 'inactive') { + type2bl[uLine.type] = uLine; + } + + if (uLine.protocol != type2bl[uLine.type].protocol) { + throw new Error('Cannot convert to Plan B because m-lines ' + + 'have different protocols and this library does not have ' + + 'support for that'); + } + + if (uLine.payloads != type2bl[uLine.type].payloads) { + throw new Error('Cannot convert to Plan B because m-lines ' + + 'have different payloads and this library does not have ' + + 'support for that'); + } + + }); + + // Implode the Unified Plan m-lines/tracks into Plan B channels. + media.forEach(function(uLine) { + if (uLine.type === 'application') { + session.media.push(uLine); + types.push(uLine.mid); + return; + } + + // Add sources to the channel and handle a=msid. + if (typeof uLine.sources === 'object') { + Object.keys(uLine.sources).forEach(function(ssrc) { + if (typeof type2bl[uLine.type].sources !== 'object') + type2bl[uLine.type].sources = {}; + + // Assign the sources to the channel. + type2bl[uLine.type].sources[ssrc] = + uLine.sources[ssrc]; + + if (typeof uLine.msid !== 'undefined') { + // In Plan B the msid is an SSRC attribute. Also, we don't + // care about the obsolete label and mslabel attributes. + // + // Note that it is not guaranteed that the uLine will + // have an msid. recvonly channels in particular don't have + // one. + type2bl[uLine.type].sources[ssrc].msid = + uLine.msid; + } + // NOTE ssrcs in ssrc groups will share msids, as + // draft-uberti-rtcweb-plan-00 mandates. + }); + } + + // Add ssrc groups to the channel. + if (typeof uLine.ssrcGroups !== 'undefined' && + Array.isArray(uLine.ssrcGroups)) { + + // Create the ssrcGroups array, if it's not defined. + if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' || + !Array.isArray(type2bl[uLine.type].ssrcGroups)) { + type2bl[uLine.type].ssrcGroups = []; + } + + type2bl[uLine.type].ssrcGroups = + type2bl[uLine.type].ssrcGroups.concat( + uLine.ssrcGroups); + } + + if (type2bl[uLine.type] === uLine) { + // Plan B mids are in ['audio', 'video', 'data'] + uLine.mid = uLine.type; + + // Plan B doesn't support/need the bundle-only attribute. + delete uLine.bundleOnly; + + // In Plan B the msid is an SSRC attribute. + delete uLine.msid; + + if (uLine.type == media[0].type) { + types.unshift(uLine.type); + // Add the channel to the new media array. + session.media.unshift(uLine); + } else { + types.push(uLine.type); + // Add the channel to the new media array. + session.media.push(uLine); + } + } + }); + + if (typeof session.groups !== 'undefined') { + // We regenerate the BUNDLE group with the new mids. + session.groups.some(function(group) { + if (group.type === 'BUNDLE') { + group.mids = types.join(' '); + return true; + } + }); + } + + // msid semantic + session.msidSemantic = { + semantic: 'WMS', + token: '*' + }; + + var resStr = transform.write(session); + + return new RTCSessionDescription({ + type: desc.type, + sdp: resStr + }); + + //#endregion +}; + +/* follow rules defined in RFC4145 */ +function addSetupAttr(uLine) { + if (typeof uLine.setup === 'undefined') { + return; + } + + if (uLine.setup === "active") { + uLine.setup = "passive"; + } else if (uLine.setup === "passive") { + uLine.setup = "active"; + } +} + +/** + * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A + * PeerConnection wrapper transforms the SDP to Unified Plan before passing it + * to FF. + * + * @param desc + * @returns {*} + */ +Interop.prototype.toUnifiedPlan = function(desc) { + var self = this; + //#region Preliminary input validation. + + if (typeof desc !== 'object' || desc === null || + typeof desc.sdp !== 'string') { + console.warn('An empty description was passed as an argument.'); + return desc; + } + + var session = transform.parse(desc.sdp); + + // If the SDP contains no media, there's nothing to transform. + if (typeof session.media === 'undefined' || + !Array.isArray(session.media) || session.media.length === 0) { + console.warn('The description has no media.'); + return desc; + } + + // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has + // a video, an audio and a data "channel" at most. + if (session.media.length > 3 || !session.media.every(function(m) { + return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; + })) { + console.warn('This description does not look like Plan B.'); + return desc; + } + + // Make sure this Plan B SDP can be converted to a Unified Plan SDP. + var mids = []; + session.media.forEach(function(m) { + mids.push(m.mid); + }); + + var hasBundle = false; + if (typeof session.groups !== 'undefined' && + Array.isArray(session.groups)) { + hasBundle = session.groups.every(function(g) { + return g.type !== 'BUNDLE' || + arrayEquals.apply(g.mids.sort(), [mids.sort()]); + }); + } + + if (!hasBundle) { + var mustBeBundle = false; + + session.media.forEach(function(m) { + if (m.direction !== 'inactive') { + mustBeBundle = true; + } + }); + + if (mustBeBundle) { + throw new Error("Cannot convert to Unified Plan because m-lines that" + + " are not bundled were found."); + } + } + + //#endregion + + + //#region Convert from Plan B to Unified Plan. + + // Unfortunately, a Plan B offer/answer doesn't have enough information to + // rebuild an equivalent Unified Plan offer/answer. + // + // For example, if this is a local answer (in Unified Plan style) that we + // convert to Plan B prior to handing it over to the application (the + // PeerConnection wrapper called us, for instance, after a successful + // createAnswer), we want to remember the m-line at which we've seen the + // (local) SSRC. That's because when the application wants to do call the + // SLD method, forcing us to do the inverse transformation (from Plan B to + // Unified Plan), we need to know to which m-line to assign the (local) + // SSRC. We also need to know all the other m-lines that the original + // answer had and include them in the transformed answer as well. + // + // Another example is if this is a remote offer that we convert to Plan B + // prior to giving it to the application, we want to remember the mid at + // which we've seen the (remote) SSRC. + // + // In the iteration that follows, we use the cached Unified Plan (if it + // exists) to assign mids to ssrcs. + + var type; + if (desc.type === 'answer') { + type = 'offer'; + } else if (desc.type === 'offer') { + type = 'answer'; + } else { + throw new Error("Type '" + desc.type + "' not supported."); + } + + var cached; + if (typeof this.cache[type] !== 'undefined') { + cached = transform.parse(this.cache[type]); + } + + var recvonlySsrcs = { + audio: {}, + video: {} + }; + + // A helper map that sends mids to m-line objects. We use it later to + // rebuild the Unified Plan style session.media array. + var mid2ul = {}; + var bIdx = 0; + var uIdx = 0; + + var sources2ul = {}; + + var candidates; + var iceUfrag; + var icePwd; + var fingerprint; + var payloads = {}; + var rtcpFb = {}; + var rtp = {}; + + session.media.forEach(function(bLine) { + if ((typeof bLine.rtcpMux !== 'string' || + bLine.rtcpMux !== 'rtcp-mux') && + bLine.direction !== 'inactive') { + throw new Error("Cannot convert to Unified Plan because m-lines " + + "without the rtcp-mux attribute were found."); + } + + if (bLine.type === 'application') { + mid2ul[bLine.mid] = bLine; + return; + } + + // With rtcp-mux and bundle all the channels should have the same ICE + // stuff. + var sources = bLine.sources; + var ssrcGroups = bLine.ssrcGroups; + var port = bLine.port; + + /* Chrome adds different candidates even using bundle, so we concat the candidates list */ + if (typeof bLine.candidates != 'undefined') { + if (typeof candidates != 'undefined') { + candidates = candidates.concat(bLine.candidates); + } else { + candidates = bLine.candidates; + } + } + + if ((typeof iceUfrag != 'undefined') && (typeof bLine.iceUfrag != 'undefined') && (iceUfrag != bLine.iceUfrag)) { + throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n" + + "\tLast iceUfrag: " + iceUfrag + "\n" + + "\tNew iceUfrag: " + bLine.iceUfrag + ); + } + + if (typeof bLine.iceUfrag != 'undefined') { + iceUfrag = bLine.iceUfrag; + } + + if ((typeof icePwd != 'undefined') && (typeof bLine.icePwd != 'undefined') && (icePwd != bLine.icePwd)) { + throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n" + + "\tLast icePwd: " + icePwd + "\n" + + "\tNew icePwd: " + bLine.icePwd + ); + } + + if (typeof bLine.icePwd != 'undefined') { + icePwd = bLine.icePwd; + } + + if ((typeof fingerprint != 'undefined') && (typeof bLine.fingerprint != 'undefined') && + (fingerprint.type != bLine.fingerprint.type || fingerprint.hash != bLine.fingerprint.hash)) { + throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n" + + "\tLast fingerprint: " + JSON.stringify(fingerprint) + "\n" + + "\tNew fingerprint: " + JSON.stringify(bLine.fingerprint) + ); + } + + if (typeof bLine.fingerprint != 'undefined') { + fingerprint = bLine.fingerprint; + } + + payloads[bLine.type] = bLine.payloads; + rtcpFb[bLine.type] = bLine.rtcpFb; + rtp[bLine.type] = bLine.rtp; + + // inverted ssrc group map + var ssrc2group = {}; + if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) { + ssrcGroups.forEach(function (ssrcGroup) { + // XXX This might brake if an SSRC is in more than one group + // for some reason. + if (typeof ssrcGroup.ssrcs !== 'undefined' && + Array.isArray(ssrcGroup.ssrcs)) { + ssrcGroup.ssrcs.forEach(function (ssrc) { + if (typeof ssrc2group[ssrc] === 'undefined') { + ssrc2group[ssrc] = []; + } + + ssrc2group[ssrc].push(ssrcGroup); + }); + } + }); + } + + // ssrc to m-line index. + var ssrc2ml = {}; + + if (typeof sources === 'object') { + + // We'll use the "bLine" object as a prototype for each new "mLine" + // that we create, but first we need to clean it up a bit. + delete bLine.sources; + delete bLine.ssrcGroups; + delete bLine.candidates; + delete bLine.iceUfrag; + delete bLine.icePwd; + delete bLine.fingerprint; + delete bLine.port; + delete bLine.mid; + + // Explode the Plan B channel sources with one m-line per source. + Object.keys(sources).forEach(function(ssrc) { + + // The (unified) m-line for this SSRC. We either create it from + // scratch or, if it's a grouped SSRC, we re-use a related + // mline. In other words, if the source is grouped with another + // source, put the two together in the same m-line. + var uLine; + + // We assume here that we are the answerer in the O/A, so any + // offers which we translate come from the remote side, while + // answers are local. So the check below is to make that we + // handle receive-only SSRCs in a special way only if they come + // from the remote side. + if (desc.type==='offer') { + // We want to detect SSRCs which are used by a remote peer + // in an m-line with direction=recvonly (i.e. they are + // being used for RTCP only). + // This information would have gotten lost if the remote + // peer used Unified Plan and their local description was + // translated to Plan B. So we use the lack of an MSID + // attribute to deduce a "receive only" SSRC. + if (!sources[ssrc].msid) { + recvonlySsrcs[bLine.type][ssrc] = sources[ssrc]; + // Receive-only SSRCs must not create new m-lines. We + // will assign them to an existing m-line later. + return; + } + } + + if (typeof ssrc2group[ssrc] !== 'undefined' && + Array.isArray(ssrc2group[ssrc])) { + ssrc2group[ssrc].some(function (ssrcGroup) { + // ssrcGroup.ssrcs *is* an Array, no need to check + // again here. + return ssrcGroup.ssrcs.some(function (related) { + if (typeof ssrc2ml[related] === 'object') { + uLine = ssrc2ml[related]; + return true; + } + }); + }); + } + + if (typeof uLine === 'object') { + // the m-line already exists. Just add the source. + uLine.sources[ssrc] = sources[ssrc]; + delete sources[ssrc].msid; + } else { + // Use the "bLine" as a prototype for the "uLine". + uLine = Object.create(bLine); + ssrc2ml[ssrc] = uLine; + + if (typeof sources[ssrc].msid !== 'undefined') { + // Assign the msid of the source to the m-line. Note + // that it is not guaranteed that the source will have + // msid. In particular "recvonly" sources don't have an + // msid. Note that "recvonly" is a term only defined + // for m-lines. + uLine.msid = sources[ssrc].msid; + delete sources[ssrc].msid; + } + + // We assign one SSRC per media line. + uLine.sources = {}; + uLine.sources[ssrc] = sources[ssrc]; + uLine.ssrcGroups = ssrc2group[ssrc]; + + // Use the cached Unified Plan SDP (if it exists) to assign + // SSRCs to mids. + if (typeof cached !== 'undefined' && + typeof cached.media !== 'undefined' && + Array.isArray(cached.media)) { + + cached.media.forEach(function (m) { + if (typeof m.sources === 'object') { + Object.keys(m.sources).forEach(function (s) { + if (s === ssrc) { + uLine.mid = m.mid; + } + }); + } + }); + } + + if (typeof uLine.mid === 'undefined') { + + // If this is an SSRC that we see for the first time + // assign it a new mid. This is typically the case when + // this method is called to transform a remote + // description for the first time or when there is a + // new SSRC in the remote description because a new + // peer has joined the conference. Local SSRCs should + // have already been added to the map in the toPlanB + // method. + // + // Because FF generates answers in Unified Plan style, + // we MUST already have a cached answer with all the + // local SSRCs mapped to some m-line/mid. + + uLine.mid = [bLine.type, '-', ssrc].join(''); + } + + // Include the candidates in the 1st media line. + uLine.candidates = candidates; + uLine.iceUfrag = iceUfrag; + uLine.icePwd = icePwd; + uLine.fingerprint = fingerprint; + uLine.port = port; + + mid2ul[uLine.mid] = uLine; + sources2ul[uIdx] = uLine.sources; + + self.cache.mlU2BMap[uIdx] = bIdx; + if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') { + self.cache.mlB2UMap[bIdx] = uIdx; + } + uIdx++; + } + }); + } else { + var uLine = bLine; + + uLine.candidates = candidates; + uLine.iceUfrag = iceUfrag; + uLine.icePwd = icePwd; + uLine.fingerprint = fingerprint; + uLine.port = port; + + mid2ul[uLine.mid] = uLine; + + self.cache.mlU2BMap[uIdx] = bIdx; + if (typeof self.cache.mlB2UMap[bIdx] === 'undefined') { + self.cache.mlB2UMap[bIdx] = uIdx; + } + } + + bIdx++; + }); + + // Rebuild the media array in the right order and add the missing mLines + // (missing from the Plan B SDP). + session.media = []; + mids = []; // reuse + + if (desc.type === 'answer') { + + // The media lines in the answer must match the media lines in the + // offer. The order is important too. Here we assume that Firefox is + // the answerer, so we merely have to use the reconstructed (unified) + // answer to update the cached (unified) answer accordingly. + // + // In the general case, one would have to use the cached (unified) + // offer to find the m-lines that are missing from the reconstructed + // answer, potentially grabbing them from the cached (unified) answer. + // One has to be careful with this approach because inactive m-lines do + // not always have an mid, making it tricky (impossible?) to find where + // exactly and which m-lines are missing from the reconstructed answer. + + for (var i = 0; i < cached.media.length; i++) { + var uLine = cached.media[i]; + + delete uLine.msid; + delete uLine.sources; + delete uLine.ssrcGroups; + + if (typeof sources2ul[i] === 'undefined') { + if (!uLine.direction + || uLine.direction === 'sendrecv') + uLine.direction = 'recvonly'; + else if (uLine.direction === 'sendonly') + uLine.direction = 'inactive'; + } else { + if (!uLine.direction + || uLine.direction === 'sendrecv') + uLine.direction = 'sendrecv'; + else if (uLine.direction === 'recvonly') + uLine.direction = 'sendonly'; + } + + uLine.sources = sources2ul[i]; + uLine.candidates = candidates; + uLine.iceUfrag = iceUfrag; + uLine.icePwd = icePwd; + uLine.fingerprint = fingerprint; + + uLine.rtp = rtp[uLine.type]; + uLine.payloads = payloads[uLine.type]; + uLine.rtcpFb = rtcpFb[uLine.type]; + + session.media.push(uLine); + + if (typeof uLine.mid === 'string') { + // inactive lines don't/may not have an mid. + mids.push(uLine.mid); + } + } + } else { + + // SDP offer/answer (and the JSEP spec) forbids removing an m-section + // under any circumstances. If we are no longer interested in sending a + // track, we just remove the msid and ssrc attributes and set it to + // either a=recvonly (as the reofferer, we must use recvonly if the + // other side was previously sending on the m-section, but we can also + // leave the possibility open if it wasn't previously in use), or + // a=inactive. + + if (typeof cached !== 'undefined' && + typeof cached.media !== 'undefined' && + Array.isArray(cached.media)) { + cached.media.forEach(function(uLine) { + mids.push(uLine.mid); + if (typeof mid2ul[uLine.mid] !== 'undefined') { + session.media.push(mid2ul[uLine.mid]); + } else { + delete uLine.msid; + delete uLine.sources; + delete uLine.ssrcGroups; + + if (!uLine.direction + || uLine.direction === 'sendrecv') { + uLine.direction = 'sendonly'; + } + if (!uLine.direction + || uLine.direction === 'recvonly') { + uLine.direction = 'inactive'; + } + + addSetupAttr (uLine); + session.media.push(uLine); + } + }); + } + + // Add all the remaining (new) m-lines of the transformed SDP. + Object.keys(mid2ul).forEach(function(mid) { + if (mids.indexOf(mid) === -1) { + mids.push(mid); + if (mid2ul[mid].direction === 'recvonly') { + // This is a remote recvonly channel. Add its SSRC to the + // appropriate sendrecv or sendonly channel. + // TODO(gp) what if we don't have sendrecv/sendonly + // channel? + + var done = false; + + session.media.some(function (uLine) { + if ((uLine.direction === 'sendrecv' || + uLine.direction === 'sendonly') && + uLine.type === mid2ul[mid].type) { + // mid2ul[mid] shouldn't have any ssrc-groups + Object.keys(mid2ul[mid].sources).forEach( + function (ssrc) { + uLine.sources[ssrc] = + mid2ul[mid].sources[ssrc]; + }); + + done = true; + return true; + } + }); + + if (!done) { + session.media.push(mid2ul[mid]); + } + } else { + session.media.push(mid2ul[mid]); + } + } + }); + } + + // After we have constructed the Plan Unified m-lines we can figure out + // where (in which m-line) to place the 'recvonly SSRCs'. + // Note: we assume here that we are the answerer in the O/A, so any offers + // which we translate come from the remote side, while answers are local + // (and so our last local description is cached as an 'answer'). + ["audio", "video"].forEach(function (type) { + if (!session || !session.media || !Array.isArray(session.media)) + return; + + var idx = null; + if (Object.keys(recvonlySsrcs[type]).length > 0) { + idx = self.getFirstSendingIndexFromAnswer(type); + if (idx === null){ + // If this is the first offer we receive, we don't have a + // cached answer. Assume that we will be sending media using + // the first m-line for each media type. + + for (var i = 0; i < session.media.length; i++) { + if (session.media[i].type === type) { + idx = i; + break; + } + } + } + } + + if (idx && session.media.length > idx) { + var mLine = session.media[idx]; + Object.keys(recvonlySsrcs[type]).forEach(function(ssrc) { + if (mLine.sources && mLine.sources[ssrc]) { + console.warn("Replacing an existing SSRC."); + } + if (!mLine.sources) { + mLine.sources = {}; + } + + mLine.sources[ssrc] = recvonlySsrcs[type][ssrc]; + }); + } + }); + + if (typeof session.groups !== 'undefined') { + // We regenerate the BUNDLE group (since we regenerated the mids) + session.groups.some(function(group) { + if (group.type === 'BUNDLE') { + group.mids = mids.join(' '); + return true; + } + }); + } + + // msid semantic + session.msidSemantic = { + semantic: 'WMS', + token: '*' + }; + + var resStr = transform.write(session); + + // Cache the transformed SDP (Unified Plan) for later re-use in this + // function. + this.cache[desc.type] = resStr; + + return new RTCSessionDescription({ + type: desc.type, + sdp: resStr + }); + + //#endregion +}; + +},{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){ +/* Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var transform = require('sdp-transform'); + +exports.write = function(session, opts) { + + if (typeof session !== 'undefined' && + typeof session.media !== 'undefined' && + Array.isArray(session.media)) { + + session.media.forEach(function (mLine) { + // expand sources to ssrcs + if (typeof mLine.sources !== 'undefined' && + Object.keys(mLine.sources).length !== 0) { + mLine.ssrcs = []; + Object.keys(mLine.sources).forEach(function (ssrc) { + var source = mLine.sources[ssrc]; + Object.keys(source).forEach(function (attribute) { + mLine.ssrcs.push({ + id: ssrc, + attribute: attribute, + value: source[attribute] + }); + }); + }); + delete mLine.sources; + } + + // join ssrcs in ssrc groups + if (typeof mLine.ssrcGroups !== 'undefined' && + Array.isArray(mLine.ssrcGroups)) { + mLine.ssrcGroups.forEach(function (ssrcGroup) { + if (typeof ssrcGroup.ssrcs !== 'undefined' && + Array.isArray(ssrcGroup.ssrcs)) { + ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' '); + } + }); + } + }); + } + + // join group mids + if (typeof session !== 'undefined' && + typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { + + session.groups.forEach(function (g) { + if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) { + g.mids = g.mids.join(' '); + } + }); + } + + return transform.write(session, opts); +}; + +exports.parse = function(sdp) { + var session = transform.parse(sdp); + + if (typeof session !== 'undefined' && typeof session.media !== 'undefined' && + Array.isArray(session.media)) { + + session.media.forEach(function (mLine) { + // group sources attributes by ssrc + if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { + mLine.sources = {}; + mLine.ssrcs.forEach(function (ssrc) { + if (!mLine.sources[ssrc.id]) + mLine.sources[ssrc.id] = {}; + mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value; + }); + + delete mLine.ssrcs; + } + + // split ssrcs in ssrc groups + if (typeof mLine.ssrcGroups !== 'undefined' && + Array.isArray(mLine.ssrcGroups)) { + mLine.ssrcGroups.forEach(function (ssrcGroup) { + if (typeof ssrcGroup.ssrcs === 'string') { + ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' '); + } + }); + } + }); + } + // split group mids + if (typeof session !== 'undefined' && + typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { + + session.groups.forEach(function (g) { + if (typeof g.mids === 'string') { + g.mids = g.mids.split(' '); + } + }); + } + + return session; +}; + + +},{"sdp-transform":14}],21:[function(require,module,exports){ +/** + * UAParser.js v0.7.17 + * Lightweight JavaScript-based User-Agent string parser + * https://github.com/faisalman/ua-parser-js + * + * Copyright © 2012-2016 Faisal Salman <fyzlman@gmail.com> + * Dual licensed under GPLv2 & MIT + */ + +(function (window, undefined) { + + 'use strict'; + + ////////////// + // Constants + ///////////// + + + var LIBVERSION = '0.7.17', + EMPTY = '', + UNKNOWN = '?', + FUNC_TYPE = 'function', + UNDEF_TYPE = 'undefined', + OBJ_TYPE = 'object', + STR_TYPE = 'string', + MAJOR = 'major', // deprecated + MODEL = 'model', + NAME = 'name', + TYPE = 'type', + VENDOR = 'vendor', + VERSION = 'version', + ARCHITECTURE= 'architecture', + CONSOLE = 'console', + MOBILE = 'mobile', + TABLET = 'tablet', + SMARTTV = 'smarttv', + WEARABLE = 'wearable', + EMBEDDED = 'embedded'; + + + /////////// + // Helper + ////////// + + + var util = { + extend : function (regexes, extensions) { + var margedRegexes = {}; + for (var i in regexes) { + if (extensions[i] && extensions[i].length % 2 === 0) { + margedRegexes[i] = extensions[i].concat(regexes[i]); + } else { + margedRegexes[i] = regexes[i]; + } + } + return margedRegexes; + }, + has : function (str1, str2) { + if (typeof str1 === "string") { + return str2.toLowerCase().indexOf(str1.toLowerCase()) !== -1; + } else { + return false; + } + }, + lowerize : function (str) { + return str.toLowerCase(); + }, + major : function (version) { + return typeof(version) === STR_TYPE ? version.replace(/[^\d\.]/g,'').split(".")[0] : undefined; + }, + trim : function (str) { + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } + }; + + + /////////////// + // Map helper + ////////////// + + + var mapper = { + + rgx : function (ua, arrays) { + + //var result = {}, + var i = 0, j, k, p, q, matches, match;//, args = arguments; + + /*// construct object barebones + for (p = 0; p < args[1].length; p++) { + q = args[1][p]; + result[typeof q === OBJ_TYPE ? q[0] : q] = undefined; + }*/ + + // loop through all regexes maps + while (i < arrays.length && !matches) { + + var regex = arrays[i], // even sequence (0,2,4,..) + props = arrays[i + 1]; // odd sequence (1,3,5,..) + j = k = 0; + + // try matching uastring with regexes + while (j < regex.length && !matches) { + + matches = regex[j++].exec(ua); + + if (!!matches) { + for (p = 0; p < props.length; p++) { + match = matches[++k]; + q = props[p]; + // check if given property is actually array + if (typeof q === OBJ_TYPE && q.length > 0) { + if (q.length == 2) { + if (typeof q[1] == FUNC_TYPE) { + // assign modified match + this[q[0]] = q[1].call(this, match); + } else { + // assign given value, ignore regex match + this[q[0]] = q[1]; + } + } else if (q.length == 3) { + // check whether function or regex + if (typeof q[1] === FUNC_TYPE && !(q[1].exec && q[1].test)) { + // call function (usually string mapper) + this[q[0]] = match ? q[1].call(this, match, q[2]) : undefined; + } else { + // sanitize match using given regex + this[q[0]] = match ? match.replace(q[1], q[2]) : undefined; + } + } else if (q.length == 4) { + this[q[0]] = match ? q[3].call(this, match.replace(q[1], q[2])) : undefined; + } + } else { + this[q] = match ? match : undefined; + } + } + } + } + i += 2; + } + // console.log(this); + //return this; + }, + + str : function (str, map) { + + for (var i in map) { + // check if array + if (typeof map[i] === OBJ_TYPE && map[i].length > 0) { + for (var j = 0; j < map[i].length; j++) { + if (util.has(map[i][j], str)) { + return (i === UNKNOWN) ? undefined : i; + } + } + } else if (util.has(map[i], str)) { + return (i === UNKNOWN) ? undefined : i; + } + } + return str; + } + }; + + + /////////////// + // String map + ////////////// + + + var maps = { + + browser : { + oldsafari : { + version : { + '1.0' : '/8', + '1.2' : '/1', + '1.3' : '/3', + '2.0' : '/412', + '2.0.2' : '/416', + '2.0.3' : '/417', + '2.0.4' : '/419', + '?' : '/' + } + } + }, + + device : { + amazon : { + model : { + 'Fire Phone' : ['SD', 'KF'] + } + }, + sprint : { + model : { + 'Evo Shift 4G' : '7373KT' + }, + vendor : { + 'HTC' : 'APA', + 'Sprint' : 'Sprint' + } + } + }, + + os : { + windows : { + version : { + 'ME' : '4.90', + 'NT 3.11' : 'NT3.51', + 'NT 4.0' : 'NT4.0', + '2000' : 'NT 5.0', + 'XP' : ['NT 5.1', 'NT 5.2'], + 'Vista' : 'NT 6.0', + '7' : 'NT 6.1', + '8' : 'NT 6.2', + '8.1' : 'NT 6.3', + '10' : ['NT 6.4', 'NT 10.0'], + 'RT' : 'ARM' + } + } + } + }; + + + ////////////// + // Regex map + ///////////// + + + var regexes = { + + browser : [[ + + // Presto based + /(opera\smini)\/([\w\.-]+)/i, // Opera Mini + /(opera\s[mobiletab]+).+version\/([\w\.-]+)/i, // Opera Mobi/Tablet + /(opera).+version\/([\w\.]+)/i, // Opera > 9.80 + /(opera)[\/\s]+([\w\.]+)/i // Opera < 9.80 + ], [NAME, VERSION], [ + + /(opios)[\/\s]+([\w\.]+)/i // Opera mini on iphone >= 8.0 + ], [[NAME, 'Opera Mini'], VERSION], [ + + /\s(opr)\/([\w\.]+)/i // Opera Webkit + ], [[NAME, 'Opera'], VERSION], [ + + // Mixed + /(kindle)\/([\w\.]+)/i, // Kindle + /(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i, + // Lunascape/Maxthon/Netfront/Jasmine/Blazer + + // Trident based + /(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i, + // Avant/IEMobile/SlimBrowser/Baidu + /(?:ms|\()(ie)\s([\w\.]+)/i, // Internet Explorer + + // Webkit/KHTML based + /(rekonq)\/([\w\.]+)*/i, // Rekonq + /(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs|bowser)\/([\w\.-]+)/i + // Chromium/Flock/RockMelt/Midori/Epiphany/Silk/Skyfire/Bolt/Iron/Iridium/PhantomJS/Bowser + ], [NAME, VERSION], [ + + /(trident).+rv[:\s]([\w\.]+).+like\sgecko/i // IE11 + ], [[NAME, 'IE'], VERSION], [ + + /(edge)\/((\d+)?[\w\.]+)/i // Microsoft Edge + ], [NAME, VERSION], [ + + /(yabrowser)\/([\w\.]+)/i // Yandex + ], [[NAME, 'Yandex'], VERSION], [ + + /(puffin)\/([\w\.]+)/i // Puffin + ], [[NAME, 'Puffin'], VERSION], [ + + /((?:[\s\/])uc?\s?browser|(?:juc.+)ucweb)[\/\s]?([\w\.]+)/i + // UCBrowser + ], [[NAME, 'UCBrowser'], VERSION], [ + + /(comodo_dragon)\/([\w\.]+)/i // Comodo Dragon + ], [[NAME, /_/g, ' '], VERSION], [ + + /(micromessenger)\/([\w\.]+)/i // WeChat + ], [[NAME, 'WeChat'], VERSION], [ + + /(QQ)\/([\d\.]+)/i // QQ, aka ShouQ + ], [NAME, VERSION], [ + + /m?(qqbrowser)[\/\s]?([\w\.]+)/i // QQBrowser + ], [NAME, VERSION], [ + + /xiaomi\/miuibrowser\/([\w\.]+)/i // MIUI Browser + ], [VERSION, [NAME, 'MIUI Browser']], [ + + /;fbav\/([\w\.]+);/i // Facebook App for iOS & Android + ], [VERSION, [NAME, 'Facebook']], [ + + /headlesschrome(?:\/([\w\.]+)|\s)/i // Chrome Headless + ], [VERSION, [NAME, 'Chrome Headless']], [ + + /\swv\).+(chrome)\/([\w\.]+)/i // Chrome WebView + ], [[NAME, /(.+)/, '$1 WebView'], VERSION], [ + + /((?:oculus|samsung)browser)\/([\w\.]+)/i + ], [[NAME, /(.+(?:g|us))(.+)/, '$1 $2'], VERSION], [ // Oculus / Samsung Browser + + /android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i // Android Browser + ], [VERSION, [NAME, 'Android Browser']], [ + + /(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i + // Chrome/OmniWeb/Arora/Tizen/Nokia + ], [NAME, VERSION], [ + + /(dolfin)\/([\w\.]+)/i // Dolphin + ], [[NAME, 'Dolphin'], VERSION], [ + + /((?:android.+)crmo|crios)\/([\w\.]+)/i // Chrome for Android/iOS + ], [[NAME, 'Chrome'], VERSION], [ + + /(coast)\/([\w\.]+)/i // Opera Coast + ], [[NAME, 'Opera Coast'], VERSION], [ + + /fxios\/([\w\.-]+)/i // Firefox for iOS + ], [VERSION, [NAME, 'Firefox']], [ + + /version\/([\w\.]+).+?mobile\/\w+\s(safari)/i // Mobile Safari + ], [VERSION, [NAME, 'Mobile Safari']], [ + + /version\/([\w\.]+).+?(mobile\s?safari|safari)/i // Safari & Safari Mobile + ], [VERSION, NAME], [ + + /webkit.+?(gsa)\/([\w\.]+).+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Google Search Appliance on iOS + ], [[NAME, 'GSA'], VERSION], [ + + /webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i // Safari < 3.0 + ], [NAME, [VERSION, mapper.str, maps.browser.oldsafari.version]], [ + + /(konqueror)\/([\w\.]+)/i, // Konqueror + /(webkit|khtml)\/([\w\.]+)/i + ], [NAME, VERSION], [ + + // Gecko based + /(navigator|netscape)\/([\w\.-]+)/i // Netscape + ], [[NAME, 'Netscape'], VERSION], [ + /(swiftfox)/i, // Swiftfox + /(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i, + // IceDragon/Iceweasel/Camino/Chimera/Fennec/Maemo/Minimo/Conkeror + /(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i, + // Firefox/SeaMonkey/K-Meleon/IceCat/IceApe/Firebird/Phoenix + /(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i, // Mozilla + + // Other + /(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i, + // Polaris/Lynx/Dillo/iCab/Doris/Amaya/w3m/NetSurf/Sleipnir + /(links)\s\(([\w\.]+)/i, // Links + /(gobrowser)\/?([\w\.]+)*/i, // GoBrowser + /(ice\s?browser)\/v?([\w\._]+)/i, // ICE Browser + /(mosaic)[\/\s]([\w\.]+)/i // Mosaic + ], [NAME, VERSION] + + /* ///////////////////// + // Media players BEGIN + //////////////////////// + + , [ + + /(apple(?:coremedia|))\/((\d+)[\w\._]+)/i, // Generic Apple CoreMedia + /(coremedia) v((\d+)[\w\._]+)/i + ], [NAME, VERSION], [ + + /(aqualung|lyssna|bsplayer)\/((\d+)?[\w\.-]+)/i // Aqualung/Lyssna/BSPlayer + ], [NAME, VERSION], [ + + /(ares|ossproxy)\s((\d+)[\w\.-]+)/i // Ares/OSSProxy + ], [NAME, VERSION], [ + + /(audacious|audimusicstream|amarok|bass|core|dalvik|gnomemplayer|music on console|nsplayer|psp-internetradioplayer|videos)\/((\d+)[\w\.-]+)/i, + // Audacious/AudiMusicStream/Amarok/BASS/OpenCORE/Dalvik/GnomeMplayer/MoC + // NSPlayer/PSP-InternetRadioPlayer/Videos + /(clementine|music player daemon)\s((\d+)[\w\.-]+)/i, // Clementine/MPD + /(lg player|nexplayer)\s((\d+)[\d\.]+)/i, + /player\/(nexplayer|lg player)\s((\d+)[\w\.-]+)/i // NexPlayer/LG Player + ], [NAME, VERSION], [ + /(nexplayer)\s((\d+)[\w\.-]+)/i // Nexplayer + ], [NAME, VERSION], [ + + /(flrp)\/((\d+)[\w\.-]+)/i // Flip Player + ], [[NAME, 'Flip Player'], VERSION], [ + + /(fstream|nativehost|queryseekspider|ia-archiver|facebookexternalhit)/i + // FStream/NativeHost/QuerySeekSpider/IA Archiver/facebookexternalhit + ], [NAME], [ + + /(gstreamer) souphttpsrc (?:\([^\)]+\)){0,1} libsoup\/((\d+)[\w\.-]+)/i + // Gstreamer + ], [NAME, VERSION], [ + + /(htc streaming player)\s[\w_]+\s\/\s((\d+)[\d\.]+)/i, // HTC Streaming Player + /(java|python-urllib|python-requests|wget|libcurl)\/((\d+)[\w\.-_]+)/i, + // Java/urllib/requests/wget/cURL + /(lavf)((\d+)[\d\.]+)/i // Lavf (FFMPEG) + ], [NAME, VERSION], [ + + /(htc_one_s)\/((\d+)[\d\.]+)/i // HTC One S + ], [[NAME, /_/g, ' '], VERSION], [ + + /(mplayer)(?:\s|\/)(?:(?:sherpya-){0,1}svn)(?:-|\s)(r\d+(?:-\d+[\w\.-]+){0,1})/i + // MPlayer SVN + ], [NAME, VERSION], [ + + /(mplayer)(?:\s|\/|[unkow-]+)((\d+)[\w\.-]+)/i // MPlayer + ], [NAME, VERSION], [ + + /(mplayer)/i, // MPlayer (no other info) + /(yourmuze)/i, // YourMuze + /(media player classic|nero showtime)/i // Media Player Classic/Nero ShowTime + ], [NAME], [ + + /(nero (?:home|scout))\/((\d+)[\w\.-]+)/i // Nero Home/Nero Scout + ], [NAME, VERSION], [ + + /(nokia\d+)\/((\d+)[\w\.-]+)/i // Nokia + ], [NAME, VERSION], [ + + /\s(songbird)\/((\d+)[\w\.-]+)/i // Songbird/Philips-Songbird + ], [NAME, VERSION], [ + + /(winamp)3 version ((\d+)[\w\.-]+)/i, // Winamp + /(winamp)\s((\d+)[\w\.-]+)/i, + /(winamp)mpeg\/((\d+)[\w\.-]+)/i + ], [NAME, VERSION], [ + + /(ocms-bot|tapinradio|tunein radio|unknown|winamp|inlight radio)/i // OCMS-bot/tap in radio/tunein/unknown/winamp (no other info) + // inlight radio + ], [NAME], [ + + /(quicktime|rma|radioapp|radioclientapplication|soundtap|totem|stagefright|streamium)\/((\d+)[\w\.-]+)/i + // QuickTime/RealMedia/RadioApp/RadioClientApplication/ + // SoundTap/Totem/Stagefright/Streamium + ], [NAME, VERSION], [ + + /(smp)((\d+)[\d\.]+)/i // SMP + ], [NAME, VERSION], [ + + /(vlc) media player - version ((\d+)[\w\.]+)/i, // VLC Videolan + /(vlc)\/((\d+)[\w\.-]+)/i, + /(xbmc|gvfs|xine|xmms|irapp)\/((\d+)[\w\.-]+)/i, // XBMC/gvfs/Xine/XMMS/irapp + /(foobar2000)\/((\d+)[\d\.]+)/i, // Foobar2000 + /(itunes)\/((\d+)[\d\.]+)/i // iTunes + ], [NAME, VERSION], [ + + /(wmplayer)\/((\d+)[\w\.-]+)/i, // Windows Media Player + /(windows-media-player)\/((\d+)[\w\.-]+)/i + ], [[NAME, /-/g, ' '], VERSION], [ + + /windows\/((\d+)[\w\.-]+) upnp\/[\d\.]+ dlnadoc\/[\d\.]+ (home media server)/i + // Windows Media Server + ], [VERSION, [NAME, 'Windows']], [ + + /(com\.riseupradioalarm)\/((\d+)[\d\.]*)/i // RiseUP Radio Alarm + ], [NAME, VERSION], [ + + /(rad.io)\s((\d+)[\d\.]+)/i, // Rad.io + /(radio.(?:de|at|fr))\s((\d+)[\d\.]+)/i + ], [[NAME, 'rad.io'], VERSION] + + ////////////////////// + // Media players END + ////////////////////*/ + + ], + + cpu : [[ + + /(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i // AMD64 + ], [[ARCHITECTURE, 'amd64']], [ + + /(ia32(?=;))/i // IA32 (quicktime) + ], [[ARCHITECTURE, util.lowerize]], [ + + /((?:i[346]|x)86)[;\)]/i // IA32 + ], [[ARCHITECTURE, 'ia32']], [ + + // PocketPC mistakenly identified as PowerPC + /windows\s(ce|mobile);\sppc;/i + ], [[ARCHITECTURE, 'arm']], [ + + /((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i // PowerPC + ], [[ARCHITECTURE, /ower/, '', util.lowerize]], [ + + /(sun4\w)[;\)]/i // SPARC + ], [[ARCHITECTURE, 'sparc']], [ + + /((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i + // IA64, 68K, ARM/64, AVR/32, IRIX/64, MIPS/64, SPARC/64, PA-RISC + ], [[ARCHITECTURE, util.lowerize]] + ], + + device : [[ + + /\((ipad|playbook);[\w\s\);-]+(rim|apple)/i // iPad/PlayBook + ], [MODEL, VENDOR, [TYPE, TABLET]], [ + + /applecoremedia\/[\w\.]+ \((ipad)/ // iPad + ], [MODEL, [VENDOR, 'Apple'], [TYPE, TABLET]], [ + + /(apple\s{0,1}tv)/i // Apple TV + ], [[MODEL, 'Apple TV'], [VENDOR, 'Apple']], [ + + /(archos)\s(gamepad2?)/i, // Archos + /(hp).+(touchpad)/i, // HP TouchPad + /(hp).+(tablet)/i, // HP Tablet + /(kindle)\/([\w\.]+)/i, // Kindle + /\s(nook)[\w\s]+build\/(\w+)/i, // Nook + /(dell)\s(strea[kpr\s\d]*[\dko])/i // Dell Streak + ], [VENDOR, MODEL, [TYPE, TABLET]], [ + + /(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i // Kindle Fire HD + ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [ + /(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i // Fire Phone + ], [[MODEL, mapper.str, maps.device.amazon.model], [VENDOR, 'Amazon'], [TYPE, MOBILE]], [ + + /\((ip[honed|\s\w*]+);.+(apple)/i // iPod/iPhone + ], [MODEL, VENDOR, [TYPE, MOBILE]], [ + /\((ip[honed|\s\w*]+);/i // iPod/iPhone + ], [MODEL, [VENDOR, 'Apple'], [TYPE, MOBILE]], [ + + /(blackberry)[\s-]?(\w+)/i, // BlackBerry + /(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i, + // BenQ/Palm/Sony-Ericsson/Acer/Asus/Dell/Meizu/Motorola/Polytron + /(hp)\s([\w\s]+\w)/i, // HP iPAQ + /(asus)-?(\w+)/i // Asus + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + /\(bb10;\s(\w+)/i // BlackBerry 10 + ], [MODEL, [VENDOR, 'BlackBerry'], [TYPE, MOBILE]], [ + // Asus Tablets + /android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone)/i + ], [MODEL, [VENDOR, 'Asus'], [TYPE, TABLET]], [ + + /(sony)\s(tablet\s[ps])\sbuild\//i, // Sony + /(sony)?(?:sgp.+)\sbuild\//i + ], [[VENDOR, 'Sony'], [MODEL, 'Xperia Tablet'], [TYPE, TABLET]], [ + /android.+\s([c-g]\d{4}|so[-l]\w+)\sbuild\//i + ], [MODEL, [VENDOR, 'Sony'], [TYPE, MOBILE]], [ + + /\s(ouya)\s/i, // Ouya + /(nintendo)\s([wids3u]+)/i // Nintendo + ], [VENDOR, MODEL, [TYPE, CONSOLE]], [ + + /android.+;\s(shield)\sbuild/i // Nvidia + ], [MODEL, [VENDOR, 'Nvidia'], [TYPE, CONSOLE]], [ + + /(playstation\s[34portablevi]+)/i // Playstation + ], [MODEL, [VENDOR, 'Sony'], [TYPE, CONSOLE]], [ + + /(sprint\s(\w+))/i // Sprint Phones + ], [[VENDOR, mapper.str, maps.device.sprint.vendor], [MODEL, mapper.str, maps.device.sprint.model], [TYPE, MOBILE]], [ + + /(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i // Lenovo tablets + ], [VENDOR, MODEL, [TYPE, TABLET]], [ + + /(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i, // HTC + /(zte)-(\w+)*/i, // ZTE + /(alcatel|geeksphone|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i + // Alcatel/GeeksPhone/Lenovo/Nexian/Panasonic/Sony + ], [VENDOR, [MODEL, /_/g, ' '], [TYPE, MOBILE]], [ + + /(nexus\s9)/i // HTC Nexus 9 + ], [MODEL, [VENDOR, 'HTC'], [TYPE, TABLET]], [ + + /d\/huawei([\w\s-]+)[;\)]/i, + /(nexus\s6p)/i // Huawei + ], [MODEL, [VENDOR, 'Huawei'], [TYPE, MOBILE]], [ + + /(microsoft);\s(lumia[\s\w]+)/i // Microsoft Lumia + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + + /[\s\(;](xbox(?:\sone)?)[\s\);]/i // Microsoft Xbox + ], [MODEL, [VENDOR, 'Microsoft'], [TYPE, CONSOLE]], [ + /(kin\.[onetw]{3})/i // Microsoft Kin + ], [[MODEL, /\./g, ' '], [VENDOR, 'Microsoft'], [TYPE, MOBILE]], [ + + // Motorola + /\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i, + /mot[\s-]?(\w+)*/i, + /(XT\d{3,4}) build\//i, + /(nexus\s6)/i + ], [MODEL, [VENDOR, 'Motorola'], [TYPE, MOBILE]], [ + /android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i + ], [MODEL, [VENDOR, 'Motorola'], [TYPE, TABLET]], [ + + /hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i // HbbTV devices + ], [[VENDOR, util.trim], [MODEL, util.trim], [TYPE, SMARTTV]], [ + + /hbbtv.+maple;(\d+)/i + ], [[MODEL, /^/, 'SmartTV'], [VENDOR, 'Samsung'], [TYPE, SMARTTV]], [ + + /\(dtv[\);].+(aquos)/i // Sharp + ], [MODEL, [VENDOR, 'Sharp'], [TYPE, SMARTTV]], [ + + /android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i, + /((SM-T\w+))/i + ], [[VENDOR, 'Samsung'], MODEL, [TYPE, TABLET]], [ // Samsung + /smart-tv.+(samsung)/i + ], [VENDOR, [TYPE, SMARTTV], MODEL], [ + /((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i, + /(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i, + /sec-((sgh\w+))/i + ], [[VENDOR, 'Samsung'], MODEL, [TYPE, MOBILE]], [ + + /sie-(\w+)*/i // Siemens + ], [MODEL, [VENDOR, 'Siemens'], [TYPE, MOBILE]], [ + + /(maemo|nokia).*(n900|lumia\s\d+)/i, // Nokia + /(nokia)[\s_-]?([\w-]+)*/i + ], [[VENDOR, 'Nokia'], MODEL, [TYPE, MOBILE]], [ + + /android\s3\.[\s\w;-]{10}(a\d{3})/i // Acer + ], [MODEL, [VENDOR, 'Acer'], [TYPE, TABLET]], [ + + /android.+([vl]k\-?\d{3})\s+build/i // LG Tablet + ], [MODEL, [VENDOR, 'LG'], [TYPE, TABLET]], [ + /android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i // LG Tablet + ], [[VENDOR, 'LG'], MODEL, [TYPE, TABLET]], [ + /(lg) netcast\.tv/i // LG SmartTV + ], [VENDOR, MODEL, [TYPE, SMARTTV]], [ + /(nexus\s[45])/i, // LG + /lg[e;\s\/-]+(\w+)*/i, + /android.+lg(\-?[\d\w]+)\s+build/i + ], [MODEL, [VENDOR, 'LG'], [TYPE, MOBILE]], [ + + /android.+(ideatab[a-z0-9\-\s]+)/i // Lenovo + ], [MODEL, [VENDOR, 'Lenovo'], [TYPE, TABLET]], [ + + /linux;.+((jolla));/i // Jolla + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + + /((pebble))app\/[\d\.]+\s/i // Pebble + ], [VENDOR, MODEL, [TYPE, WEARABLE]], [ + + /android.+;\s(oppo)\s?([\w\s]+)\sbuild/i // OPPO + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + + /crkey/i // Google Chromecast + ], [[MODEL, 'Chromecast'], [VENDOR, 'Google']], [ + + /android.+;\s(glass)\s\d/i // Google Glass + ], [MODEL, [VENDOR, 'Google'], [TYPE, WEARABLE]], [ + + /android.+;\s(pixel c)\s/i // Google Pixel C + ], [MODEL, [VENDOR, 'Google'], [TYPE, TABLET]], [ + + /android.+;\s(pixel xl|pixel)\s/i // Google Pixel + ], [MODEL, [VENDOR, 'Google'], [TYPE, MOBILE]], [ + + /android.+(\w+)\s+build\/hm\1/i, // Xiaomi Hongmi 'numeric' models + /android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i, // Xiaomi Hongmi + /android.+(mi[\s\-_]*(?:one|one[\s_]plus|note lte)?[\s_]*(?:\d\w)?)\s+build/i, // Xiaomi Mi + /android.+(redmi[\s\-_]*(?:note)?(?:[\s_]*[\w\s]+)?)\s+build/i // Redmi Phones + ], [[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, MOBILE]], [ + /android.+(mi[\s\-_]*(?:pad)?(?:[\s_]*[\w\s]+)?)\s+build/i // Mi Pad tablets + ],[[MODEL, /_/g, ' '], [VENDOR, 'Xiaomi'], [TYPE, TABLET]], [ + /android.+;\s(m[1-5]\snote)\sbuild/i // Meizu Tablet + ], [MODEL, [VENDOR, 'Meizu'], [TYPE, TABLET]], [ + + /android.+a000(1)\s+build/i // OnePlus + ], [MODEL, [VENDOR, 'OnePlus'], [TYPE, MOBILE]], [ + + /android.+[;\/]\s*(RCT[\d\w]+)\s+build/i // RCA Tablets + ], [MODEL, [VENDOR, 'RCA'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*(Venue[\d\s]*)\s+build/i // Dell Venue Tablets + ], [MODEL, [VENDOR, 'Dell'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*(Q[T|M][\d\w]+)\s+build/i // Verizon Tablet + ], [MODEL, [VENDOR, 'Verizon'], [TYPE, TABLET]], [ + + /android.+[;\/]\s+(Barnes[&\s]+Noble\s+|BN[RT])(V?.*)\s+build/i // Barnes & Noble Tablet + ], [[VENDOR, 'Barnes & Noble'], MODEL, [TYPE, TABLET]], [ + + /android.+[;\/]\s+(TM\d{3}.*\b)\s+build/i // Barnes & Noble Tablet + ], [MODEL, [VENDOR, 'NuVision'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*(zte)?.+(k\d{2})\s+build/i // ZTE K Series Tablet + ], [[VENDOR, 'ZTE'], MODEL, [TYPE, TABLET]], [ + + /android.+[;\/]\s*(gen\d{3})\s+build.*49h/i // Swiss GEN Mobile + ], [MODEL, [VENDOR, 'Swiss'], [TYPE, MOBILE]], [ + + /android.+[;\/]\s*(zur\d{3})\s+build/i // Swiss ZUR Tablet + ], [MODEL, [VENDOR, 'Swiss'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*((Zeki)?TB.*\b)\s+build/i // Zeki Tablets + ], [MODEL, [VENDOR, 'Zeki'], [TYPE, TABLET]], [ + + /(android).+[;\/]\s+([YR]\d{2}x?.*)\s+build/i, + /android.+[;\/]\s+(Dragon[\-\s]+Touch\s+|DT)(.+)\s+build/i // Dragon Touch Tablet + ], [[VENDOR, 'Dragon Touch'], MODEL, [TYPE, TABLET]], [ + + /android.+[;\/]\s*(NS-?.+)\s+build/i // Insignia Tablets + ], [MODEL, [VENDOR, 'Insignia'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*((NX|Next)-?.+)\s+build/i // NextBook Tablets + ], [MODEL, [VENDOR, 'NextBook'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*(Xtreme\_?)?(V(1[045]|2[015]|30|40|60|7[05]|90))\s+build/i + ], [[VENDOR, 'Voice'], MODEL, [TYPE, MOBILE]], [ // Voice Xtreme Phones + + /android.+[;\/]\s*(LVTEL\-?)?(V1[12])\s+build/i // LvTel Phones + ], [[VENDOR, 'LvTel'], MODEL, [TYPE, MOBILE]], [ + + /android.+[;\/]\s*(V(100MD|700NA|7011|917G).*\b)\s+build/i // Envizen Tablets + ], [MODEL, [VENDOR, 'Envizen'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*(Le[\s\-]+Pan)[\s\-]+(.*\b)\s+build/i // Le Pan Tablets + ], [VENDOR, MODEL, [TYPE, TABLET]], [ + + /android.+[;\/]\s*(Trio[\s\-]*.*)\s+build/i // MachSpeed Tablets + ], [MODEL, [VENDOR, 'MachSpeed'], [TYPE, TABLET]], [ + + /android.+[;\/]\s*(Trinity)[\-\s]*(T\d{3})\s+build/i // Trinity Tablets + ], [VENDOR, MODEL, [TYPE, TABLET]], [ + + /android.+[;\/]\s*TU_(1491)\s+build/i // Rotor Tablets + ], [MODEL, [VENDOR, 'Rotor'], [TYPE, TABLET]], [ + + /android.+(KS(.+))\s+build/i // Amazon Kindle Tablets + ], [MODEL, [VENDOR, 'Amazon'], [TYPE, TABLET]], [ + + /android.+(Gigaset)[\s\-]+(Q.+)\s+build/i // Gigaset Tablets + ], [VENDOR, MODEL, [TYPE, TABLET]], [ + + /\s(tablet|tab)[;\/]/i, // Unidentifiable Tablet + /\s(mobile)(?:[;\/]|\ssafari)/i // Unidentifiable Mobile + ], [[TYPE, util.lowerize], VENDOR, MODEL], [ + + /(android.+)[;\/].+build/i // Generic Android Device + ], [MODEL, [VENDOR, 'Generic']] + + + /*////////////////////////// + // TODO: move to string map + //////////////////////////// + + /(C6603)/i // Sony Xperia Z C6603 + ], [[MODEL, 'Xperia Z C6603'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [ + /(C6903)/i // Sony Xperia Z 1 + ], [[MODEL, 'Xperia Z 1'], [VENDOR, 'Sony'], [TYPE, MOBILE]], [ + + /(SM-G900[F|H])/i // Samsung Galaxy S5 + ], [[MODEL, 'Galaxy S5'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ + /(SM-G7102)/i // Samsung Galaxy Grand 2 + ], [[MODEL, 'Galaxy Grand 2'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ + /(SM-G530H)/i // Samsung Galaxy Grand Prime + ], [[MODEL, 'Galaxy Grand Prime'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ + /(SM-G313HZ)/i // Samsung Galaxy V + ], [[MODEL, 'Galaxy V'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ + /(SM-T805)/i // Samsung Galaxy Tab S 10.5 + ], [[MODEL, 'Galaxy Tab S 10.5'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [ + /(SM-G800F)/i // Samsung Galaxy S5 Mini + ], [[MODEL, 'Galaxy S5 Mini'], [VENDOR, 'Samsung'], [TYPE, MOBILE]], [ + /(SM-T311)/i // Samsung Galaxy Tab 3 8.0 + ], [[MODEL, 'Galaxy Tab 3 8.0'], [VENDOR, 'Samsung'], [TYPE, TABLET]], [ + + /(T3C)/i // Advan Vandroid T3C + ], [MODEL, [VENDOR, 'Advan'], [TYPE, TABLET]], [ + /(ADVAN T1J\+)/i // Advan Vandroid T1J+ + ], [[MODEL, 'Vandroid T1J+'], [VENDOR, 'Advan'], [TYPE, TABLET]], [ + /(ADVAN S4A)/i // Advan Vandroid S4A + ], [[MODEL, 'Vandroid S4A'], [VENDOR, 'Advan'], [TYPE, MOBILE]], [ + + /(V972M)/i // ZTE V972M + ], [MODEL, [VENDOR, 'ZTE'], [TYPE, MOBILE]], [ + + /(i-mobile)\s(IQ\s[\d\.]+)/i // i-mobile IQ + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + /(IQ6.3)/i // i-mobile IQ IQ 6.3 + ], [[MODEL, 'IQ 6.3'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [ + /(i-mobile)\s(i-style\s[\d\.]+)/i // i-mobile i-STYLE + ], [VENDOR, MODEL, [TYPE, MOBILE]], [ + /(i-STYLE2.1)/i // i-mobile i-STYLE 2.1 + ], [[MODEL, 'i-STYLE 2.1'], [VENDOR, 'i-mobile'], [TYPE, MOBILE]], [ + + /(mobiistar touch LAI 512)/i // mobiistar touch LAI 512 + ], [[MODEL, 'Touch LAI 512'], [VENDOR, 'mobiistar'], [TYPE, MOBILE]], [ + + ///////////// + // END TODO + ///////////*/ + + ], + + engine : [[ + + /windows.+\sedge\/([\w\.]+)/i // EdgeHTML + ], [VERSION, [NAME, 'EdgeHTML']], [ + + /(presto)\/([\w\.]+)/i, // Presto + /(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i, // WebKit/Trident/NetFront/NetSurf/Amaya/Lynx/w3m + /(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i, // KHTML/Tasman/Links + /(icab)[\/\s]([23]\.[\d\.]+)/i // iCab + ], [NAME, VERSION], [ + + /rv\:([\w\.]+).*(gecko)/i // Gecko + ], [VERSION, NAME] + ], + + os : [[ + + // Windows based + /microsoft\s(windows)\s(vista|xp)/i // Windows (iTunes) + ], [NAME, VERSION], [ + /(windows)\snt\s6\.2;\s(arm)/i, // Windows RT + /(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s]+\w)*/i, // Windows Phone + /(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i + ], [NAME, [VERSION, mapper.str, maps.os.windows.version]], [ + /(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i + ], [[NAME, 'Windows'], [VERSION, mapper.str, maps.os.windows.version]], [ + + // Mobile/Embedded OS + /\((bb)(10);/i // BlackBerry 10 + ], [[NAME, 'BlackBerry'], VERSION], [ + /(blackberry)\w*\/?([\w\.]+)*/i, // Blackberry + /(tizen)[\/\s]([\w\.]+)/i, // Tizen + /(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i, + // Android/WebOS/Palm/QNX/Bada/RIM/MeeGo/Contiki + /linux;.+(sailfish);/i // Sailfish OS + ], [NAME, VERSION], [ + /(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i // Symbian + ], [[NAME, 'Symbian'], VERSION], [ + /\((series40);/i // Series 40 + ], [NAME], [ + /mozilla.+\(mobile;.+gecko.+firefox/i // Firefox OS + ], [[NAME, 'Firefox OS'], VERSION], [ + + // Console + /(nintendo|playstation)\s([wids34portablevu]+)/i, // Nintendo/Playstation + + // GNU/Linux based + /(mint)[\/\s\(]?(\w+)*/i, // Mint + /(mageia|vectorlinux)[;\s]/i, // Mageia/VectorLinux + /(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]+)*/i, + // Joli/Ubuntu/Debian/SUSE/Gentoo/Arch/Slackware + // Fedora/Mandriva/CentOS/PCLinuxOS/RedHat/Zenwalk/Linpus + /(hurd|linux)\s?([\w\.]+)*/i, // Hurd/Linux + /(gnu)\s?([\w\.]+)*/i // GNU + ], [NAME, VERSION], [ + + /(cros)\s[\w]+\s([\w\.]+\w)/i // Chromium OS + ], [[NAME, 'Chromium OS'], VERSION],[ + + // Solaris + /(sunos)\s?([\w\.]+\d)*/i // Solaris + ], [[NAME, 'Solaris'], VERSION], [ + + // BSD based + /\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i // FreeBSD/NetBSD/OpenBSD/PC-BSD/DragonFly + ], [NAME, VERSION],[ + + /(haiku)\s(\w+)/i // Haiku + ], [NAME, VERSION],[ + + /cfnetwork\/.+darwin/i, + /ip[honead]+(?:.*os\s([\w]+)\slike\smac|;\sopera)/i // iOS + ], [[VERSION, /_/g, '.'], [NAME, 'iOS']], [ + + /(mac\sos\sx)\s?([\w\s\.]+\w)*/i, + /(macintosh|mac(?=_powerpc)\s)/i // Mac OS + ], [[NAME, 'Mac OS'], [VERSION, /_/g, '.']], [ + + // Other + /((?:open)?solaris)[\/\s-]?([\w\.]+)*/i, // Solaris + /(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i, // AIX + /(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i, + // Plan9/Minix/BeOS/OS2/AmigaOS/MorphOS/RISCOS/OpenVMS + /(unix)\s?([\w\.]+)*/i // UNIX + ], [NAME, VERSION] + ] + }; + + + ///////////////// + // Constructor + //////////////// + /* + var Browser = function (name, version) { + this[NAME] = name; + this[VERSION] = version; + }; + var CPU = function (arch) { + this[ARCHITECTURE] = arch; + }; + var Device = function (vendor, model, type) { + this[VENDOR] = vendor; + this[MODEL] = model; + this[TYPE] = type; + }; + var Engine = Browser; + var OS = Browser; + */ + var UAParser = function (uastring, extensions) { + + if (typeof uastring === 'object') { + extensions = uastring; + uastring = undefined; + } + + if (!(this instanceof UAParser)) { + return new UAParser(uastring, extensions).getResult(); + } + + var ua = uastring || ((window && window.navigator && window.navigator.userAgent) ? window.navigator.userAgent : EMPTY); + var rgxmap = extensions ? util.extend(regexes, extensions) : regexes; + //var browser = new Browser(); + //var cpu = new CPU(); + //var device = new Device(); + //var engine = new Engine(); + //var os = new OS(); + + this.getBrowser = function () { + var browser = { name: undefined, version: undefined }; + mapper.rgx.call(browser, ua, rgxmap.browser); + browser.major = util.major(browser.version); // deprecated + return browser; + }; + this.getCPU = function () { + var cpu = { architecture: undefined }; + mapper.rgx.call(cpu, ua, rgxmap.cpu); + return cpu; + }; + this.getDevice = function () { + var device = { vendor: undefined, model: undefined, type: undefined }; + mapper.rgx.call(device, ua, rgxmap.device); + return device; + }; + this.getEngine = function () { + var engine = { name: undefined, version: undefined }; + mapper.rgx.call(engine, ua, rgxmap.engine); + return engine; + }; + this.getOS = function () { + var os = { name: undefined, version: undefined }; + mapper.rgx.call(os, ua, rgxmap.os); + return os; + }; + this.getResult = function () { + return { + ua : this.getUA(), + browser : this.getBrowser(), + engine : this.getEngine(), + os : this.getOS(), + device : this.getDevice(), + cpu : this.getCPU() + }; + }; + this.getUA = function () { + return ua; + }; + this.setUA = function (uastring) { + ua = uastring; + //browser = new Browser(); + //cpu = new CPU(); + //device = new Device(); + //engine = new Engine(); + //os = new OS(); + return this; + }; + return this; + }; + + UAParser.VERSION = LIBVERSION; + UAParser.BROWSER = { + NAME : NAME, + MAJOR : MAJOR, // deprecated + VERSION : VERSION + }; + UAParser.CPU = { + ARCHITECTURE : ARCHITECTURE + }; + UAParser.DEVICE = { + MODEL : MODEL, + VENDOR : VENDOR, + TYPE : TYPE, + CONSOLE : CONSOLE, + MOBILE : MOBILE, + SMARTTV : SMARTTV, + TABLET : TABLET, + WEARABLE: WEARABLE, + EMBEDDED: EMBEDDED + }; + UAParser.ENGINE = { + NAME : NAME, + VERSION : VERSION + }; + UAParser.OS = { + NAME : NAME, + VERSION : VERSION + }; + //UAParser.Utils = util; + + /////////// + // Export + ////////// + + + // check js environment + if (typeof(exports) !== UNDEF_TYPE) { + // nodejs env + if (typeof module !== UNDEF_TYPE && module.exports) { + exports = module.exports = UAParser; + } + // TODO: test!!!!!!!! + /* + if (require && require.main === module && process) { + // cli + var jsonize = function (arr) { + var res = []; + for (var i in arr) { + res.push(new UAParser(arr[i]).getResult()); + } + process.stdout.write(JSON.stringify(res, null, 2) + '\n'); + }; + if (process.stdin.isTTY) { + // via args + jsonize(process.argv.slice(2)); + } else { + // via pipe + var str = ''; + process.stdin.on('readable', function() { + var read = process.stdin.read(); + if (read !== null) { + str += read; + } + }); + process.stdin.on('end', function () { + jsonize(str.replace(/\n$/, '').split('\n')); + }); + } + } + */ + exports.UAParser = UAParser; + } else { + // requirejs env (optional) + if (typeof(define) === FUNC_TYPE && define.amd) { + define(function () { + return UAParser; + }); + } else if (window) { + // browser env + window.UAParser = UAParser; + } + } + + // jQuery/Zepto specific (optional) + // Note: + // In AMD env the global scope should be kept clean, but jQuery is an exception. + // jQuery always exports to global scope, unless jQuery.noConflict(true) is used, + // and we should catch that. + var $ = window && (window.jQuery || window.Zepto); + if (typeof $ !== UNDEF_TYPE) { + var parser = new UAParser(); + $.ua = parser.getResult(); + $.ua.get = function () { + return parser.getUA(); + }; + $.ua.set = function (uastring) { + parser.setUA(uastring); + var result = parser.getResult(); + for (var prop in result) { + $.ua[prop] = result[prop]; + } + }; + } + +})(typeof window === 'object' ? window : this); + +},{}],22:[function(require,module,exports){ +(function (global){ + +var rng; + +var crypto = global.crypto || global.msCrypto; // for IE 11 +if (crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; +} + +if (!rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + rng = function() { + for (var i = 0, r; i < 16; i++) { + if ((i & 0x03) === 0) r = Math.random() * 0x100000000; + _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return _rnds; + }; +} + +module.exports = rng; + + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],23:[function(require,module,exports){ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +// Unique ID creation requires a high quality random # generator. We feature +// detect to determine the best RNG source, normalizing to a function that +// returns 128-bits of randomness, since that's what's usually required +var _rng = require('./rng'); + +// Maps for number <-> hex string conversion +var _byteToHex = []; +var _hexToByte = {}; +for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; +} + +// **`parse()` - Parse a UUID into it's component bytes** +function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; +} + +// **`unparse()` - Convert UUID byte array (ala parse()) into a string** +function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; +} + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html + +// random #'s we need to init node and clockseq +var _seedBytes = _rng(); + +// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) +var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] +]; + +// Per 4.2.2, randomize (14 bit) clockseq +var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + +// Previous uuid creation time +var _lastMSecs = 0, _lastNSecs = 0; + +// See https://github.com/broofa/node-uuid for API details +function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); +} + +// **`v4()` - Generate random UUID** + +// See https://github.com/broofa/node-uuid for API details +function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); +} + +// Export public API +var uuid = v4; +uuid.v1 = v1; +uuid.v4 = v4; +uuid.parse = parse; +uuid.unparse = unparse; + +module.exports = uuid; + +},{"./rng":22}],24:[function(require,module,exports){ +/* +WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based +on @visionmedia's Emitter from UI Kit. + +Why? I wanted it standalone. + +I also wanted support for wildcard emitters like this: + +emitter.on('*', function (eventName, other, event, payloads) { + +}); + +emitter.on('somenamespace*', function (eventName, payloads) { + +}); + +Please note that callbacks triggered by wildcard registered events also get +the event name as the first argument. +*/ + +module.exports = WildEmitter; + +function WildEmitter() { } + +WildEmitter.mixin = function (constructor) { + var prototype = constructor.prototype || constructor; + + prototype.isWildEmitter= true; + + // Listen on the given `event` with `fn`. Store a group name if present. + prototype.on = function (event, groupName, fn) { + this.callbacks = this.callbacks || {}; + var hasGroup = (arguments.length === 3), + group = hasGroup ? arguments[1] : undefined, + func = hasGroup ? arguments[2] : arguments[1]; + func._groupName = group; + (this.callbacks[event] = this.callbacks[event] || []).push(func); + return this; + }; + + // Adds an `event` listener that will be invoked a single + // time then automatically removed. + prototype.once = function (event, groupName, fn) { + var self = this, + hasGroup = (arguments.length === 3), + group = hasGroup ? arguments[1] : undefined, + func = hasGroup ? arguments[2] : arguments[1]; + function on() { + self.off(event, on); + func.apply(this, arguments); + } + this.on(event, group, on); + return this; + }; + + // Unbinds an entire group + prototype.releaseGroup = function (groupName) { + this.callbacks = this.callbacks || {}; + var item, i, len, handlers; + for (item in this.callbacks) { + handlers = this.callbacks[item]; + for (i = 0, len = handlers.length; i < len; i++) { + if (handlers[i]._groupName === groupName) { + //console.log('removing'); + // remove it and shorten the array we're looping through + handlers.splice(i, 1); + i--; + len--; + } + } + } + return this; + }; + + // Remove the given callback for `event` or all + // registered callbacks. + prototype.off = function (event, fn) { + this.callbacks = this.callbacks || {}; + var callbacks = this.callbacks[event], + i; + + if (!callbacks) return this; + + // remove all handlers + if (arguments.length === 1) { + delete this.callbacks[event]; + return this; + } + + // remove specific handler + i = callbacks.indexOf(fn); + callbacks.splice(i, 1); + if (callbacks.length === 0) { + delete this.callbacks[event]; + } + return this; + }; + + /// Emit `event` with the given args. + // also calls any `*` handlers + prototype.emit = function (event) { + this.callbacks = this.callbacks || {}; + var args = [].slice.call(arguments, 1), + callbacks = this.callbacks[event], + specialCallbacks = this.getWildcardCallbacks(event), + i, + len, + item, + listeners; + + if (callbacks) { + listeners = callbacks.slice(); + for (i = 0, len = listeners.length; i < len; ++i) { + if (!listeners[i]) { + break; + } + listeners[i].apply(this, args); + } + } + + if (specialCallbacks) { + len = specialCallbacks.length; + listeners = specialCallbacks.slice(); + for (i = 0, len = listeners.length; i < len; ++i) { + if (!listeners[i]) { + break; + } + listeners[i].apply(this, [event].concat(args)); + } + } + + return this; + }; + + // Helper for for finding special wildcard event handlers that match the event + prototype.getWildcardCallbacks = function (eventName) { + this.callbacks = this.callbacks || {}; + var item, + split, + result = []; + + for (item in this.callbacks) { + split = item.split('*'); + if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) { + result = result.concat(this.callbacks[item]); + } + } + return result; + }; + +}; + +WildEmitter.mixin(WildEmitter); + +},{}]},{},[2])(2) +}); \ No newline at end of file diff --git a/bigbluebutton-client/resources/prod/lib/kurento-utils.min.js b/bigbluebutton-client/resources/prod/lib/kurento-utils.min.js deleted file mode 100644 index 4e88a14cb52432fe068dd62adf6ec94350103ae2..0000000000000000000000000000000000000000 --- a/bigbluebutton-client/resources/prod/lib/kurento-utils.min.js +++ /dev/null @@ -1,56 +0,0 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.kurentoUtils = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ -function noop(e){e&&logger.error(e)}function trackStop(e){e.stop&&e.stop()}function streamStop(e){e.getTracks().forEach(trackStop)}function bufferizeCandidates(e,n){var t=[];return e.addEventListener("signalingstatechange",function(){if("stable"===this.signalingState)for(;t.length;){var e=t.shift();this.addIceCandidate(e.candidate,e.callback,e.callback)}}),function(r,i){switch(i=i||n,e.signalingState){case"closed":i(new Error("PeerConnection object is closed"));break;case"stable":if(e.remoteDescription){e.addIceCandidate(r,i,i);break}default:t.push({candidate:r,callback:i})}}}function removeFIDFromOffer(e){var n=e.indexOf("a=ssrc-group:FID");return n>0?e.slice(0,n):e}function getSimulcastInfo(e){var n=e.getVideoTracks();if(!n.length)return logger.warn("No video tracks available in the video stream"),"";var t=["a=x-google-flag:conference","a=ssrc-group:SIM 1 2 3","a=ssrc:1 cname:localVideo","a=ssrc:1 msid:"+e.id+" "+n[0].id,"a=ssrc:1 mslabel:"+e.id,"a=ssrc:1 label:"+n[0].id,"a=ssrc:2 cname:localVideo","a=ssrc:2 msid:"+e.id+" "+n[0].id,"a=ssrc:2 mslabel:"+e.id,"a=ssrc:2 label:"+n[0].id,"a=ssrc:3 cname:localVideo","a=ssrc:3 msid:"+e.id+" "+n[0].id,"a=ssrc:3 mslabel:"+e.id,"a=ssrc:3 label:"+n[0].id];return t.push(""),t.join("\n")}function WebRtcPeer(e,n,t){function r(){if(l){var e=p.getRemoteStreams()[0],n=e?URL.createObjectURL(e):"";l.pause(),l.src=n,l.load(),logger.info("Remote URL:",n)}}function i(e){return w&&("Chrome"===browser.name||"Chromium"===browser.name?(logger.info("Adding multicast info"),e=new RTCSessionDescription({type:e.type,sdp:removeFIDFromOffer(e.sdp)+getSimulcastInfo(u)})):logger.warn("Simulcast is only available in Chrome browser.")),e}function o(){"closed"===p.signalingState&&t('The peer connection object is in "closed" state. This is most likely due to an invocation of the dispose method before accepting in the dialogue'),u&&d&&c.showLocalVideo(),u&&p.addStream(u),f&&p.addStream(f);var n=parser.getBrowser();"sendonly"!==e||"Chrome"!==n.name&&"Chromium"!==n.name||39!==n.major||(e="sendrecv"),t()}function a(e){void 0===e&&(e=MEDIA_CONSTRAINTS),navigator.mediaDevices.getUserMedia(e).then(function(e){u=e,o()}).catch(t)}if(!(this instanceof WebRtcPeer))return new WebRtcPeer(e,n,t);WebRtcPeer.super_.call(this),n instanceof Function&&(t=n,n=void 0),n=n||{},t=(t||noop).bind(this);var s,c=this,d=n.localVideo,l=n.remoteVideo,u=n.videoStream,f=n.audioStream,g=n.mediaConstraints,p=(n.connectionConstraints,n.peerConnection),h=n.sendSource||"webcam",m=n.dataChannelConfig,v=n.dataChannels||!1,b=uuid.v4(),P=recursive({iceServers:freeice()},n.configuration),S=n.onicecandidate;S&&this.on("icecandidate",S);var R=n.oncandidategatheringdone;R&&this.on("candidategatheringdone",R);var w=n.simulcast,C=n.multistream,y=new sdpTranslator.Interop,D=[],W=!1;if(Object.defineProperties(this,{peerConnection:{get:function(){return p}},id:{value:n.id||b,writable:!1},remoteVideo:{get:function(){return l}},localVideo:{get:function(){return d}},dataChannel:{get:function(){return s}},currentFrame:{get:function(){if(l){if(l.readyState<l.HAVE_CURRENT_DATA)throw new Error("No video stream data available");var e=document.createElement("canvas");return e.width=l.videoWidth,e.height=l.videoHeight,e.getContext("2d").drawImage(l,0,0),e}}}}),!p&&(p=new RTCPeerConnection(P),v&&!s)){var E="WebRtcPeer-"+c.id,T=void 0;m&&(E=m.id||E,T=m.options),s=p.createDataChannel(E,T),m&&(s.onopen=m.onopen,s.onclose=m.onclose,s.onmessage=m.onmessage,s.onbufferedamountlow=m.onbufferedamountlow,s.onerror=m.onerror||noop)}p.addEventListener("icecandidate",function(e){var n=e.candidate;if(EventEmitter.listenerCount(c,"icecandidate")||EventEmitter.listenerCount(c,"candidategatheringdone"))if(n){var t;t=C&&usePlanB?y.candidateToUnifiedPlan(n):n,c.emit("icecandidate",t),W=!1}else W||(c.emit("candidategatheringdone"),W=!0);else W||(D.push(n),n||(W=!0))}),p.ontrack=n.onaddstream,p.onnegotiationneeded=n.onnegotiationneeded,this.on("newListener",function(e,n){if("icecandidate"===e||"candidategatheringdone"===e)for(;D.length;){var t=D.shift();!t==("candidategatheringdone"===e)&&n(t)}});var k=bufferizeCandidates(p);this.addIceCandidate=function(e,n){var t;t=C&&usePlanB?y.candidateToPlanB(e):new RTCIceCandidate(e),logger.debug("Remote ICE candidate received",e),n=(n||noop).bind(this),k(t,n)},this.generateOffer=function(n){n=n.bind(this);var t=!0,r=!0;g&&(t="boolean"!=typeof g.audio||g.audio,r="boolean"!=typeof g.video||g.video);var o={offerToReceiveAudio:"sendonly"!==e&&t,offerToReceiveVideo:"sendonly"!==e&&r},a=o;logger.info("constraints: "+JSON.stringify(a)),p.createOffer(a).then(function(e){return logger.info("Created SDP offer"),e=i(e),p.setLocalDescription(e)}).then(function(){var e=p.localDescription;logger.info("Local description set",e.sdp),C&&usePlanB&&(e=y.toUnifiedPlan(e),logger.info("offer::origPlanB->UnifiedPlan",dumpSDP(e))),n(null,e.sdp,c.processAnswer.bind(c))}).catch(n)},this.getLocalSessionDescriptor=function(){return p.localDescription},this.getRemoteSessionDescriptor=function(){return p.remoteDescription},this.showLocalVideo=function(){d.src=URL.createObjectURL(u),d.muted=!0},this.send=function(e){s&&"open"===s.readyState?s.send(e):logger.warn("Trying to send data over a non-existing or closed data channel")},this.processAnswer=function(e,n){n=(n||noop).bind(this);var t=new RTCSessionDescription({type:"answer",sdp:e});if(C&&usePlanB){var i=y.toPlanB(t);logger.info("asnwer::planB",dumpSDP(i)),t=i}if(logger.info("SDP answer received, setting remote description"),"closed"===p.signalingState)return n("PeerConnection is closed");p.setRemoteDescription(t,function(){r(),n()},n)},this.processOffer=function(e,n){n=n.bind(this);var t=new RTCSessionDescription({type:"offer",sdp:e});if(C&&usePlanB){var o=y.toPlanB(t);logger.info("offer::planB",dumpSDP(o)),t=o}if(logger.info("SDP offer received, setting remote description"),"closed"===p.signalingState)return n("PeerConnection is closed");p.setRemoteDescription(t).then(function(){return r()}).then(function(){return p.createAnswer()}).then(function(e){return e=i(e),logger.info("Created SDP answer"),p.setLocalDescription(e)}).then(function(){var e=p.localDescription;C&&usePlanB&&(e=y.toUnifiedPlan(e),logger.info("answer::origPlanB->UnifiedPlan",dumpSDP(e))),logger.info("Local description set",e.sdp),n(null,e.sdp)}).catch(n)},"recvonly"===e||u||f?setTimeout(o,0):"webcam"===h?a(g):getScreenConstraints(h,function(e,n){if(e)return t(e);constraints=[g],constraints.unshift(n),a(recursive.apply(void 0,constraints))},b),this.on("_dispose",function(){d&&(d.pause(),d.src="",d.load(),d.muted=!1),l&&(l.pause(),l.src="",l.load()),c.removeAllListeners(),void 0!==window.cancelChooseDesktopMedia&&window.cancelChooseDesktopMedia(b)})}function createEnableDescriptor(e){var n="get"+e+"Tracks";return{enumerable:!0,get:function(){if(this.peerConnection){var e=this.peerConnection.getLocalStreams();if(e.length){for(var t,r=0;t=e[r];r++)for(var i,o=t[n](),a=0;i=o[a];a++)if(!i.enabled)return!1;return!0}}},set:function(e){function t(n){n.enabled=e}this.peerConnection.getLocalStreams().forEach(function(e){e[n]().forEach(t)})}}}function WebRtcPeerRecvonly(e,n){if(!(this instanceof WebRtcPeerRecvonly))return new WebRtcPeerRecvonly(e,n);WebRtcPeerRecvonly.super_.call(this,"recvonly",e,n)}function WebRtcPeerSendonly(e,n){if(!(this instanceof WebRtcPeerSendonly))return new WebRtcPeerSendonly(e,n);WebRtcPeerSendonly.super_.call(this,"sendonly",e,n)}function WebRtcPeerSendrecv(e,n){if(!(this instanceof WebRtcPeerSendrecv))return new WebRtcPeerSendrecv(e,n);WebRtcPeerSendrecv.super_.call(this,"sendrecv",e,n)}function harkUtils(e,n){return hark(e,n)}var freeice=require("freeice"),inherits=require("inherits"),UAParser=require("ua-parser-js"),uuid=require("uuid"),hark=require("hark"),EventEmitter=require("events").EventEmitter,recursive=require("merge").recursive.bind(void 0,!0),sdpTranslator=require("sdp-translator"),logger=window.Logger||console;try{require("kurento-browser-extensions")}catch(e){"undefined"==typeof getScreenConstraints&&(logger.warn("screen sharing is not available"),getScreenConstraints=function(e,n){n(new Error("This library is not enabled for screen sharing"))})}var MEDIA_CONSTRAINTS={audio:!0,video:{width:640,framerate:15}},ua=window&&window.navigator?window.navigator.userAgent:"",parser=new UAParser(ua),browser=parser.getBrowser(),usePlanB=!1;"Chrome"!==browser.name&&"Chromium"!==browser.name||(logger.info(browser.name+": using SDP PlanB"),usePlanB=!0);var dumpSDP=function(e){return void 0===e||null===e?"":"type: "+e.type+"\r\n"+e.sdp};inherits(WebRtcPeer,EventEmitter),Object.defineProperties(WebRtcPeer.prototype,{enabled:{enumerable:!0,get:function(){return this.audioEnabled&&this.videoEnabled},set:function(e){this.audioEnabled=this.videoEnabled=e}},audioEnabled:createEnableDescriptor("Audio"),videoEnabled:createEnableDescriptor("Video")}),WebRtcPeer.prototype.getLocalStream=function(e){if(this.peerConnection)return this.peerConnection.getLocalStreams()[e||0]},WebRtcPeer.prototype.getRemoteStream=function(e){if(this.peerConnection)return this.peerConnection.getRemoteStreams()[e||0]},WebRtcPeer.prototype.dispose=function(){logger.info("Disposing WebRtcPeer");var e=this.peerConnection,n=this.dataChannel;try{if(n){if("closed"===n.signalingState)return;n.close()}if(e){if("closed"===e.signalingState)return;e.getLocalStreams().forEach(streamStop),e.close()}}catch(e){logger.warn("Exception disposing webrtc peer "+e)}this.emit("_dispose")},inherits(WebRtcPeerRecvonly,WebRtcPeer),inherits(WebRtcPeerSendonly,WebRtcPeer),inherits(WebRtcPeerSendrecv,WebRtcPeer),exports.bufferizeCandidates=bufferizeCandidates,exports.WebRtcPeerRecvonly=WebRtcPeerRecvonly,exports.WebRtcPeerSendonly=WebRtcPeerSendonly,exports.WebRtcPeerSendrecv=WebRtcPeerSendrecv,exports.hark=harkUtils; -},{"events":4,"freeice":5,"hark":8,"inherits":9,"kurento-browser-extensions":10,"merge":11,"sdp-translator":18,"ua-parser-js":21,"uuid":23}],2:[function(require,module,exports){ -window.addEventListener&&(module.exports=require("./index")); -},{"./index":3}],3:[function(require,module,exports){ -var WebRtcPeer=require("./WebRtcPeer");exports.WebRtcPeer=WebRtcPeer; -},{"./WebRtcPeer":1}],4:[function(require,module,exports){ -function EventEmitter(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function isFunction(e){return"function"==typeof e}function isNumber(e){return"number"==typeof e}function isObject(e){return"object"==typeof e&&null!==e}function isUndefined(e){return void 0===e}module.exports=EventEmitter,EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._maxListeners=void 0,EventEmitter.defaultMaxListeners=10,EventEmitter.prototype.setMaxListeners=function(e){if(!isNumber(e)||e<0||isNaN(e))throw TypeError("n must be a positive number");return this._maxListeners=e,this},EventEmitter.prototype.emit=function(e){var t,i,n,s,r,o;if(this._events||(this._events={}),"error"===e&&(!this._events.error||isObject(this._events.error)&&!this._events.error.length)){if((t=arguments[1])instanceof Error)throw t;var h=new Error('Uncaught, unspecified "error" event. ('+t+")");throw h.context=t,h}if(i=this._events[e],isUndefined(i))return!1;if(isFunction(i))switch(arguments.length){case 1:i.call(this);break;case 2:i.call(this,arguments[1]);break;case 3:i.call(this,arguments[1],arguments[2]);break;default:s=Array.prototype.slice.call(arguments,1),i.apply(this,s)}else if(isObject(i))for(s=Array.prototype.slice.call(arguments,1),o=i.slice(),n=o.length,r=0;r<n;r++)o[r].apply(this,s);return!0},EventEmitter.prototype.addListener=function(e,t){var i;if(!isFunction(t))throw TypeError("listener must be a function");return this._events||(this._events={}),this._events.newListener&&this.emit("newListener",e,isFunction(t.listener)?t.listener:t),this._events[e]?isObject(this._events[e])?this._events[e].push(t):this._events[e]=[this._events[e],t]:this._events[e]=t,isObject(this._events[e])&&!this._events[e].warned&&(i=isUndefined(this._maxListeners)?EventEmitter.defaultMaxListeners:this._maxListeners)&&i>0&&this._events[e].length>i&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace()),this},EventEmitter.prototype.on=EventEmitter.prototype.addListener,EventEmitter.prototype.once=function(e,t){function i(){this.removeListener(e,i),n||(n=!0,t.apply(this,arguments))}if(!isFunction(t))throw TypeError("listener must be a function");var n=!1;return i.listener=t,this.on(e,i),this},EventEmitter.prototype.removeListener=function(e,t){var i,n,s,r;if(!isFunction(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(i=this._events[e],s=i.length,n=-1,i===t||isFunction(i.listener)&&i.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(isObject(i)){for(r=s;r-- >0;)if(i[r]===t||i[r].listener&&i[r].listener===t){n=r;break}if(n<0)return this;1===i.length?(i.length=0,delete this._events[e]):i.splice(n,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},EventEmitter.prototype.removeAllListeners=function(e){var t,i;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(i=this._events[e],isFunction(i))this.removeListener(e,i);else if(i)for(;i.length;)this.removeListener(e,i[i.length-1]);return delete this._events[e],this},EventEmitter.prototype.listeners=function(e){return this._events&&this._events[e]?isFunction(this._events[e])?[this._events[e]]:this._events[e].slice():[]},EventEmitter.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(isFunction(t))return 1;if(t)return t.length}return 0},EventEmitter.listenerCount=function(e,t){return e.listenerCount(t)}; -},{}],5:[function(require,module,exports){ -"use strict";var normalice=require("normalice"),freeice=module.exports=function(n){function t(n,t){for(var r,u=[],o=[].concat(e[n]);o.length&&u.length<t;)r=Math.random()*o.length|0,u=u.concat(o.splice(r,1));return u.map(function(t){return"string"==typeof t||t instanceof String?normalice(n+":"+t):t})}var r,e={stun:(n||{}).stun||require("./stun.json"),turn:(n||{}).turn||require("./turn.json")},u=(n||{}).stunCount||2,o=(n||{}).turnCount||0;return r=[].concat(t("stun",u)),o&&(r=r.concat(t("turn",o))),r}; -},{"./stun.json":6,"./turn.json":7,"normalice":12}],6:[function(require,module,exports){ -module.exports=["stun.l.google.com:19302","stun1.l.google.com:19302","stun2.l.google.com:19302","stun3.l.google.com:19302","stun4.l.google.com:19302","stun.ekiga.net","stun.ideasip.com","stun.schlund.de","stun.stunprotocol.org:3478","stun.voiparound.com","stun.voipbuster.com","stun.voipstunt.com","stun.voxgratia.org","stun.services.mozilla.com"] -},{}],7:[function(require,module,exports){ -module.exports=[] -},{}],8:[function(require,module,exports){ -function getMaxVolume(e,t){var i=-1/0;e.getFloatFrequencyData(t);for(var n=4,o=t.length;n<o;n++)t[n]>i&&t[n]<0&&(i=t[n]);return i}var WildEmitter=require("wildemitter"),audioContextType=window.AudioContext||window.webkitAudioContext,audioContext=null;module.exports=function(e,t){var i=new WildEmitter;if(!audioContextType)return i;var t=t||{},n=t.smoothing||.1,o=t.interval||50,a=t.threshold,r=t.play,s=t.history||10,u=!0;audioContext||(audioContext=new audioContextType);var p,g,d;d=audioContext.createAnalyser(),d.fftSize=512,d.smoothingTimeConstant=n,g=new Float32Array(d.fftSize),e.jquery&&(e=e[0]),e instanceof HTMLAudioElement||e instanceof HTMLVideoElement?(p=audioContext.createMediaElementSource(e),void 0===r&&(r=!0),a=a||-50):(p=audioContext.createMediaStreamSource(e),a=a||-50),p.connect(d),r&&d.connect(audioContext.destination),i.speaking=!1,i.setThreshold=function(e){a=e},i.setInterval=function(e){o=e},i.stop=function(){u=!1,i.emit("volume_change",-100,a),i.speaking&&(i.speaking=!1,i.emit("stopped_speaking"))},i.speakingHistory=[];for(var l=0;l<s;l++)i.speakingHistory.push(0);var f=function(){setTimeout(function(){if(u){var e=getMaxVolume(d,g);i.emit("volume_change",e,a);var t=0;if(e>a&&!i.speaking){for(var n=i.speakingHistory.length-3;n<i.speakingHistory.length;n++)t+=i.speakingHistory[n];t>=2&&(i.speaking=!0,i.emit("speaking"))}else if(e<a&&i.speaking){for(var n=0;n<i.speakingHistory.length;n++)t+=i.speakingHistory[n];0==t&&(i.speaking=!1,i.emit("stopped_speaking"))}i.speakingHistory.shift(),i.speakingHistory.push(0+(e>a)),f()}},o)};return f(),i}; -},{"wildemitter":24}],9:[function(require,module,exports){ -"function"==typeof Object.create?module.exports=function(t,e){t.super_=e,t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}})}:module.exports=function(t,e){t.super_=e;var o=function(){};o.prototype=e.prototype,t.prototype=new o,t.prototype.constructor=t}; -},{}],10:[function(require,module,exports){ - -},{}],11:[function(require,module,exports){ -!function(e){function o(e,r){if("object"!==n(e))return r;for(var t in r)"object"===n(e[t])&&"object"===n(r[t])?e[t]=o(e[t],r[t]):e[t]=r[t];return e}function r(e,r,c){var u=c[0],f=c.length;(e||"object"!==n(u))&&(u={});for(var i=0;i<f;++i){var l=c[i];if("object"===n(l))for(var a in l){var v=e?t.clone(l[a]):l[a];u[a]=r?o(u[a],v):v}}return u}function n(e){return{}.toString.call(e).slice(8,-1).toLowerCase()}var t=function(e){return r(!0===e,!1,arguments)};t.recursive=function(e){return r(!0===e,!0,arguments)},t.clone=function(e){var o,r,c=e,u=n(e);if("array"===u)for(c=[],r=e.length,o=0;o<r;++o)c[o]=t.clone(e[o]);else if("object"===u){c={};for(o in e)c[o]=t.clone(e[o])}return c},e?module.exports=t:window.merge=t}("object"==typeof module&&module&&"object"==typeof module.exports&&module.exports); -},{}],12:[function(require,module,exports){ -var protocols=["stun:","turn:"];module.exports=function(e){var r,t,n=(e||{}).url||e,l={};return"string"==typeof n||n instanceof String?(n=n.trim(),(r=protocols[protocols.indexOf(n.slice(0,5))])?(n=n.slice(5),t=n.split("@"),l.username=e.username,l.credential=e.credential,t.length>1&&(n=t[1],t=t[0].split(":"),l.username=t[0],l.credential=(e||{}).credential||t[1]||""),l.url=r+n,l.urls=[l.url],l):e):e}; -},{}],13:[function(require,module,exports){ -var grammar=module.exports={v:[{name:"version",reg:/^(\d*)$/}],o:[{name:"origin",reg:/^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)/,names:["username","sessionId","sessionVersion","netType","ipVer","address"],format:"%s %s %d %s IP%d %s"}],s:[{name:"name"}],i:[{name:"description"}],u:[{name:"uri"}],e:[{name:"email"}],p:[{name:"phone"}],z:[{name:"timezones"}],r:[{name:"repeats"}],t:[{name:"timing",reg:/^(\d*) (\d*)/,names:["start","stop"],format:"%d %d"}],c:[{name:"connection",reg:/^IN IP(\d) (\S*)/,names:["version","ip"],format:"IN IP%d %s"}],b:[{push:"bandwidth",reg:/^(TIAS|AS|CT|RR|RS):(\d*)/,names:["type","limit"],format:"%s:%s"}],m:[{reg:/^(\w*) (\d*) ([\w\/]*)(?: (.*))?/,names:["type","port","protocol","payloads"],format:"%s %d %s %s"}],a:[{push:"rtp",reg:/^rtpmap:(\d*) ([\w\-]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?/,names:["payload","codec","rate","encoding"],format:function(e){return e.encoding?"rtpmap:%d %s/%s/%s":e.rate?"rtpmap:%d %s/%s":"rtpmap:%d %s"}},{push:"fmtp",reg:/^fmtp:(\d*) ([\S| ]*)/,names:["payload","config"],format:"fmtp:%d %s"},{name:"control",reg:/^control:(.*)/,format:"control:%s"},{name:"rtcp",reg:/^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?/,names:["port","netType","ipVer","address"],format:function(e){return null!=e.address?"rtcp:%d %s IP%d %s":"rtcp:%d"}},{push:"rtcpFbTrrInt",reg:/^rtcp-fb:(\*|\d*) trr-int (\d*)/,names:["payload","value"],format:"rtcp-fb:%d trr-int %d"},{push:"rtcpFb",reg:/^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?/,names:["payload","type","subtype"],format:function(e){return null!=e.subtype?"rtcp-fb:%s %s %s":"rtcp-fb:%s %s"}},{push:"ext",reg:/^extmap:([\w_\/]*) (\S*)(?: (\S*))?/,names:["value","uri","config"],format:function(e){return null!=e.config?"extmap:%s %s %s":"extmap:%s %s"}},{push:"crypto",reg:/^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?/,names:["id","suite","config","sessionConfig"],format:function(e){return null!=e.sessionConfig?"crypto:%d %s %s %s":"crypto:%d %s %s"}},{name:"setup",reg:/^setup:(\w*)/,format:"setup:%s"},{name:"mid",reg:/^mid:([^\s]*)/,format:"mid:%s"},{name:"msid",reg:/^msid:(.*)/,format:"msid:%s"},{name:"ptime",reg:/^ptime:(\d*)/,format:"ptime:%d"},{name:"maxptime",reg:/^maxptime:(\d*)/,format:"maxptime:%d"},{name:"direction",reg:/^(sendrecv|recvonly|sendonly|inactive)/},{name:"icelite",reg:/^(ice-lite)/},{name:"iceUfrag",reg:/^ice-ufrag:(\S*)/,format:"ice-ufrag:%s"},{name:"icePwd",reg:/^ice-pwd:(\S*)/,format:"ice-pwd:%s"},{name:"fingerprint",reg:/^fingerprint:(\S*) (\S*)/,names:["type","hash"],format:"fingerprint:%s %s"},{push:"candidates",reg:/^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?/,names:["foundation","component","transport","priority","ip","port","type","raddr","rport","tcptype","generation"],format:function(e){var r="candidate:%s %d %s %d %s %d typ %s";return r+=null!=e.raddr?" raddr %s rport %d":"%v%v",r+=null!=e.tcptype?" tcptype %s":"%v",null!=e.generation&&(r+=" generation %d"),r}},{name:"endOfCandidates",reg:/^(end-of-candidates)/},{name:"remoteCandidates",reg:/^remote-candidates:(.*)/,format:"remote-candidates:%s"},{name:"iceOptions",reg:/^ice-options:(\S*)/,format:"ice-options:%s"},{push:"ssrcs",reg:/^ssrc:(\d*) ([\w_]*):(.*)/,names:["id","attribute","value"],format:"ssrc:%d %s:%s"},{push:"ssrcGroups",reg:/^ssrc-group:(\w*) (.*)/,names:["semantics","ssrcs"],format:"ssrc-group:%s %s"},{name:"msidSemantic",reg:/^msid-semantic:\s?(\w*) (\S*)/,names:["semantic","token"],format:"msid-semantic: %s %s"},{push:"groups",reg:/^group:(\w*) (.*)/,names:["type","mids"],format:"group:%s %s"},{name:"rtcpMux",reg:/^(rtcp-mux)/},{name:"rtcpRsize",reg:/^(rtcp-rsize)/},{push:"invalid",names:["value"]}]};Object.keys(grammar).forEach(function(e){grammar[e].forEach(function(e){e.reg||(e.reg=/(.*)/),e.format||(e.format="%s")})}); -},{}],14:[function(require,module,exports){ -var parser=require("./parser"),writer=require("./writer");exports.write=writer,exports.parse=parser.parse,exports.parseFmtpConfig=parser.parseFmtpConfig,exports.parsePayloads=parser.parsePayloads,exports.parseRemoteCandidates=parser.parseRemoteCandidates; -},{"./parser":15,"./writer":16}],15:[function(require,module,exports){ -var toIntIfInt=function(t){return String(Number(t))===t?Number(t):t},attachProperties=function(t,r,e,n){if(n&&!e)r[n]=toIntIfInt(t[1]);else for(var a=0;a<e.length;a+=1)null!=t[a+1]&&(r[e[a]]=toIntIfInt(t[a+1]))},parseReg=function(t,r,e){var n=t.name&&t.names;t.push&&!r[t.push]?r[t.push]=[]:n&&!r[t.name]&&(r[t.name]={});var a=t.push?{}:n?r[t.name]:r;attachProperties(e.match(t.reg),a,t.names,t.name),t.push&&r[t.push].push(a)},grammar=require("./grammar"),validLine=RegExp.prototype.test.bind(/^([a-z])=(.*)/);exports.parse=function(t){var r={},e=[],n=r;return t.split(/(\r\n|\r|\n)/).filter(validLine).forEach(function(t){var r=t[0],a=t.slice(2);"m"===r&&(e.push({rtp:[],fmtp:[]}),n=e[e.length-1]);for(var p=0;p<(grammar[r]||[]).length;p+=1){var s=grammar[r][p];if(s.reg.test(a))return parseReg(s,n,a)}}),r.media=e,r};var fmtpReducer=function(t,r){var e=r.split("=");return 2===e.length&&(t[e[0]]=toIntIfInt(e[1])),t};exports.parseFmtpConfig=function(t){return t.split(/\;\s?/).reduce(fmtpReducer,{})},exports.parsePayloads=function(t){return t.split(" ").map(Number)},exports.parseRemoteCandidates=function(t){for(var r=[],e=t.split(" ").map(toIntIfInt),n=0;n<e.length;n+=3)r.push({component:e[n],ip:e[n+1],port:e[n+2]});return r}; -},{"./grammar":13}],16:[function(require,module,exports){ -var grammar=require("./grammar"),formatRegExp=/%[sdv%]/g,format=function(n){var r=1,e=arguments,a=e.length;return n.replace(formatRegExp,function(n){if(r>=a)return n;var u=e[r];switch(r+=1,n){case"%%":return"%";case"%s":return String(u);case"%d":return Number(u);case"%v":return""}})},makeLine=function(n,r,e){var a=r.format instanceof Function?r.format(r.push?e:e[r.name]):r.format,u=[n+"="+a];if(r.names)for(var m=0;m<r.names.length;m+=1){var t=r.names[m];r.name?u.push(e[r.name][t]):u.push(e[r.names[m]])}else u.push(e[r.name]);return format.apply(null,u)},defaultOuterOrder=["v","o","s","i","u","e","p","c","b","t","r","z","a"],defaultInnerOrder=["i","c","b","a"];module.exports=function(n,r){r=r||{},null==n.version&&(n.version=0),null==n.name&&(n.name=" "),n.media.forEach(function(n){null==n.payloads&&(n.payloads="")});var e=r.outerOrder||defaultOuterOrder,a=r.innerOrder||defaultInnerOrder,u=[];return e.forEach(function(r){grammar[r].forEach(function(e){e.name in n&&null!=n[e.name]?u.push(makeLine(r,e,n)):e.push in n&&null!=n[e.push]&&n[e.push].forEach(function(n){u.push(makeLine(r,e,n))})})}),n.media.forEach(function(n){u.push(makeLine("m",grammar.m[0],n)),a.forEach(function(r){grammar[r].forEach(function(e){e.name in n&&null!=n[e.name]?u.push(makeLine(r,e,n)):e.push in n&&null!=n[e.push]&&n[e.push].forEach(function(n){u.push(makeLine(r,e,n))})})})}),u.join("\r\n")+"\r\n"}; -},{"./grammar":13}],17:[function(require,module,exports){ -module.exports=function r(t){if(!t)return!1;if(this.length!=t.length)return!1;for(var e=0,n=this.length;e<n;e++)if(this[e]instanceof Array&&t[e]instanceof Array){if(!r.apply(this[e],[t[e]]))return!1}else if(this[e]!=t[e])return!1;return!0}; -},{}],18:[function(require,module,exports){ -exports.Interop=require("./interop"); -},{"./interop":19}],19:[function(require,module,exports){ -"use strict";function Interop(){this.cache={mlB2UMap:{},mlU2BMap:{}}}function addSetupAttr(e){void 0!==e.setup&&("active"===e.setup?e.setup="passive":"passive"===e.setup&&(e.setup="active"))}var transform=require("./transform"),arrayEquals=require("./array-equals");module.exports=Interop,Interop.prototype.candidateToUnifiedPlan=function(e){var r=new RTCIceCandidate(e);return r.sdpMLineIndex=this.cache.mlB2UMap[r.sdpMLineIndex],r},Interop.prototype.candidateToPlanB=function(e){var r=new RTCIceCandidate(e);if(0===r.sdpMid.indexOf("audio"))r.sdpMid="audio";else{if(0!==r.sdpMid.indexOf("video"))throw new Error("candidate with "+r.sdpMid+" not allowed");r.sdpMid="video"}return r.sdpMLineIndex=this.cache.mlU2BMap[r.sdpMLineIndex],r},Interop.prototype.getFirstSendingIndexFromAnswer=function(e){if(!this.cache.answer)return null;var r=transform.parse(this.cache.answer);if(r&&r.media&&Array.isArray(r.media))for(var i=0;i<r.media.length;i++)if(r.media[i].type==e&&(!r.media[i].direction||"sendrecv"===r.media[i].direction||"sendonly"===r.media[i].direction))return i;return null},Interop.prototype.toPlanB=function(e){var r=this;if("object"!=typeof e||null===e||"string"!=typeof e.sdp)return console.warn("An empty description was passed as an argument."),e;var i=transform.parse(e.sdp);if(void 0===i.media||!Array.isArray(i.media)||0===i.media.length)return console.warn("The description has no media."),e;if(i.media.length<=3&&i.media.every(function(e){return-1!==["video","audio","data"].indexOf(e.mid)}))return console.warn("This description does not look like Unified Plan."),e;for(var t=e.sdp,o=!1,n=0;n<i.media.length;n++){i.media[n].rtp.forEach(function(e){if("NULL"===e.codec){o=!0;var i=transform.parse(r.cache.offer);e.codec=i.media[n].rtp[0].codec}})}o&&(t=transform.write(i)),this.cache[e.type]=t;var s=i.media;i.media=[];var a={},d=[];s.forEach(function(e){if(("string"!=typeof e.rtcpMux||"rtcp-mux"!==e.rtcpMux)&&"inactive"!==e.direction)throw new Error("Cannot convert to Plan B because m-lines without the rtcp-mux attribute were found.");if(void 0!==a[e.type]&&"inactive"!==a[e.type].direction||(a[e.type]=e),e.protocol!=a[e.type].protocol)throw new Error("Cannot convert to Plan B because m-lines have different protocols and this library does not have support for that");if(e.payloads!=a[e.type].payloads)throw new Error("Cannot convert to Plan B because m-lines have different payloads and this library does not have support for that")}),s.forEach(function(e){if("application"===e.type)return i.media.push(e),void d.push(e.mid);"object"==typeof e.sources&&Object.keys(e.sources).forEach(function(r){"object"!=typeof a[e.type].sources&&(a[e.type].sources={}),a[e.type].sources[r]=e.sources[r],void 0!==e.msid&&(a[e.type].sources[r].msid=e.msid)}),void 0!==e.ssrcGroups&&Array.isArray(e.ssrcGroups)&&(void 0!==a[e.type].ssrcGroups&&Array.isArray(a[e.type].ssrcGroups)||(a[e.type].ssrcGroups=[]),a[e.type].ssrcGroups=a[e.type].ssrcGroups.concat(e.ssrcGroups)),a[e.type]===e&&(e.mid=e.type,delete e.bundleOnly,delete e.msid,e.type==s[0].type?(d.unshift(e.type),i.media.unshift(e)):(d.push(e.type),i.media.push(e)))}),void 0!==i.groups&&i.groups.some(function(e){if("BUNDLE"===e.type)return e.mids=d.join(" "),!0}),i.msidSemantic={semantic:"WMS",token:"*"};var c=transform.write(i);return new RTCSessionDescription({type:e.type,sdp:c})},Interop.prototype.toUnifiedPlan=function(e){var r=this;if("object"!=typeof e||null===e||"string"!=typeof e.sdp)return console.warn("An empty description was passed as an argument."),e;var i=transform.parse(e.sdp);if(void 0===i.media||!Array.isArray(i.media)||0===i.media.length)return console.warn("The description has no media."),e;if(i.media.length>3||!i.media.every(function(e){return-1!==["video","audio","data"].indexOf(e.mid)}))return console.warn("This description does not look like Plan B."),e;var t=[];i.media.forEach(function(e){t.push(e.mid)});var o=!1;if(void 0!==i.groups&&Array.isArray(i.groups)&&(o=i.groups.every(function(e){return"BUNDLE"!==e.type||arrayEquals.apply(e.mids.sort(),[t.sort()])})),!o){var n=!1;if(i.media.forEach(function(e){"inactive"!==e.direction&&(n=!0)}),n)throw new Error("Cannot convert to Unified Plan because m-lines that are not bundled were found.")}var s;if("answer"===e.type)s="offer";else{if("offer"!==e.type)throw new Error("Type '"+e.type+"' not supported.");s="answer"}var a;void 0!==this.cache[s]&&(a=transform.parse(this.cache[s]));var d,c,p,u,f={audio:{},video:{}},m={},y=0,l=0,v={},h={},w={},g={};if(i.media.forEach(function(i){if(("string"!=typeof i.rtcpMux||"rtcp-mux"!==i.rtcpMux)&&"inactive"!==i.direction)throw new Error("Cannot convert to Unified Plan because m-lines without the rtcp-mux attribute were found.");if("application"===i.type)return void(m[i.mid]=i);var t=i.sources,o=i.ssrcGroups,n=i.port;if(void 0!==i.candidates&&(d=void 0!==d?d.concat(i.candidates):i.candidates),void 0!==c&&void 0!==i.iceUfrag&&c!=i.iceUfrag)throw new Error("Only BUNDLE supported, iceUfrag must be the same for all m-lines.\n\tLast iceUfrag: "+c+"\n\tNew iceUfrag: "+i.iceUfrag);if(void 0!==i.iceUfrag&&(c=i.iceUfrag),void 0!==p&&void 0!==i.icePwd&&p!=i.icePwd)throw new Error("Only BUNDLE supported, icePwd must be the same for all m-lines.\n\tLast icePwd: "+p+"\n\tNew icePwd: "+i.icePwd);if(void 0!==i.icePwd&&(p=i.icePwd),void 0!==u&&void 0!==i.fingerprint&&(u.type!=i.fingerprint.type||u.hash!=i.fingerprint.hash))throw new Error("Only BUNDLE supported, fingerprint must be the same for all m-lines.\n\tLast fingerprint: "+JSON.stringify(u)+"\n\tNew fingerprint: "+JSON.stringify(i.fingerprint));void 0!==i.fingerprint&&(u=i.fingerprint),h[i.type]=i.payloads,w[i.type]=i.rtcpFb,g[i.type]=i.rtp;var s={};void 0!==o&&Array.isArray(o)&&o.forEach(function(e){void 0!==e.ssrcs&&Array.isArray(e.ssrcs)&&e.ssrcs.forEach(function(r){void 0===s[r]&&(s[r]=[]),s[r].push(e)})});var E={};if("object"==typeof t)delete i.sources,delete i.ssrcGroups,delete i.candidates,delete i.iceUfrag,delete i.icePwd,delete i.fingerprint,delete i.port,delete i.mid,Object.keys(t).forEach(function(o){var h;if("offer"===e.type&&!t[o].msid)return void(f[i.type][o]=t[o]);void 0!==s[o]&&Array.isArray(s[o])&&s[o].some(function(e){return e.ssrcs.some(function(e){if("object"==typeof E[e])return h=E[e],!0})}),"object"==typeof h?(h.sources[o]=t[o],delete t[o].msid):(h=Object.create(i),E[o]=h,void 0!==t[o].msid&&(h.msid=t[o].msid,delete t[o].msid),h.sources={},h.sources[o]=t[o],h.ssrcGroups=s[o],void 0!==a&&void 0!==a.media&&Array.isArray(a.media)&&a.media.forEach(function(e){"object"==typeof e.sources&&Object.keys(e.sources).forEach(function(r){r===o&&(h.mid=e.mid)})}),void 0===h.mid&&(h.mid=[i.type,"-",o].join("")),h.candidates=d,h.iceUfrag=c,h.icePwd=p,h.fingerprint=u,h.port=n,m[h.mid]=h,v[l]=h.sources,r.cache.mlU2BMap[l]=y,void 0===r.cache.mlB2UMap[y]&&(r.cache.mlB2UMap[y]=l),l++)});else{var U=i;U.candidates=d,U.iceUfrag=c,U.icePwd=p,U.fingerprint=u,U.port=n,m[U.mid]=U,r.cache.mlU2BMap[l]=y,void 0===r.cache.mlB2UMap[y]&&(r.cache.mlB2UMap[y]=l)}y++}),i.media=[],t=[],"answer"===e.type)for(var E=0;E<a.media.length;E++){var U=a.media[E];delete U.msid,delete U.sources,delete U.ssrcGroups,void 0===v[E]?U.direction&&"sendrecv"!==U.direction?"sendonly"===U.direction&&(U.direction="inactive"):U.direction="recvonly":U.direction&&"sendrecv"!==U.direction?"recvonly"===U.direction&&(U.direction="sendonly"):U.direction="sendrecv",U.sources=v[E],U.candidates=d,U.iceUfrag=c,U.icePwd=p,U.fingerprint=u,U.rtp=g[U.type],U.payloads=h[U.type],U.rtcpFb=w[U.type],i.media.push(U),"string"==typeof U.mid&&t.push(U.mid)}else void 0!==a&&void 0!==a.media&&Array.isArray(a.media)&&a.media.forEach(function(e){t.push(e.mid),void 0!==m[e.mid]?i.media.push(m[e.mid]):(delete e.msid,delete e.sources,delete e.ssrcGroups,e.direction&&"sendrecv"!==e.direction||(e.direction="sendonly"),e.direction&&"recvonly"!==e.direction||(e.direction="inactive"),addSetupAttr(e),i.media.push(e))}),Object.keys(m).forEach(function(e){if(-1===t.indexOf(e))if(t.push(e),"recvonly"===m[e].direction){var r=!1;i.media.some(function(i){if(("sendrecv"===i.direction||"sendonly"===i.direction)&&i.type===m[e].type)return Object.keys(m[e].sources).forEach(function(r){i.sources[r]=m[e].sources[r]}),r=!0,!0}),r||i.media.push(m[e])}else i.media.push(m[e])});["audio","video"].forEach(function(e){if(i&&i.media&&Array.isArray(i.media)){var t=null;if(Object.keys(f[e]).length>0&&null===(t=r.getFirstSendingIndexFromAnswer(e)))for(var o=0;o<i.media.length;o++)if(i.media[o].type===e){t=o;break}if(t&&i.media.length>t){var n=i.media[t];Object.keys(f[e]).forEach(function(r){n.sources&&n.sources[r]&&console.warn("Replacing an existing SSRC."),n.sources||(n.sources={}),n.sources[r]=f[e][r]})}}}),void 0!==i.groups&&i.groups.some(function(e){if("BUNDLE"===e.type)return e.mids=t.join(" "),!0}),i.msidSemantic={semantic:"WMS",token:"*"};var b=transform.write(i);return this.cache[e.type]=b,new RTCSessionDescription({type:e.type,sdp:b})}; -},{"./array-equals":17,"./transform":20}],20:[function(require,module,exports){ -var transform=require("sdp-transform");exports.write=function(s,r){return void 0!==s&&void 0!==s.media&&Array.isArray(s.media)&&s.media.forEach(function(s){void 0!==s.sources&&0!==Object.keys(s.sources).length&&(s.ssrcs=[],Object.keys(s.sources).forEach(function(r){var o=s.sources[r];Object.keys(o).forEach(function(i){s.ssrcs.push({id:r,attribute:i,value:o[i]})})}),delete s.sources),void 0!==s.ssrcGroups&&Array.isArray(s.ssrcGroups)&&s.ssrcGroups.forEach(function(s){void 0!==s.ssrcs&&Array.isArray(s.ssrcs)&&(s.ssrcs=s.ssrcs.join(" "))})}),void 0!==s&&void 0!==s.groups&&Array.isArray(s.groups)&&s.groups.forEach(function(s){void 0!==s.mids&&Array.isArray(s.mids)&&(s.mids=s.mids.join(" "))}),transform.write(s,r)},exports.parse=function(s){var r=transform.parse(s);return void 0!==r&&void 0!==r.media&&Array.isArray(r.media)&&r.media.forEach(function(s){void 0!==s.ssrcs&&Array.isArray(s.ssrcs)&&(s.sources={},s.ssrcs.forEach(function(r){s.sources[r.id]||(s.sources[r.id]={}),s.sources[r.id][r.attribute]=r.value}),delete s.ssrcs),void 0!==s.ssrcGroups&&Array.isArray(s.ssrcGroups)&&s.ssrcGroups.forEach(function(s){"string"==typeof s.ssrcs&&(s.ssrcs=s.ssrcs.split(" "))})}),void 0!==r&&void 0!==r.groups&&Array.isArray(r.groups)&&r.groups.forEach(function(s){"string"==typeof s.mids&&(s.mids=s.mids.split(" "))}),r}; -},{"sdp-transform":14}],21:[function(require,module,exports){ -!function(i,e){"use strict";var s="model",o="name",r="type",n="vendor",a="version",t="mobile",w="tablet",d={extend:function(i,e){var s={};for(var o in i)e[o]&&e[o].length%2==0?s[o]=e[o].concat(i[o]):s[o]=i[o];return s},has:function(i,e){return"string"==typeof i&&-1!==e.toLowerCase().indexOf(i.toLowerCase())},lowerize:function(i){return i.toLowerCase()},major:function(i){return"string"==typeof i?i.replace(/[^\d\.]/g,"").split(".")[0]:void 0},trim:function(i){return i.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},l={rgx:function(){for(var i,e,s,o,r,n,a,t=0,w=arguments;t<w.length&&!n;){var d=w[t],l=w[t+1];if(void 0===i){i={};for(o in l)l.hasOwnProperty(o)&&(r=l[o],"object"==typeof r?i[r[0]]=void 0:i[r]=void 0)}for(e=s=0;e<d.length&&!n;)if(n=d[e++].exec(this.getUA()))for(o=0;o<l.length;o++)a=n[++s],r=l[o],"object"==typeof r&&r.length>0?2==r.length?"function"==typeof r[1]?i[r[0]]=r[1].call(this,a):i[r[0]]=r[1]:3==r.length?"function"!=typeof r[1]||r[1].exec&&r[1].test?i[r[0]]=a?a.replace(r[1],r[2]):void 0:i[r[0]]=a?r[1].call(this,a,r[2]):void 0:4==r.length&&(i[r[0]]=a?r[3].call(this,a.replace(r[1],r[2])):void 0):i[r]=a||void 0;t+=2}return i},str:function(i,e){for(var s in e)if("object"==typeof e[s]&&e[s].length>0){for(var o=0;o<e[s].length;o++)if(d.has(e[s][o],i))return"?"===s?void 0:s}else if(d.has(e[s],i))return"?"===s?void 0:s;return i}},c={browser:{oldsafari:{version:{"1.0":"/8",1.2:"/1",1.3:"/3","2.0":"/412","2.0.2":"/416","2.0.3":"/417","2.0.4":"/419","?":"/"}}},device:{amazon:{model:{"Fire Phone":["SD","KF"]}},sprint:{model:{"Evo Shift 4G":"7373KT"},vendor:{HTC:"APA",Sprint:"Sprint"}}},os:{windows:{version:{ME:"4.90","NT 3.11":"NT3.51","NT 4.0":"NT4.0",2000:"NT 5.0",XP:["NT 5.1","NT 5.2"],Vista:"NT 6.0",7:"NT 6.1",8:"NT 6.2",8.1:"NT 6.3",10:["NT 6.4","NT 10.0"],RT:"ARM"}}}},u={browser:[[/(opera\smini)\/([\w\.-]+)/i,/(opera\s[mobiletab]+).+version\/([\w\.-]+)/i,/(opera).+version\/([\w\.]+)/i,/(opera)[\/\s]+([\w\.]+)/i],[o,a],[/(opios)[\/\s]+([\w\.]+)/i],[[o,"Opera Mini"],a],[/\s(opr)\/([\w\.]+)/i],[[o,"Opera"],a],[/(kindle)\/([\w\.]+)/i,/(lunascape|maxthon|netfront|jasmine|blazer)[\/\s]?([\w\.]+)*/i,/(avant\s|iemobile|slim|baidu)(?:browser)?[\/\s]?([\w\.]*)/i,/(?:ms|\()(ie)\s([\w\.]+)/i,/(rekonq)\/([\w\.]+)*/i,/(chromium|flock|rockmelt|midori|epiphany|silk|skyfire|ovibrowser|bolt|iron|vivaldi|iridium|phantomjs)\/([\w\.-]+)/i],[o,a],[/(trident).+rv[:\s]([\w\.]+).+like\sgecko/i],[[o,"IE"],a],[/(edge)\/((\d+)?[\w\.]+)/i],[o,a],[/(yabrowser)\/([\w\.]+)/i],[[o,"Yandex"],a],[/(comodo_dragon)\/([\w\.]+)/i],[[o,/_/g," "],a],[/(micromessenger)\/([\w\.]+)/i],[[o,"WeChat"],a],[/xiaomi\/miuibrowser\/([\w\.]+)/i],[a,[o,"MIUI Browser"]],[/\swv\).+(chrome)\/([\w\.]+)/i],[[o,/(.+)/,"$1 WebView"],a],[/android.+samsungbrowser\/([\w\.]+)/i,/android.+version\/([\w\.]+)\s+(?:mobile\s?safari|safari)*/i],[a,[o,"Android Browser"]],[/(chrome|omniweb|arora|[tizenoka]{5}\s?browser)\/v?([\w\.]+)/i,/(qqbrowser)[\/\s]?([\w\.]+)/i],[o,a],[/(uc\s?browser)[\/\s]?([\w\.]+)/i,/ucweb.+(ucbrowser)[\/\s]?([\w\.]+)/i,/juc.+(ucweb)[\/\s]?([\w\.]+)/i],[[o,"UCBrowser"],a],[/(dolfin)\/([\w\.]+)/i],[[o,"Dolphin"],a],[/((?:android.+)crmo|crios)\/([\w\.]+)/i],[[o,"Chrome"],a],[/;fbav\/([\w\.]+);/i],[a,[o,"Facebook"]],[/fxios\/([\w\.-]+)/i],[a,[o,"Firefox"]],[/version\/([\w\.]+).+?mobile\/\w+\s(safari)/i],[a,[o,"Mobile Safari"]],[/version\/([\w\.]+).+?(mobile\s?safari|safari)/i],[a,o],[/webkit.+?(mobile\s?safari|safari)(\/[\w\.]+)/i],[o,[a,l.str,c.browser.oldsafari.version]],[/(konqueror)\/([\w\.]+)/i,/(webkit|khtml)\/([\w\.]+)/i],[o,a],[/(navigator|netscape)\/([\w\.-]+)/i],[[o,"Netscape"],a],[/(swiftfox)/i,/(icedragon|iceweasel|camino|chimera|fennec|maemo\sbrowser|minimo|conkeror)[\/\s]?([\w\.\+]+)/i,/(firefox|seamonkey|k-meleon|icecat|iceape|firebird|phoenix)\/([\w\.-]+)/i,/(mozilla)\/([\w\.]+).+rv\:.+gecko\/\d+/i,/(polaris|lynx|dillo|icab|doris|amaya|w3m|netsurf|sleipnir)[\/\s]?([\w\.]+)/i,/(links)\s\(([\w\.]+)/i,/(gobrowser)\/?([\w\.]+)*/i,/(ice\s?browser)\/v?([\w\._]+)/i,/(mosaic)[\/\s]([\w\.]+)/i],[o,a]],cpu:[[/(?:(amd|x(?:(?:86|64)[_-])?|wow|win)64)[;\)]/i],[["architecture","amd64"]],[/(ia32(?=;))/i],[["architecture",d.lowerize]],[/((?:i[346]|x)86)[;\)]/i],[["architecture","ia32"]],[/windows\s(ce|mobile);\sppc;/i],[["architecture","arm"]],[/((?:ppc|powerpc)(?:64)?)(?:\smac|;|\))/i],[["architecture",/ower/,"",d.lowerize]],[/(sun4\w)[;\)]/i],[["architecture","sparc"]],[/((?:avr32|ia64(?=;))|68k(?=\))|arm(?:64|(?=v\d+;))|(?=atmel\s)avr|(?:irix|mips|sparc)(?:64)?(?=;)|pa-risc)/i],[["architecture",d.lowerize]]],device:[[/\((ipad|playbook);[\w\s\);-]+(rim|apple)/i],[s,n,[r,w]],[/applecoremedia\/[\w\.]+ \((ipad)/],[s,[n,"Apple"],[r,w]],[/(apple\s{0,1}tv)/i],[[s,"Apple TV"],[n,"Apple"]],[/(archos)\s(gamepad2?)/i,/(hp).+(touchpad)/i,/(hp).+(tablet)/i,/(kindle)\/([\w\.]+)/i,/\s(nook)[\w\s]+build\/(\w+)/i,/(dell)\s(strea[kpr\s\d]*[\dko])/i],[n,s,[r,w]],[/(kf[A-z]+)\sbuild\/[\w\.]+.*silk\//i],[s,[n,"Amazon"],[r,w]],[/(sd|kf)[0349hijorstuw]+\sbuild\/[\w\.]+.*silk\//i],[[s,l.str,c.device.amazon.model],[n,"Amazon"],[r,t]],[/\((ip[honed|\s\w*]+);.+(apple)/i],[s,n,[r,t]],[/\((ip[honed|\s\w*]+);/i],[s,[n,"Apple"],[r,t]],[/(blackberry)[\s-]?(\w+)/i,/(blackberry|benq|palm(?=\-)|sonyericsson|acer|asus|dell|huawei|meizu|motorola|polytron)[\s_-]?([\w-]+)*/i,/(hp)\s([\w\s]+\w)/i,/(asus)-?(\w+)/i],[n,s,[r,t]],[/\(bb10;\s(\w+)/i],[s,[n,"BlackBerry"],[r,t]],[/android.+(transfo[prime\s]{4,10}\s\w+|eeepc|slider\s\w+|nexus 7|padfone)/i],[s,[n,"Asus"],[r,w]],[/(sony)\s(tablet\s[ps])\sbuild\//i,/(sony)?(?:sgp.+)\sbuild\//i],[[n,"Sony"],[s,"Xperia Tablet"],[r,w]],[/(?:sony)?(?:(?:(?:c|d)\d{4})|(?:so[-l].+))\sbuild\//i],[[n,"Sony"],[s,"Xperia Phone"],[r,t]],[/\s(ouya)\s/i,/(nintendo)\s([wids3u]+)/i],[n,s,[r,"console"]],[/android.+;\s(shield)\sbuild/i],[s,[n,"Nvidia"],[r,"console"]],[/(playstation\s[34portablevi]+)/i],[s,[n,"Sony"],[r,"console"]],[/(sprint\s(\w+))/i],[[n,l.str,c.device.sprint.vendor],[s,l.str,c.device.sprint.model],[r,t]],[/(lenovo)\s?(S(?:5000|6000)+(?:[-][\w+]))/i],[n,s,[r,w]],[/(htc)[;_\s-]+([\w\s]+(?=\))|\w+)*/i,/(zte)-(\w+)*/i,/(alcatel|geeksphone|huawei|lenovo|nexian|panasonic|(?=;\s)sony)[_\s-]?([\w-]+)*/i],[n,[s,/_/g," "],[r,t]],[/(nexus\s9)/i],[s,[n,"HTC"],[r,w]],[/(nexus\s6p)/i],[s,[n,"Huawei"],[r,t]],[/(microsoft);\s(lumia[\s\w]+)/i],[n,s,[r,t]],[/[\s\(;](xbox(?:\sone)?)[\s\);]/i],[s,[n,"Microsoft"],[r,"console"]],[/(kin\.[onetw]{3})/i],[[s,/\./g," "],[n,"Microsoft"],[r,t]],[/\s(milestone|droid(?:[2-4x]|\s(?:bionic|x2|pro|razr))?(:?\s4g)?)[\w\s]+build\//i,/mot[\s-]?(\w+)*/i,/(XT\d{3,4}) build\//i,/(nexus\s6)/i],[s,[n,"Motorola"],[r,t]],[/android.+\s(mz60\d|xoom[\s2]{0,2})\sbuild\//i],[s,[n,"Motorola"],[r,w]],[/hbbtv\/\d+\.\d+\.\d+\s+\([\w\s]*;\s*(\w[^;]*);([^;]*)/i],[[n,d.trim],[s,d.trim],[r,"smarttv"]],[/hbbtv.+maple;(\d+)/i],[[s,/^/,"SmartTV"],[n,"Samsung"],[r,"smarttv"]],[/\(dtv[\);].+(aquos)/i],[s,[n,"Sharp"],[r,"smarttv"]],[/android.+((sch-i[89]0\d|shw-m380s|gt-p\d{4}|gt-n\d+|sgh-t8[56]9|nexus 10))/i,/((SM-T\w+))/i],[[n,"Samsung"],s,[r,w]],[/smart-tv.+(samsung)/i],[n,[r,"smarttv"],s],[/((s[cgp]h-\w+|gt-\w+|galaxy\snexus|sm-\w[\w\d]+))/i,/(sam[sung]*)[\s-]*(\w+-?[\w-]*)*/i,/sec-((sgh\w+))/i],[[n,"Samsung"],s,[r,t]],[/sie-(\w+)*/i],[s,[n,"Siemens"],[r,t]],[/(maemo|nokia).*(n900|lumia\s\d+)/i,/(nokia)[\s_-]?([\w-]+)*/i],[[n,"Nokia"],s,[r,t]],[/android\s3\.[\s\w;-]{10}(a\d{3})/i],[s,[n,"Acer"],[r,w]],[/android\s3\.[\s\w;-]{10}(lg?)-([06cv9]{3,4})/i],[[n,"LG"],s,[r,w]],[/(lg) netcast\.tv/i],[n,s,[r,"smarttv"]],[/(nexus\s[45])/i,/lg[e;\s\/-]+(\w+)*/i],[s,[n,"LG"],[r,t]],[/android.+(ideatab[a-z0-9\-\s]+)/i],[s,[n,"Lenovo"],[r,w]],[/linux;.+((jolla));/i],[n,s,[r,t]],[/((pebble))app\/[\d\.]+\s/i],[n,s,[r,"wearable"]],[/android.+;\s(glass)\s\d/i],[s,[n,"Google"],[r,"wearable"]],[/android.+(\w+)\s+build\/hm\1/i,/android.+(hm[\s\-_]*note?[\s_]*(?:\d\w)?)\s+build/i,/android.+(mi[\s\-_]*(?:one|one[\s_]plus|note lte)?[\s_]*(?:\d\w)?)\s+build/i],[[s,/_/g," "],[n,"Xiaomi"],[r,t]],[/android.+a000(1)\s+build/i],[s,[n,"OnePlus"],[r,t]],[/\s(tablet)[;\/]/i,/\s(mobile)(?:[;\/]|\ssafari)/i],[[r,d.lowerize],n,s]],engine:[[/windows.+\sedge\/([\w\.]+)/i],[a,[o,"EdgeHTML"]],[/(presto)\/([\w\.]+)/i,/(webkit|trident|netfront|netsurf|amaya|lynx|w3m)\/([\w\.]+)/i,/(khtml|tasman|links)[\/\s]\(?([\w\.]+)/i,/(icab)[\/\s]([23]\.[\d\.]+)/i],[o,a],[/rv\:([\w\.]+).*(gecko)/i],[a,o]],os:[[/microsoft\s(windows)\s(vista|xp)/i],[o,a],[/(windows)\snt\s6\.2;\s(arm)/i,/(windows\sphone(?:\sos)*)[\s\/]?([\d\.\s]+\w)*/i,/(windows\smobile|windows)[\s\/]?([ntce\d\.\s]+\w)/i],[o,[a,l.str,c.os.windows.version]],[/(win(?=3|9|n)|win\s9x\s)([nt\d\.]+)/i],[[o,"Windows"],[a,l.str,c.os.windows.version]],[/\((bb)(10);/i],[[o,"BlackBerry"],a],[/(blackberry)\w*\/?([\w\.]+)*/i,/(tizen)[\/\s]([\w\.]+)/i,/(android|webos|palm\sos|qnx|bada|rim\stablet\sos|meego|contiki)[\/\s-]?([\w\.]+)*/i,/linux;.+(sailfish);/i],[o,a],[/(symbian\s?os|symbos|s60(?=;))[\/\s-]?([\w\.]+)*/i],[[o,"Symbian"],a],[/\((series40);/i],[o],[/mozilla.+\(mobile;.+gecko.+firefox/i],[[o,"Firefox OS"],a],[/(nintendo|playstation)\s([wids34portablevu]+)/i,/(mint)[\/\s\(]?(\w+)*/i,/(mageia|vectorlinux)[;\s]/i,/(joli|[kxln]?ubuntu|debian|[open]*suse|gentoo|(?=\s)arch|slackware|fedora|mandriva|centos|pclinuxos|redhat|zenwalk|linpus)[\/\s-]?(?!chrom)([\w\.-]+)*/i,/(hurd|linux)\s?([\w\.]+)*/i,/(gnu)\s?([\w\.]+)*/i],[o,a],[/(cros)\s[\w]+\s([\w\.]+\w)/i],[[o,"Chromium OS"],a],[/(sunos)\s?([\w\.]+\d)*/i],[[o,"Solaris"],a],[/\s([frentopc-]{0,4}bsd|dragonfly)\s?([\w\.]+)*/i],[o,a],[/(haiku)\s(\w+)/i],[o,a],[/(ip[honead]+)(?:.*os\s([\w]+)*\slike\smac|;\sopera)/i],[[o,"iOS"],[a,/_/g,"."]],[/(mac\sos\sx)\s?([\w\s\.]+\w)*/i,/(macintosh|mac(?=_powerpc)\s)/i],[[o,"Mac OS"],[a,/_/g,"."]],[/((?:open)?solaris)[\/\s-]?([\w\.]+)*/i,/(aix)\s((\d)(?=\.|\)|\s)[\w\.]*)*/i,/(plan\s9|minix|beos|os\/2|amigaos|morphos|risc\sos|openvms)/i,/(unix)\s?([\w\.]+)*/i],[o,a]]},p=function(e,s){if(!(this instanceof p))return new p(e,s).getResult();var o=e||(i&&i.navigator&&i.navigator.userAgent?i.navigator.userAgent:""),r=s?d.extend(u,s):u;return this.getBrowser=function(){var i=l.rgx.apply(this,r.browser);return i.major=d.major(i.version),i},this.getCPU=function(){return l.rgx.apply(this,r.cpu)},this.getDevice=function(){return l.rgx.apply(this,r.device)},this.getEngine=function(){return l.rgx.apply(this,r.engine)},this.getOS=function(){return l.rgx.apply(this,r.os)},this.getResult=function(){return{ua:this.getUA(),browser:this.getBrowser(),engine:this.getEngine(),os:this.getOS(),device:this.getDevice(),cpu:this.getCPU()}},this.getUA=function(){return o},this.setUA=function(i){return o=i,this},this};p.VERSION="0.7.12",p.BROWSER={NAME:o,MAJOR:"major",VERSION:a},p.CPU={ARCHITECTURE:"architecture"},p.DEVICE={MODEL:s,VENDOR:n,TYPE:r,CONSOLE:"console",MOBILE:t,SMARTTV:"smarttv",TABLET:w,WEARABLE:"wearable",EMBEDDED:"embedded"},p.ENGINE={NAME:o,VERSION:a},p.OS={NAME:o,VERSION:a},"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=p),exports.UAParser=p):"function"==typeof define&&define.amd?define(function(){return p}):i.UAParser=p;var m=i.jQuery||i.Zepto;if(void 0!==m){var b=new p;m.ua=b.getResult(),m.ua.get=function(){return b.getUA()},m.ua.set=function(i){b.setUA(i);var e=b.getResult();for(var s in e)m.ua[s]=e[s]}}}("object"==typeof window?window:this); -},{}],22:[function(require,module,exports){ -(function (global){ -var rng,crypto=global.crypto||global.msCrypto;if(crypto&&crypto.getRandomValues){var _rnds8=new Uint8Array(16);rng=function(){return crypto.getRandomValues(_rnds8),_rnds8}}if(!rng){var _rnds=new Array(16);rng=function(){for(var r,n=0;n<16;n++)0==(3&n)&&(r=4294967296*Math.random()),_rnds[n]=r>>>((3&n)<<3)&255;return _rnds}}module.exports=rng; -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - -},{}],23:[function(require,module,exports){ -function parse(e,s,r){var t=s&&r||0,n=0;for(s=s||[],e.toLowerCase().replace(/[0-9a-f]{2}/g,function(e){n<16&&(s[t+n++]=_hexToByte[e])});n<16;)s[t+n++]=0;return s}function unparse(e,s){var r=s||0,t=_byteToHex;return t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+"-"+t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]+t[e[r++]]}function v1(e,s,r){var t=s&&r||0,n=s||[];e=e||{};var o=void 0!==e.clockseq?e.clockseq:_clockseq,a=void 0!==e.msecs?e.msecs:(new Date).getTime(),u=void 0!==e.nsecs?e.nsecs:_lastNSecs+1,c=a-_lastMSecs+(u-_lastNSecs)/1e4;if(c<0&&void 0===e.clockseq&&(o=o+1&16383),(c<0||a>_lastMSecs)&&void 0===e.nsecs&&(u=0),u>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");_lastMSecs=a,_lastNSecs=u,_clockseq=o,a+=122192928e5;var i=(1e4*(268435455&a)+u)%4294967296;n[t++]=i>>>24&255,n[t++]=i>>>16&255,n[t++]=i>>>8&255,n[t++]=255&i;var _=a/4294967296*1e4&268435455;n[t++]=_>>>8&255,n[t++]=255&_,n[t++]=_>>>24&15|16,n[t++]=_>>>16&255,n[t++]=o>>>8|128,n[t++]=255&o;for(var d=e.node||_nodeId,v=0;v<6;v++)n[t+v]=d[v];return s||unparse(n)}function v4(e,s,r){var t=s&&r||0;"string"==typeof e&&(s="binary"==e?new Array(16):null,e=null),e=e||{};var n=e.random||(e.rng||_rng)();if(n[6]=15&n[6]|64,n[8]=63&n[8]|128,s)for(var o=0;o<16;o++)s[t+o]=n[o];return s||unparse(n)}for(var _rng=require("./rng"),_byteToHex=[],_hexToByte={},i=0;i<256;i++)_byteToHex[i]=(i+256).toString(16).substr(1),_hexToByte[_byteToHex[i]]=i;var _seedBytes=_rng(),_nodeId=[1|_seedBytes[0],_seedBytes[1],_seedBytes[2],_seedBytes[3],_seedBytes[4],_seedBytes[5]],_clockseq=16383&(_seedBytes[6]<<8|_seedBytes[7]),_lastMSecs=0,_lastNSecs=0,uuid=v4;uuid.v1=v1,uuid.v4=v4,uuid.parse=parse,uuid.unparse=unparse,module.exports=uuid; -},{"./rng":22}],24:[function(require,module,exports){ -function WildEmitter(){}module.exports=WildEmitter,WildEmitter.mixin=function(t){var l=t.prototype||t;l.isWildEmitter=!0,l.on=function(t,l,i){this.callbacks=this.callbacks||{};var s=3===arguments.length,c=s?arguments[1]:void 0,a=s?arguments[2]:arguments[1];return a._groupName=c,(this.callbacks[t]=this.callbacks[t]||[]).push(a),this},l.once=function(t,l,i){function s(){c.off(t,s),h.apply(this,arguments)}var c=this,a=3===arguments.length,e=a?arguments[1]:void 0,h=a?arguments[2]:arguments[1];return this.on(t,e,s),this},l.releaseGroup=function(t){this.callbacks=this.callbacks||{};var l,i,s,c;for(l in this.callbacks)for(c=this.callbacks[l],i=0,s=c.length;i<s;i++)c[i]._groupName===t&&(c.splice(i,1),i--,s--);return this},l.off=function(t,l){this.callbacks=this.callbacks||{};var i,s=this.callbacks[t];return s?1===arguments.length?(delete this.callbacks[t],this):(i=s.indexOf(l),s.splice(i,1),0===s.length&&delete this.callbacks[t],this):this},l.emit=function(t){this.callbacks=this.callbacks||{};var l,i,s,c=[].slice.call(arguments,1),a=this.callbacks[t],e=this.getWildcardCallbacks(t);if(a)for(s=a.slice(),l=0,i=s.length;l<i&&s[l];++l)s[l].apply(this,c);if(e)for(i=e.length,s=e.slice(),l=0,i=s.length;l<i&&s[l];++l)s[l].apply(this,[t].concat(c));return this},l.getWildcardCallbacks=function(t){this.callbacks=this.callbacks||{};var l,i,s=[];for(l in this.callbacks)i=l.split("*"),("*"===l||2===i.length&&t.slice(0,i[0].length)===i[0])&&(s=s.concat(this.callbacks[l]));return s}},WildEmitter.mixin(WildEmitter); -},{}]},{},[2])(2) -}); - - -//# sourceMappingURL=kurento-utils.map \ No newline at end of file diff --git a/bigbluebutton-client/resources/prod/lib/reconnecting-websocket.min.js b/bigbluebutton-client/resources/prod/lib/reconnecting-websocket.min.js new file mode 100644 index 0000000000000000000000000000000000000000..3015099ac17bb3ec057e545e21a4db524265d74f --- /dev/null +++ b/bigbluebutton-client/resources/prod/lib/reconnecting-websocket.min.js @@ -0,0 +1 @@ +!function(a,b){"function"==typeof define&&define.amd?define([],b):"undefined"!=typeof module&&module.exports?module.exports=b():a.ReconnectingWebSocket=b()}(this,function(){function a(b,c,d){function l(a,b){var c=document.createEvent("CustomEvent");return c.initCustomEvent(a,!1,!1,b),c}var e={debug:!1,automaticOpen:!0,reconnectInterval:1e3,maxReconnectInterval:3e4,reconnectDecay:1.5,timeoutInterval:2e3};d||(d={});for(var f in e)this[f]="undefined"!=typeof d[f]?d[f]:e[f];this.url=b,this.reconnectAttempts=0,this.readyState=WebSocket.CONNECTING,this.protocol=null;var h,g=this,i=!1,j=!1,k=document.createElement("div");k.addEventListener("open",function(a){g.onopen(a)}),k.addEventListener("close",function(a){g.onclose(a)}),k.addEventListener("connecting",function(a){g.onconnecting(a)}),k.addEventListener("message",function(a){g.onmessage(a)}),k.addEventListener("error",function(a){g.onerror(a)}),this.addEventListener=k.addEventListener.bind(k),this.removeEventListener=k.removeEventListener.bind(k),this.dispatchEvent=k.dispatchEvent.bind(k),this.open=function(b){h=new WebSocket(g.url,c||[]),b||k.dispatchEvent(l("connecting")),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","attempt-connect",g.url);var d=h,e=setTimeout(function(){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","connection-timeout",g.url),j=!0,d.close(),j=!1},g.timeoutInterval);h.onopen=function(){clearTimeout(e),(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onopen",g.url),g.protocol=h.protocol,g.readyState=WebSocket.OPEN,g.reconnectAttempts=0;var d=l("open");d.isReconnect=b,b=!1,k.dispatchEvent(d)},h.onclose=function(c){if(clearTimeout(e),h=null,i)g.readyState=WebSocket.CLOSED,k.dispatchEvent(l("close"));else{g.readyState=WebSocket.CONNECTING;var d=l("connecting");d.code=c.code,d.reason=c.reason,d.wasClean=c.wasClean,k.dispatchEvent(d),b||j||((g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onclose",g.url),k.dispatchEvent(l("close")));var e=g.reconnectInterval*Math.pow(g.reconnectDecay,g.reconnectAttempts);setTimeout(function(){g.reconnectAttempts++,g.open(!0)},e>g.maxReconnectInterval?g.maxReconnectInterval:e)}},h.onmessage=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onmessage",g.url,b.data);var c=l("message");c.data=b.data,k.dispatchEvent(c)},h.onerror=function(b){(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","onerror",g.url,b),k.dispatchEvent(l("error"))}},1==this.automaticOpen&&this.open(!1),this.send=function(b){if(h)return(g.debug||a.debugAll)&&console.debug("ReconnectingWebSocket","send",g.url,b),h.send(b);throw"INVALID_STATE_ERR : Pausing to reconnect websocket"},this.close=function(a,b){"undefined"==typeof a&&(a=1e3),i=!0,h&&h.close(a,b)},this.refresh=function(){h&&h.close()}}return a.prototype.onopen=function(){},a.prototype.onclose=function(){},a.prototype.onconnecting=function(){},a.prototype.onmessage=function(){},a.prototype.onerror=function(){},a.debugAll=!1,a.CONNECTING=WebSocket.CONNECTING,a.OPEN=WebSocket.OPEN,a.CLOSING=WebSocket.CLOSING,a.CLOSED=WebSocket.CLOSED,a}); diff --git a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as index 1100f5c2ca28630f96da3e04c27b1259e39a16de..e22cb1a50273812f9b238b00b3a4ecf8c9f31914 100755 --- a/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as +++ b/bigbluebutton-client/src/org/bigbluebutton/modules/users/services/MessageReceiver.as @@ -487,7 +487,7 @@ package org.bigbluebutton.modules.users.services logData.tags = ["users"]; logData.status = "user_ejected"; logData.message = "User ejected from meeting."; - LOGGER.info(JSON.stringify(logData)); + LOGGER.debug(JSON.stringify(logData)); } private function handleUserLocked(msg:Object):void { @@ -698,21 +698,24 @@ package org.bigbluebutton.modules.users.services private function handleUserBroadcastCamStartedEvtMsg(msg:Object):void { var userId: String = msg.body.userId as String; var streamId: String = msg.body.stream as String; + var isHtml5Client: Boolean = msg.body.isHtml5Client as Boolean; var logData:Object = UsersUtil.initLogData(); logData.tags = ["webcam"]; logData.message = "UserBroadcastCamStartedEvtMsg server message"; logData.user.webcamStream = streamId; + logData.user.isHtml5Client = isHtml5Client; LOGGER.info(JSON.stringify(logData)); - - var mediaStream: MediaStream = new MediaStream(streamId, userId) - LiveMeeting.inst().webcams.add(mediaStream); - - var webUser: User2x = UsersUtil.getUser(userId); - if (webUser != null) { - sendStreamStartedEvent(userId, webUser.name, streamId); + + if (!isHtml5Client) { + var mediaStream: MediaStream = new MediaStream(streamId, userId) + LiveMeeting.inst().webcams.add(mediaStream); + + var webUser: User2x = UsersUtil.getUser(userId); + if (webUser != null) { + sendStreamStartedEvent(userId, webUser.name, streamId); + } } - } private function sendStreamStartedEvent(userId: String, name: String, stream: String):void{ diff --git a/bigbluebutton-html5/client/main.html b/bigbluebutton-html5/client/main.html index 8344297a488e8a26ce5370de565ce88f34071844..e29b01b158425285740c30daf634096ab0ae608f 100644 --- a/bigbluebutton-html5/client/main.html +++ b/bigbluebutton-html5/client/main.html @@ -53,4 +53,9 @@ <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> + <script src="/client/lib/reconnecting-websocket.min.js"></script> + <script src="/client/lib/kurento-utils.js"></script> + <script src="/client/lib/kurento-extension.js"></script> + <script src="/html5client/js/adapter.js"></script> + <script src="/html5client/js/adjust-videos.js"></script> </body> diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js index 5b953b2ea43f93c1add309adf8b5eb185d52ae17..2c6f548690c4ffbe20d8352b7ef5dab2392630e5 100644 --- a/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/index.js @@ -1,5 +1,7 @@ import VertoBridge from './verto'; +import KurentoBridge from './kurento'; -const screenshareBridge = new VertoBridge(); +//const screenshareBridge = new VertoBridge(); +const screenshareBridge = new KurentoBridge(); export default screenshareBridge; diff --git a/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js new file mode 100755 index 0000000000000000000000000000000000000000..77e10ef49f17aa622bb35ba843f86e073fd885d6 --- /dev/null +++ b/bigbluebutton-html5/imports/api/screenshare/client/bridge/kurento.js @@ -0,0 +1,51 @@ +import Users from '/imports/api/users'; +import Auth from '/imports/ui/services/auth'; +import BridgeService from './service'; + +const CHROME_EXTENSION_KEY = Meteor.settings.public.kurento.chromeExtensionKey; + +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, + CHROME_EXTENSION_KEY, + ); + } + + kurentoExitScreenShare() { + window.kurentoExitScreenShare(); + } +} diff --git a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js index 92b24864c11fe4e244538a58fc0f594d61cdcbe8..aacc7d1eeab570c5cbd5169f0105164bccd2c99d 100644 --- a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js +++ b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStarted.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import addScreenshare from '../modifiers/addScreenshare'; -export default function handleBroadcastStartedVoice({ body }, meetingId) { +export default function handleScreenshareStarted({ body }, meetingId) { check(meetingId, String); check(body, Object); diff --git a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js index d0308ab5a312c3de18cc23175ce23e9237a92372..11e2871f0c7fb7ab0ad494fd8bb5336774b4fd55 100644 --- a/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js +++ b/bigbluebutton-html5/imports/api/screenshare/server/handlers/screenshareStopped.js @@ -1,7 +1,7 @@ import { check } from 'meteor/check'; import clearScreenshare from '../modifiers/clearScreenshare'; -export default function handleBroadcastStartedVoice({ body }, meetingId) { +export default function handleScreenshareStopped({ body }, meetingId) { const { screenshareConf } = body; check(meetingId, String); diff --git a/bigbluebutton-html5/imports/api/video/server/eventHandlers.js b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js new file mode 100644 index 0000000000000000000000000000000000000000..0e773b08eafe12ba8138ab8523b8a4c2e5196a82 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/eventHandlers.js @@ -0,0 +1,6 @@ +import RedisPubSub from '/imports/startup/server/redis'; +import handleUserSharedHtml5Webcam from './handlers/userSharedHtml5Webcam'; +import handleUserUnsharedHtml5Webcam from './handlers/userUnsharedHtml5Webcam'; + +RedisPubSub.on('UserBroadcastCamStartedEvtMsg', handleUserSharedHtml5Webcam); +RedisPubSub.on('UserBroadcastCamStoppedEvtMsg', handleUserUnsharedHtml5Webcam); diff --git a/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js new file mode 100644 index 0000000000000000000000000000000000000000..abf5466ffea602df604717d33c941c39c0b3316b --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/handlers/userSharedHtml5Webcam.js @@ -0,0 +1,11 @@ +import sharedWebcam from '../modifiers/sharedWebcam'; +import {check} from 'meteor/check'; + +export default function handleUserSharedHtml5Webcam({ header }, meetingId ) { + check(header, Object); + const { userId } = header; + check(meetingId, String); + check(userId, String); + + return sharedWebcam(meetingId, userId); +} diff --git a/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js new file mode 100644 index 0000000000000000000000000000000000000000..bf6ba596e487375e198c671f8fe154f4e4da7c18 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/handlers/userUnsharedHtml5Webcam.js @@ -0,0 +1,11 @@ +import unsharedWebcam from '../modifiers/unsharedWebcam'; +import { check } from 'meteor/check'; + +export default function handleUserUnsharedHtml5Webcam({ header }, meetingId) { + check(header, Object); + const { userId } = header; + check(meetingId, String); + check(userId, String); + + return unsharedWebcam(meetingId, userId); +} diff --git a/bigbluebutton-html5/imports/api/video/server/index.js b/bigbluebutton-html5/imports/api/video/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9a7510e23ef5b6306a53e4b5743c7810976a3c18 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/index.js @@ -0,0 +1,2 @@ +import './eventHandlers'; +import './methods'; diff --git a/bigbluebutton-html5/imports/api/video/server/methods.js b/bigbluebutton-html5/imports/api/video/server/methods.js new file mode 100644 index 0000000000000000000000000000000000000000..1f1dd46f1096c1479436e90cf776da4d57c1ac2f --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/methods.js @@ -0,0 +1,7 @@ +import { Meteor } from 'meteor/meteor'; +import userShareWebcam from './methods/userShareWebcam'; +import userUnshareWebcam from './methods/userUnshareWebcam'; + +Meteor.methods({ + userShareWebcam, userUnshareWebcam, +}); diff --git a/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..9dbcabf1dde5f3dfb3833ae8d9244cca34b9c012 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/methods/userShareWebcam.js @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import RedisPubSub from '/imports/startup/server/redis'; + +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, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..ebec447808fa5c3e44ea168649cfdb74a0e4d8d4 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/methods/userUnshareWebcam.js @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import Logger from '/imports/startup/server/logger'; +import RedisPubSub from '/imports/startup/server/redis'; + +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, + }; + + return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload); +} diff --git a/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..749407ffc44860feb53b2389a2f68ce6d2e60bf5 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/modifiers/sharedWebcam.js @@ -0,0 +1,33 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; +import { check } from 'meteor/check'; + +export default function sharedWebcam(meetingId, userId) { + check(meetingId, String); + check(userId, String); + + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + meetingId, + userId, + has_stream: true, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding user to collection: ${err}`); + } + + if (numChanged) { + return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`); + } + }; + + return Users.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js new file mode 100644 index 0000000000000000000000000000000000000000..0ef8f0be36d552cd1610f739b30515f75308fac2 --- /dev/null +++ b/bigbluebutton-html5/imports/api/video/server/modifiers/unsharedWebcam.js @@ -0,0 +1,33 @@ +import Logger from '/imports/startup/server/logger'; +import Users from '/imports/api/users'; +import { check } from 'meteor/check'; + +export default function unsharedWebcam(meetingId, userId) { + check(meetingId, String); + check(userId, String); + + const selector = { + meetingId, + userId, + }; + + const modifier = { + $set: { + meetingId, + userId, + has_stream: false, + }, + }; + + const cb = (err, numChanged) => { + if (err) { + return Logger.error(`Adding user to collection: ${err}`); + } + + if (numChanged) { + return Logger.info(`Upserted user id=${userId} meeting=${meetingId}`); + } + }; + + return Users.upsert(selector, modifier, cb); +} diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx index d825b5dc704208a5eea0c51fc5d469ee351e829d..9970b8e74ed1e5815653a22f11cea19b57b16740 100644 --- a/bigbluebutton-html5/imports/startup/client/base.jsx +++ b/bigbluebutton-html5/imports/startup/client/base.jsx @@ -83,7 +83,7 @@ Base.defaultProps = defaultProps; const SUBSCRIPTIONS_NAME = [ 'users', 'chat', 'cursor', 'meetings', 'polls', 'presentations', 'annotations', - 'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', + 'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', 'screenshare', ]; const BaseContainer = createContainer(({ params }) => { diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx index 97a7174b025cf7defe285639e811044edf78298e..8e15d2021a9bcffbfcc3d123d977f9b184c190a9 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/actions-dropdown/component.jsx @@ -32,6 +32,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 { @@ -53,7 +69,13 @@ class ActionsDropdown extends Component { } render() { - const { intl, isUserPresenter } = this.props; + const { + intl, + isUserPresenter, + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, + } = this.props; if (!isUserPresenter) return null; @@ -80,6 +102,18 @@ class ActionsDropdown extends Component { description={intl.formatMessage(intlMessages.presentationDesc)} onClick={this.handlePresentationClick} /> + <DropdownListItem + icon="desktop" + label={intl.formatMessage(intlMessages.desktopShareLabel)} + description={intl.formatMessage(intlMessages.desktopShareDesc)} + onClick={handleShareScreen} + /> + <DropdownListItem + icon="desktop" + label={intl.formatMessage(intlMessages.stopDesktopShareLabel)} + description={intl.formatMessage(intlMessages.stopDesktopShareDesc)} + onClick={handleUnshareScreen} + /> </DropdownList> </DropdownContent> </Dropdown> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx index 74482af8407f3fe4d6d71c3eb91d014b6a897779..f671845580c4b104a657ac7703250d265a14d81b 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/component.jsx @@ -3,20 +3,31 @@ import styles from './styles.scss'; import EmojiSelect from './emoji-select/component'; import ActionsDropdown from './actions-dropdown/component'; import AudioControlsContainer from '../audio/audio-controls/container'; +import JoinVideoOptionsContainer from '../video-dock/video-menu/container'; const ActionsBar = ({ isUserPresenter, + handleExitVideo, + handleJoinVideo, + handleShareScreen, + handleUnshareScreen, + isVideoBroadcasting, emojiList, emojiSelected, handleEmojiChange, }) => ( <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} <EmojiSelect options={emojiList} selected={emojiSelected} onChange={handleEmojiChange} /> </div> </div> diff --git a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx index 943d138689e941a85ebf25c44ffb0dffbb5f31a6..7465303e68711b11b76a5b611aac41c4a70260d3 100644 --- a/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/actions-bar/container.jsx @@ -2,6 +2,8 @@ import React from 'react'; import { createContainer } from 'meteor/react-meteor-data'; import ActionsBar from './component'; import Service from './service'; +import VideoService from '../video-dock/service'; +import ScreenshareService from '../screenshare/service'; const ActionsBarContainer = props => <ActionsBar {...props} />; @@ -10,4 +12,10 @@ export default createContainer(() => ({ emojiList: Service.getEmojiList(), emojiSelected: Service.getEmoji(), handleEmojiChange: Service.setEmoji, + handleExitVideo: () => VideoService.exitVideo(), + handleJoinVideo: () => VideoService.joinVideo(), + handleShareScreen: () => ScreenshareService.shareScreen(), + handleUnshareScreen: () => ScreenshareService.unshareScreen(), + isVideoBroadcasting: () => ScreenshareService.isVideoBroadcasting(), + }), ActionsBarContainer); diff --git a/bigbluebutton-html5/imports/ui/components/media/container.jsx b/bigbluebutton-html5/imports/ui/components/media/container.jsx index de9221e18afa228ef46256da32d02c13a5e6e2f3..119567f8a137dc3093fd980b0a9d0859398276b5 100644 --- a/bigbluebutton-html5/imports/ui/components/media/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/media/container.jsx @@ -8,7 +8,7 @@ import ScreenshareContainer from '../screenshare/container'; import DefaultContent from '../presentation/default-content/component'; const defaultProps = { - overlay: null, // <VideoDockContainer/>, + overlay: <VideoDockContainer />, content: <PresentationAreaContainer />, defaultContent: <DefaultContent />, }; diff --git a/bigbluebutton-html5/imports/ui/components/media/service.js b/bigbluebutton-html5/imports/ui/components/media/service.js index d583c089f19be3a4c79ea3e9d21b9f3f1164ad9b..9523f64779f3dc4eee14ac7f0f138856d0384a1d 100644 --- a/bigbluebutton-html5/imports/ui/components/media/service.js +++ b/bigbluebutton-html5/imports/ui/components/media/service.js @@ -17,11 +17,11 @@ function shouldShowWhiteboard() { } function shouldShowScreenshare() { - return isVideoBroadcasting(); + return isVideoBroadcasting() && Meteor.settings.public.kurento.enableScreensharing; } function shouldShowOverlay() { - return false; + return Meteor.settings.public.kurento.enableVideo; } export default { diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx index 093de370e6ef54ab0be0c1ed601359aa9f67c039..c381280101b799860a9807c0cabff1e0c31199fe 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/screenshare/component.jsx @@ -7,7 +7,7 @@ export default class ScreenshareComponent extends React.Component { render() { return ( - <video id="screenshareVideo" style={{ height: '100%', width: '100%' }} /> + <video id="screenshareVideo" style={{ height: '100%', width: '100%' }} autoPlay playsInline /> ); } } diff --git a/bigbluebutton-html5/imports/ui/components/screenshare/service.js b/bigbluebutton-html5/imports/ui/components/screenshare/service.js index cdb7ecc5ef638abb9c47709b390b6e2b68bab139..33470b2ecd571fe97d01092772178b9240ecaf88 100644 --- a/bigbluebutton-html5/imports/ui/components/screenshare/service.js +++ b/bigbluebutton-html5/imports/ui/components/screenshare/service.js @@ -1,33 +1,46 @@ import Screenshare from '/imports/api/screenshare'; import VertoBridge from '/imports/api/screenshare/client/bridge'; +import KurentoBridge from '/imports/api/screenshare/client/bridge'; import PresentationService from '/imports/ui/components/presentation/service'; // when the meeting information has been updated check to see if it was // screensharing. If it has changed either trigger a call to receive video // and display it, or end the call and hide the video -function isVideoBroadcasting() { +const isVideoBroadcasting = () => { const ds = Screenshare.findOne({}); if (!ds) { return false; } - return ds.screenshare.stream && !PresentationService.isPresenter(); + + // TODO commented out isPresenter to enable screen viewing to the presenter + return ds.screenshare.stream; // && !PresentationService.isPresenter(); } // if remote screenshare has been ended disconnect and hide the video stream -function presenterScreenshareHasEnded() { - // references a function in the global namespace inside verto_extension.js +const presenterScreenshareHasEnded = () => { + // references a function in the global namespace inside kurento-extension.js // that we load dynamically - VertoBridge.vertoExitVideo(); + KurentoBridge.kurentoExitVideo(); } // if remote screenshare has been started connect and display the video stream -function presenterScreenshareHasStarted() { - // references a function in the global namespace inside verto_extension.js +const presenterScreenshareHasStarted = () => { + // references a function in the global namespace inside kurento-extension.js // that we load dynamically - VertoBridge.vertoWatchVideo(); + //VertoBridge.vertoWatchVideo(); + KurentoBridge.kurentoWatchVideo(); +} + +const shareScreen = () => { + KurentoBridge.kurentoShareScreen(); +} + +const unshareScreen = () => { + console.log("Exiting screenshare"); + KurentoBridge.kurentoExitScreenShare(); } export { - isVideoBroadcasting, presenterScreenshareHasEnded, presenterScreenshareHasStarted, + isVideoBroadcasting, presenterScreenshareHasEnded, presenterScreenshareHasStarted, shareScreen, unshareScreen, }; diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx index fcd9e12dbd8bff5bd7965e6b0cc626e36b7bd76c..107fc4785b2ed9dff874629097297eaf3d094887 100644 --- a/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-dock/component.jsx @@ -1,11 +1,504 @@ -import React from 'react'; +import React, { Component } from 'react'; import ScreenshareContainer from '/imports/ui/components/screenshare/container'; import styles from './styles'; +import { log } from '/imports/ui/services/api'; -const VideoDock = () => ( - <div className={styles.videoDock}> - <ScreenshareContainer /> - </div> -); -export default VideoDock; +class VideoElement extends Component { + constructor(props) { + super(props); + } + + render() { + return <video id={`video-elem-${this.props.videoId}`} width={320} height={240} autoPlay={true} playsInline={true} />; + } + + componentDidMount() { + this.props.onMount(this.props.videoId, false); + } +} + +export default class VideoDock extends Component { + constructor(props) { + super(props); + + // Set a valid bbb-webrtc-sfu application server socket in the settings + this.ws = new ReconnectingWebSocket(Meteor.settings.public.kurento.wsUrl); + this.wsQueue = []; + this.webRtcPeers = {}; + this.reconnectWebcam = false; + this.reconnectList = false; + this.sharedCameraTimeout = null; + this.subscribedCamerasTimeouts = []; + + this.state = { + videos: {}, + sharedWebcam : false, + }; + + this.sendUserShareWebcam = props.sendUserShareWebcam.bind(this); + this.sendUserUnshareWebcam = props.sendUserUnshareWebcam.bind(this); + + this.unshareWebcam = this.unshareWebcam.bind(this); + this.shareWebcam = this.shareWebcam.bind(this); + + this.onWsOpen = this.onWsOpen.bind(this); + this.onWsClose = this.onWsClose.bind(this); + this.onWsMessage = this.onWsMessage.bind(this); + } + + setupReconnectVideos() { + for (id in this.webRtcPeers) { + this.disconnected(id); + this.stop(id); + } + } + + reconnectVideos() { + for (i in this.reconnectList) { + const id = this.reconnectList[i]; + + // TODO: base this on BBB API users instead of using memory + if (id != this.myId) { + setTimeout(() => { + log('debug', ` [camera] Trying to reconnect camera ${id}`); + this.start(id, false); + }, 5000); + } + } + + if (this.reconnectWebcam) { + log('debug', ` [camera] Trying to re-share ${this.myId} after reconnect.`); + this.start(this.myId, true); + } + + this.reconnectWebcam = false; + this.reconnectList = []; + } + + componentDidMount() { + const ws = this.ws; + const { users } = this.props; + const id = users[0].userId; + + for (let i = 0; i < users.length; i++) { + if (users[i].has_stream && users[i].userId !== id) { + this.start(users[i].userId, false); + } + } + + document.addEventListener('joinVideo', this.shareWebcam.bind(this));// TODO find a better way to do this + document.addEventListener('exitVideo', this.unshareWebcam.bind(this)); + + window.addEventListener('resize', this.adjustVideos); + + ws.addEventListener('message', this.onWsMessage); + } + + componentWillMount () { + this.ws.addEventListener('open', this.onWsOpen); + this.ws.addEventListener('close', this.onWsClose); + } + + componentWillUnmount () { + document.removeEventListener('joinVideo', this.shareWebcam); + document.removeEventListener('exitVideo', this.shareWebcam); + window.removeEventListener('resize', this.adjustVideos); + + this.ws.removeEventListener('message', this.onWsMessage); + this.ws.removeEventListener('open', this.onWsOpen); + this.ws.removeEventListener('close', this.onWsClose); + // Close websocket connection to prevent multiple reconnects from happening + this.ws.close(); + } + + adjustVideos () { + window.adjustVideos('webcamArea', true); + } + + onWsOpen () { + log('debug', '------ Websocket connection opened.'); + + // -- Resend queued messages that happened when socket was not connected + while (this.wsQueue.length > 0) { + this.sendMessage(this.wsQueue.pop()); + } + + this.reconnectVideos(); + } + + onWsClose (error) { + log('debug', '------ Websocket connection closed.'); + + this.setupReconnectVideos(); + } + + onWsMessage (msg) { + const parsedMessage = JSON.parse(msg.data); + + console.log('Received message new ws message: '); + console.log(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.webRtcPeers[parsedMessage.cameraId]; + + if (webRtcPeer !== null) { + if (webRtcPeer.didSDPAnswered) { + webRtcPeer.addIceCandidate(parsedMessage.candidate, (err) => { + if (err) { + return log('error', `Error adding candidate: ${err}`); + } + }); + } else { + webRtcPeer.iceQueue.push(parsedMessage.candidate); + } + } else { + log('error', ' [ICE] Message arrived before webRtcPeer?'); + } + break; + } + }; + + start(id, shareWebcam) { + const that = this; + + console.log(`Starting video call for video: ${id} with ${shareWebcam}`); + + if (shareWebcam) { + this.setState({sharedWebcam: true}); + this.initWebRTC(id, true); + } else { + // initWebRTC with shareWebcam false will be called after react mounts the element + this.createVideoTag(id); + } + } + + initWebRTC(id, shareWebcam) { + let that = this; + + const onIceCandidate = function (candidate) { + const message = { + type: 'video', + role: shareWebcam ? 'share' : 'viewer', + id: 'onIceCandidate', + candidate, + cameraId: id, + }; + that.sendMessage(message); + }; + + let videoConstraints = {}; + if (!!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/)) { // Custom constraints for Safari + videoConstraints = { + width: {min:320, max:640}, + height: {min:240, max:480} + } + } else { + videoConstraints = { + width: {min: 320, ideal: 320}, + height: {min: 240, ideal:240}, + frameRate: {min: 5, ideal: 10} + }; + } + + let options = { + mediaConstraints: { + audio: false, + video: videoConstraints + }, + onicecandidate: onIceCandidate, + }; + + let peerObj; + if (shareWebcam) { + options.localVideo = this.refs.videoInput; + peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerSendonly; + } else { + peerObj = kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly; + options.remoteVideo = document.getElementById(`video-elem-${id}`); + } + + let webRtcPeer = new peerObj(options, function (error) { + if (error) { + log('error', ' WebRTC peerObj create error'); + + that.destroyWebRTCPeer(id); + that.destroyVideoTag(id); + + return log('error', error); + } + + this.didSDPAnswered = false; + this.iceQueue = []; + + that.webRtcPeers[id] = webRtcPeer; + if (shareWebcam) { + that.sharedWebcam = webRtcPeer; + that.myId = id; + } + + this.generateOffer((error, offerSdp) => { + if (error) { + log('error', ' WebRtc generate offer error'); + + that.destroyWebRTCPeer(id); + that.destroyVideoTag(id); + + return log('error', error); + } + + console.log(`Invoking SDP offer callback function ${location.host}`); + const message = { + type: 'video', + role: shareWebcam ? 'share' : 'viewer', + id: 'start', + sdpOffer: offerSdp, + cameraId: id, + }; + that.sendMessage(message); + }); + while (this.iceQueue.length) { + let candidate = this.iceQueue.shift(); + this.addIceCandidate(candidate, (err) => { + if (err) { + return console.error(`Error adding candidate: ${err}`); + } + }); + } + this.didSDPAnswered = true; + }); + } + + disconnected(id) { + if (this.sharedWebcam) { + log('debug', ' [camera] Webcam disconnected, will try re-share webcam later.'); + this.reconnectWebcam = true; + } else { + this.reconnectList.push(id); + + log('debug', ` [camera] ${id} disconnected, will try re-subscribe later.`); + } + } + + stop(id) { + const { users } = this.props; + this.sendMessage({ + type: 'video', + role: id == users[0].userId ? 'share' : 'viewer', + id: 'stop', + cameraId: id, + }); + + this.destroyWebRTCPeer(id); + this.destroyVideoTag(id); + } + + createVideoTag(id) { + let videos = this.state.videos; + + videos[id] = true; + this.setState({videos: videos}) + } + + destroyVideoTag(id) { + let videos = this.state.videos; + + delete videos[id]; + this.setState({videos: videos}); + + if (id == this.myId) { + this.setState({sharedWebcam: false}); + } + } + + destroyWebRTCPeer(id) { + const webRtcPeer = this.webRtcPeers[id]; + + if (webRtcPeer) { + log('info', 'Stopping WebRTC peer'); + + if (id == this.myId && this.sharedWebcam) { + this.sharedWebcam.dispose(); + this.sharedWebcam = null; + } + + webRtcPeer.dispose(); + delete this.webRtcPeers[id]; + } else { + log('info', 'No WebRTC peer to stop (not an error)'); + } + } + + shareWebcam() { + const { users } = this.props; + const id = users[0].userId; + + if (this.connectedToMediaServer()) { + this.start(id, true); + } else { + log("error", "Not connected to media server BRA"); + } + } + + unshareWebcam() { + log('info', 'Unsharing webcam'); + const { users } = this.props; + const id = users[0].userId; + this.sendUserUnshareWebcam(id); + } + + startResponse(message) { + const id = message.cameraId; + const webRtcPeer = this.webRtcPeers[id]; + + if (message.sdpAnswer == null) { + return log('debug', 'Null sdp answer. Camera unplugged?'); + } + + if (webRtcPeer == null) { + return log('debug', 'Null webrtc peer ????'); + } + + log('info', 'SDP answer received from server. Processing ...'); + + webRtcPeer.processAnswer(message.sdpAnswer, (error) => { + if (error) { + return log('error', error); + } + }); + + this.sendUserShareWebcam(id); + } + + sendMessage(message) { + const ws = this.ws; + + if (this.connectedToMediaServer()) { + 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 { + // No need to queue video stop messages + if (message.id != 'stop') { + this.wsQueue.push(message); + } + } + } + + connectedToMediaServer() { + return this.ws.readyState === WebSocket.OPEN; + } + + connectionStatus() { + return this.ws.readyState; + } + + handlePlayStop(message) { + log('info', 'Handle play stop <--------------------'); + log('error', message); + + const { users } = this.props; + + if (message.cameraId == users[0].userId) { + this.unshareWebcam(); + } else { + this.stop(message.cameraId); + } + } + + handlePlayStart(message) { + log('info', 'Handle play start <==================='); + } + + handleError(message) { + console.error(' Handle error --------------------->'); + log('debug', message.message); + } + + componentDidUpdate() { + this.adjustVideos(); + } + + render() { + let cssClass; + if (this.state.sharedWebcam) { + cssClass = styles.sharedWebcamVideoLocal; + } + else { + cssClass = styles.sharedWebcamVideo; + } + + return ( + + <div className={styles.videoDock}> + <div id="webcamArea"> + {Object.keys(this.state.videos).map((id) => { + return (<VideoElement videoId={id} key={id} onMount={this.initWebRTC.bind(this)} />); + })} + <video autoPlay={true} playsInline={true} muted={true} id="shareWebcamVideo" className={cssClass} ref="videoInput" /> + </div> + </div> + ); + } + + shouldComponentUpdate(nextProps, nextState) { + const { users } = this.props; + const nextUsers = nextProps.users; + const id = users[0].userId; + + 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) { + if (id !== users[i].userId) { + this.start(users[i].userId, false); + } + } else { + this.stop(users[i].userId); + } + + if (!nextUsers[i].has_stream) { + this.destroyVideoTag(users[i].userId); + } + + suc = suc || true; + } + } + } + + return true; + } + + return false; + } +} diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx index ad18e501e980831cb554c5070b75b55c51a12f16..a92bbb3d0989192cca4b2840832df304611843d5 100644 --- a/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/video-dock/container.jsx @@ -1,15 +1,25 @@ -import React from 'react'; +import React, { Component } from 'react'; import { createContainer } from 'meteor/react-meteor-data'; import VideoDock from './component'; +import VideoService from './service'; -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: VideoService.getAllUsers(), +}), VideoDockContainer); diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/service.js b/bigbluebutton-html5/imports/ui/components/video-dock/service.js new file mode 100644 index 0000000000000000000000000000000000000000..5d778fca8f2385b81697876dc7773327f94bcbaa --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/service.js @@ -0,0 +1,29 @@ +import { makeCall } from '/imports/ui/services/api'; +import Users from '/imports/api/users'; +import { createContainer } from 'meteor/react-meteor-data'; + +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); +}; + +const getAllUsers = () => { + return Users.find().fetch(); +} + +export default { + sendUserShareWebcam, sendUserUnshareWebcam, joinVideo, exitVideo, getAllUsers, +}; diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss index a37e967d4ce74c1e91f784b5d0fac052d3fd6009..2dce62f218a11fd0b3ffbc6fc7e9c561bb4a6c37 100644 --- a/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss +++ b/bigbluebutton-html5/imports/ui/components/video-dock/styles.scss @@ -7,9 +7,16 @@ 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; } + +.sharedWebcamVideo { + display: none; +} + +.sharedWebcamVideoLocal { + display: normal; +} diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx new file mode 100755 index 0000000000000000000000000000000000000000..7d08e37f35c430269cb66b01fad4bf237c458ca6 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/component.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { createContainer } from 'meteor/react-meteor-data'; +import Button from '/imports/ui/components/button/component'; +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)} + hideLabel + aria-label={intl.formatMessage(intlMessages.leaveVideo)} + color={'danger'} + icon={'video'} + size={'lg'} + circle + /> + ); + } + + return ( + <Button + onClick={handleJoinVideo} + label={intl.formatMessage(intlMessages.joinVideo)} + hideLabel + aria-label={intl.formatMessage(intlMessages.joinVideo)} + color={'primary'} + icon={'video_off'} + size={'lg'} + circle + /> + ); + } +} + +export default injectIntl(JoinVideoOptions); diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx new file mode 100755 index 0000000000000000000000000000000000000000..da7d5517f5147408c91fba55207818fc666b7423 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/container.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { createContainer } from 'meteor/react-meteor-data'; +import JoinVideoOptions from './component'; +import VideoMenuService from './service'; + +const JoinVideoOptionsContainer = props => (<JoinVideoOptions {...props} />); + +export default createContainer((params) => { + const isSharingVideo = VideoMenuService.isSharingVideo(); + return { + isSharingVideo, + handleJoinVideo: params.handleJoinVideo, + handleCloseVideo: params.handleCloseVideo, + }; +}, JoinVideoOptionsContainer); diff --git a/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/service.js b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/service.js new file mode 100644 index 0000000000000000000000000000000000000000..92a58cd21afc50dac0196a3d19d325fea668e125 --- /dev/null +++ b/bigbluebutton-html5/imports/ui/components/video-dock/video-menu/service.js @@ -0,0 +1,12 @@ +import Users from '/imports/api/users'; +import Auth from '/imports/ui/services/auth/index'; + +const isSharingVideo = () => { + const userId = Auth.userID; + const user = Users.findOne({ userId: userId }); + return user.has_stream ? true : false; +}; + +export default { + isSharingVideo +}; diff --git a/bigbluebutton-html5/private/config/development/public/kurento.yaml b/bigbluebutton-html5/private/config/development/public/kurento.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3a2e239be80123772738cea177f5bda6b7fd79e7 --- /dev/null +++ b/bigbluebutton-html5/private/config/development/public/kurento.yaml @@ -0,0 +1,6 @@ +kurento: + wsUrl: 'HOST' + chromeExtensionKey: 'KEY' + chromeExtensionLink: 'LINK' + enableScreensharing: false + enableVideo: false diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 073bc37727d99671911557d1a7b06431c26d0242..728ec78ae34f2731b4b648c3b788838be27be730 100644 --- a/bigbluebutton-html5/private/locales/en.json +++ b/bigbluebutton-html5/private/locales/en.json @@ -173,9 +173,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", @@ -244,6 +246,8 @@ "app.audioManager.mediaError": "Error: There was an issue getting your media devices", "app.audio.joinAudio": "Join Audio", "app.audio.leaveAudio": "Leave Audio", + "app.video.joinVideo": "Share Webcam", + "app.video.leaveVideo": "Un-share Webcam", "app.audio.enterSessionLabel": "Enter Session", "app.audio.playSoundLabel": "Play Sound", "app.audio.backLabel": "Back", @@ -269,5 +273,7 @@ "app.toast.chat.singular":"you have {0} new message in {1}", "app.toast.chat.plural":"you have {0} new messages in {1}", "app.notification.recordingStart": "This session is now being recorded", - "app.notification.recordingStop": "This session is not being recorded anymore" + "app.notification.recordingStop": "This session is not being recorded anymore", + "app.video.joinVideo": "Share webcam", + "app.video.leaveVideo": "Unshare webcam" } diff --git a/bigbluebutton-html5/private/locales/pt_BR.json b/bigbluebutton-html5/private/locales/pt_BR.json index 0cb562cf2bfb75eacb809fea852dedb35f4a4d0a..00caa32a14f37e6fdedab57ad5ee5e5c0e5e5618 100644 --- a/bigbluebutton-html5/private/locales/pt_BR.json +++ b/bigbluebutton-html5/private/locales/pt_BR.json @@ -204,6 +204,8 @@ "app.audioModal.closeLabel": "Fechar", "app.audio.joinAudio": "Ativar áudio", "app.audio.leaveAudio": "Desativar áudio", + "app.video.joinVideo": "Compartilhar câmera", + "app.video.leaveVideo": "Des-compartilhar câmera", "app.audio.enterSessionLabel": "Entrar na reunião", "app.audio.playSoundLabel": "Tocar som de teste", "app.audio.backLabel": "Voltar", diff --git a/bigbluebutton-html5/public/js/adapter.js b/bigbluebutton-html5/public/js/adapter.js new file mode 100644 index 0000000000000000000000000000000000000000..fc1d8c8ad9bf5df2141e314a6fcc09f01b627db4 --- /dev/null +++ b/bigbluebutton-html5/public/js/adapter.js @@ -0,0 +1,4450 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* + * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var SDPUtils = require('sdp'); + +function writeMediaSection(transceiver, caps, type, stream, dtlsRole) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : dtlsRole || 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.direction) { + sdp += 'a=' + transceiver.direction + '\r\n'; + } else if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + if (transceiver.rtpSender) { + // spec. + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + + // for Chrome. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + if (transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' ' + msid; + sdp += 'a=ssrc-group:FID ' + + transceiver.sendEncodingParameters[0].ssrc + ' ' + + transceiver.sendEncodingParameters[0].rtx.ssrc + + '\r\n'; + } + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + } + return sdp; +} + +// Edge does not like +// 1) stun: filtered after 14393 unless ?transport=udp is present +// 2) turn: that does not have all of turn:host:port?transport=udp +// 3) turn: with ipv6 addresses +// 4) turn: occurring muliple times +function filterIceServers(iceServers, edgeVersion) { + var hasTurn = false; + iceServers = JSON.parse(JSON.stringify(iceServers)); + return iceServers.filter(function(server) { + if (server && (server.urls || server.url)) { + var urls = server.urls || server.url; + if (server.url && !server.urls) { + console.warn('RTCIceServer.url is deprecated! Use urls instead.'); + } + var isString = typeof urls === 'string'; + if (isString) { + urls = [urls]; + } + urls = urls.filter(function(url) { + var validTurn = url.indexOf('turn:') === 0 && + url.indexOf('transport=udp') !== -1 && + url.indexOf('turn:[') === -1 && + !hasTurn; + + if (validTurn) { + hasTurn = true; + return true; + } + return url.indexOf('stun:') === 0 && edgeVersion >= 14393 && + url.indexOf('?transport=udp') === -1; + }); + + delete server.url; + server.urls = isString ? urls[0] : urls; + return !!urls.length; + } + return false; + }); +} + +// Determines the intersection of local and remote capabilities. +function getCommonCapabilities(localCapabilities, remoteCapabilities) { + var commonCapabilities = { + codecs: [], + headerExtensions: [], + fecMechanisms: [] + }; + + var findCodecByPayloadType = function(pt, codecs) { + pt = parseInt(pt, 10); + for (var i = 0; i < codecs.length; i++) { + if (codecs[i].payloadType === pt || + codecs[i].preferredPayloadType === pt) { + return codecs[i]; + } + } + }; + + var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) { + var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs); + var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs); + return lCodec && rCodec && + lCodec.name.toLowerCase() === rCodec.name.toLowerCase(); + }; + + localCapabilities.codecs.forEach(function(lCodec) { + for (var i = 0; i < remoteCapabilities.codecs.length; i++) { + var rCodec = remoteCapabilities.codecs[i]; + if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && + lCodec.clockRate === rCodec.clockRate) { + if (lCodec.name.toLowerCase() === 'rtx' && + lCodec.parameters && rCodec.parameters.apt) { + // for RTX we need to find the local rtx that has a apt + // which points to the same local codec as the remote one. + if (!rtxCapabilityMatches(lCodec, rCodec, + localCapabilities.codecs, remoteCapabilities.codecs)) { + continue; + } + } + rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy + // number of channels is the highest common number of channels + rCodec.numChannels = Math.min(lCodec.numChannels, + rCodec.numChannels); + // push rCodec so we reply with offerer payload type + commonCapabilities.codecs.push(rCodec); + + // determine common feedback mechanisms + rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) { + for (var j = 0; j < lCodec.rtcpFeedback.length; j++) { + if (lCodec.rtcpFeedback[j].type === fb.type && + lCodec.rtcpFeedback[j].parameter === fb.parameter) { + return true; + } + } + return false; + }); + // FIXME: also need to determine .parameters + // see https://github.com/openpeer/ortc/issues/569 + break; + } + } + }); + + localCapabilities.headerExtensions.forEach(function(lHeaderExtension) { + for (var i = 0; i < remoteCapabilities.headerExtensions.length; + i++) { + var rHeaderExtension = remoteCapabilities.headerExtensions[i]; + if (lHeaderExtension.uri === rHeaderExtension.uri) { + commonCapabilities.headerExtensions.push(rHeaderExtension); + break; + } + } + }); + + // FIXME: fecMechanisms + return commonCapabilities; +} + +// is action=setLocalDescription with type allowed in signalingState +function isActionAllowedInSignalingState(action, type, signalingState) { + return { + offer: { + setLocalDescription: ['stable', 'have-local-offer'], + setRemoteDescription: ['stable', 'have-remote-offer'] + }, + answer: { + setLocalDescription: ['have-remote-offer', 'have-local-pranswer'], + setRemoteDescription: ['have-local-offer', 'have-remote-pranswer'] + } + }[type][action].indexOf(signalingState) !== -1; +} + +function maybeAddCandidate(iceTransport, candidate) { + // Edge's internal representation adds some fields therefore + // not all fieldѕ are taken into account. + var alreadyAdded = iceTransport.getRemoteCandidates() + .find(function(remoteCandidate) { + return candidate.foundation === remoteCandidate.foundation && + candidate.ip === remoteCandidate.ip && + candidate.port === remoteCandidate.port && + candidate.priority === remoteCandidate.priority && + candidate.protocol === remoteCandidate.protocol && + candidate.type === remoteCandidate.type; + }); + if (!alreadyAdded) { + iceTransport.addRemoteCandidate(candidate); + } + return !alreadyAdded; +} + +module.exports = function(window, edgeVersion) { + var RTCPeerConnection = function(config) { + var self = this; + + var _eventTarget = document.createDocumentFragment(); + ['addEventListener', 'removeEventListener', 'dispatchEvent'] + .forEach(function(method) { + self[method] = _eventTarget[method].bind(_eventTarget); + }); + + this.onicecandidate = null; + this.onaddstream = null; + this.ontrack = null; + this.onremovestream = null; + this.onsignalingstatechange = null; + this.oniceconnectionstatechange = null; + this.onicegatheringstatechange = null; + this.onnegotiationneeded = null; + this.ondatachannel = null; + this.canTrickleIceCandidates = null; + + this.needNegotiation = false; + + this.localStreams = []; + this.remoteStreams = []; + + this.localDescription = null; + this.remoteDescription = null; + + this.signalingState = 'stable'; + this.iceConnectionState = 'new'; + this.iceGatheringState = 'new'; + + config = JSON.parse(JSON.stringify(config || {})); + + this.usingBundle = config.bundlePolicy === 'max-bundle'; + if (config.rtcpMuxPolicy === 'negotiate') { + var e = new Error('rtcpMuxPolicy \'negotiate\' is not supported'); + e.name = 'NotSupportedError'; + throw(e); + } else if (!config.rtcpMuxPolicy) { + config.rtcpMuxPolicy = 'require'; + } + + switch (config.iceTransportPolicy) { + case 'all': + case 'relay': + break; + default: + config.iceTransportPolicy = 'all'; + break; + } + + switch (config.bundlePolicy) { + case 'balanced': + case 'max-compat': + case 'max-bundle': + break; + default: + config.bundlePolicy = 'balanced'; + break; + } + + config.iceServers = filterIceServers(config.iceServers || [], edgeVersion); + + this._iceGatherers = []; + if (config.iceCandidatePoolSize) { + for (var i = config.iceCandidatePoolSize; i > 0; i--) { + this._iceGatherers = new window.RTCIceGatherer({ + iceServers: config.iceServers, + gatherPolicy: config.iceTransportPolicy + }); + } + } else { + config.iceCandidatePoolSize = 0; + } + + this._config = config; + + // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... + // everything that is needed to describe a SDP m-line. + this.transceivers = []; + + this._sdpSessionId = SDPUtils.generateSessionId(); + this._sdpSessionVersion = 0; + + this._dtlsRole = undefined; // role for a=setup to use in answers. + }; + + RTCPeerConnection.prototype._emitGatheringStateChange = function() { + var event = new Event('icegatheringstatechange'); + this.dispatchEvent(event); + if (typeof this.onicegatheringstatechange === 'function') { + this.onicegatheringstatechange(event); + } + }; + + RTCPeerConnection.prototype.getConfiguration = function() { + return this._config; + }; + + RTCPeerConnection.prototype.getLocalStreams = function() { + return this.localStreams; + }; + + RTCPeerConnection.prototype.getRemoteStreams = function() { + return this.remoteStreams; + }; + + // internal helper to create a transceiver object. + // (whih is not yet the same as the WebRTC 1.0 transceiver) + RTCPeerConnection.prototype._createTransceiver = function(kind) { + var hasBundleTransport = this.transceivers.length > 0; + var transceiver = { + track: null, + iceGatherer: null, + iceTransport: null, + dtlsTransport: null, + localCapabilities: null, + remoteCapabilities: null, + rtpSender: null, + rtpReceiver: null, + kind: kind, + mid: null, + sendEncodingParameters: null, + recvEncodingParameters: null, + stream: null, + wantReceive: true + }; + if (this.usingBundle && hasBundleTransport) { + transceiver.iceTransport = this.transceivers[0].iceTransport; + transceiver.dtlsTransport = this.transceivers[0].dtlsTransport; + } else { + var transports = this._createIceAndDtlsTransports(); + transceiver.iceTransport = transports.iceTransport; + transceiver.dtlsTransport = transports.dtlsTransport; + } + this.transceivers.push(transceiver); + return transceiver; + }; + + RTCPeerConnection.prototype.addTrack = function(track, stream) { + var transceiver; + for (var i = 0; i < this.transceivers.length; i++) { + if (!this.transceivers[i].track && + this.transceivers[i].kind === track.kind) { + transceiver = this.transceivers[i]; + } + } + if (!transceiver) { + transceiver = this._createTransceiver(track.kind); + } + + this._maybeFireNegotiationNeeded(); + + if (this.localStreams.indexOf(stream) === -1) { + this.localStreams.push(stream); + } + + transceiver.track = track; + transceiver.stream = stream; + transceiver.rtpSender = new window.RTCRtpSender(track, + transceiver.dtlsTransport); + return transceiver.rtpSender; + }; + + RTCPeerConnection.prototype.addStream = function(stream) { + var self = this; + if (edgeVersion >= 15025) { + stream.getTracks().forEach(function(track) { + self.addTrack(track, stream); + }); + } else { + // Clone is necessary for local demos mostly, attaching directly + // to two different senders does not work (build 10547). + // Fixed in 15025 (or earlier) + var clonedStream = stream.clone(); + stream.getTracks().forEach(function(track, idx) { + var clonedTrack = clonedStream.getTracks()[idx]; + track.addEventListener('enabled', function(event) { + clonedTrack.enabled = event.enabled; + }); + }); + clonedStream.getTracks().forEach(function(track) { + self.addTrack(track, clonedStream); + }); + } + }; + + RTCPeerConnection.prototype.removeStream = function(stream) { + var idx = this.localStreams.indexOf(stream); + if (idx > -1) { + this.localStreams.splice(idx, 1); + this._maybeFireNegotiationNeeded(); + } + }; + + RTCPeerConnection.prototype.getSenders = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpSender; + }) + .map(function(transceiver) { + return transceiver.rtpSender; + }); + }; + + RTCPeerConnection.prototype.getReceivers = function() { + return this.transceivers.filter(function(transceiver) { + return !!transceiver.rtpReceiver; + }) + .map(function(transceiver) { + return transceiver.rtpReceiver; + }); + }; + + + RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex, + usingBundle) { + var self = this; + if (usingBundle && sdpMLineIndex > 0) { + return this.transceivers[0].iceGatherer; + } else if (this._iceGatherers.length) { + return this._iceGatherers.shift(); + } + var iceGatherer = new window.RTCIceGatherer({ + iceServers: this._config.iceServers, + gatherPolicy: this._config.iceTransportPolicy + }); + Object.defineProperty(iceGatherer, 'state', + {value: 'new', writable: true} + ); + + this.transceivers[sdpMLineIndex].candidates = []; + this.transceivers[sdpMLineIndex].bufferCandidates = function(event) { + var end = !event.candidate || Object.keys(event.candidate).length === 0; + // polyfill since RTCIceGatherer.state is not implemented in + // Edge 10547 yet. + iceGatherer.state = end ? 'completed' : 'gathering'; + if (self.transceivers[sdpMLineIndex].candidates !== null) { + self.transceivers[sdpMLineIndex].candidates.push(event.candidate); + } + }; + iceGatherer.addEventListener('localcandidate', + this.transceivers[sdpMLineIndex].bufferCandidates); + return iceGatherer; + }; + + // start gathering from an RTCIceGatherer. + RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) { + var self = this; + var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; + if (iceGatherer.onlocalcandidate) { + return; + } + var candidates = this.transceivers[sdpMLineIndex].candidates; + this.transceivers[sdpMLineIndex].candidates = null; + iceGatherer.removeEventListener('localcandidate', + this.transceivers[sdpMLineIndex].bufferCandidates); + iceGatherer.onlocalcandidate = function(evt) { + if (self.usingBundle && sdpMLineIndex > 0) { + // if we know that we use bundle we can drop candidates with + // ѕdpMLineIndex > 0. If we don't do this then our state gets + // confused since we dispose the extra ice gatherer. + return; + } + var event = new Event('icecandidate'); + event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; + + var cand = evt.candidate; + // Edge emits an empty object for RTCIceCandidateComplete‥ + var end = !cand || Object.keys(cand).length === 0; + if (end) { + // polyfill since RTCIceGatherer.state is not implemented in + // Edge 10547 yet. + if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') { + iceGatherer.state = 'completed'; + } + } else { + if (iceGatherer.state === 'new') { + iceGatherer.state = 'gathering'; + } + // RTCIceCandidate doesn't have a component, needs to be added + cand.component = 1; + event.candidate.candidate = SDPUtils.writeCandidate(cand); + } + + // update local description. + var sections = SDPUtils.splitSections(self.localDescription.sdp); + if (!end) { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=' + event.candidate.candidate + '\r\n'; + } else { + sections[event.candidate.sdpMLineIndex + 1] += + 'a=end-of-candidates\r\n'; + } + self.localDescription.sdp = sections.join(''); + var complete = self.transceivers.every(function(transceiver) { + return transceiver.iceGatherer && + transceiver.iceGatherer.state === 'completed'; + }); + + if (self.iceGatheringState !== 'gathering') { + self.iceGatheringState = 'gathering'; + self._emitGatheringStateChange(); + } + + // Emit candidate. Also emit null candidate when all gatherers are + // complete. + if (!end) { + self.dispatchEvent(event); + if (typeof self.onicecandidate === 'function') { + self.onicecandidate(event); + } + } + if (complete) { + self.dispatchEvent(new Event('icecandidate')); + if (typeof self.onicecandidate === 'function') { + self.onicecandidate(new Event('icecandidate')); + } + self.iceGatheringState = 'complete'; + self._emitGatheringStateChange(); + } + }; + + // emit already gathered candidates. + window.setTimeout(function() { + candidates.forEach(function(candidate) { + var e = new Event('RTCIceGatherEvent'); + e.candidate = candidate; + iceGatherer.onlocalcandidate(e); + }); + }, 0); + }; + + // Create ICE transport and DTLS transport. + RTCPeerConnection.prototype._createIceAndDtlsTransports = function() { + var self = this; + var iceTransport = new window.RTCIceTransport(null); + iceTransport.onicestatechange = function() { + self._updateConnectionState(); + }; + + var dtlsTransport = new window.RTCDtlsTransport(iceTransport); + dtlsTransport.ondtlsstatechange = function() { + self._updateConnectionState(); + }; + dtlsTransport.onerror = function() { + // onerror does not set state to failed by itself. + Object.defineProperty(dtlsTransport, 'state', + {value: 'failed', writable: true}); + self._updateConnectionState(); + }; + + return { + iceTransport: iceTransport, + dtlsTransport: dtlsTransport + }; + }; + + // Destroy ICE gatherer, ICE transport and DTLS transport. + // Without triggering the callbacks. + RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function( + sdpMLineIndex) { + var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; + if (iceGatherer) { + delete iceGatherer.onlocalcandidate; + delete this.transceivers[sdpMLineIndex].iceGatherer; + } + var iceTransport = this.transceivers[sdpMLineIndex].iceTransport; + if (iceTransport) { + delete iceTransport.onicestatechange; + delete this.transceivers[sdpMLineIndex].iceTransport; + } + var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport; + if (dtlsTransport) { + delete dtlsTransport.ondtlsstatechange; + delete dtlsTransport.onerror; + delete this.transceivers[sdpMLineIndex].dtlsTransport; + } + }; + + // Start the RTP Sender and Receiver for a transceiver. + RTCPeerConnection.prototype._transceive = function(transceiver, + send, recv) { + var params = getCommonCapabilities(transceiver.localCapabilities, + transceiver.remoteCapabilities); + if (send && transceiver.rtpSender) { + params.encodings = transceiver.sendEncodingParameters; + params.rtcp = { + cname: SDPUtils.localCName, + compound: transceiver.rtcpParameters.compound + }; + if (transceiver.recvEncodingParameters.length) { + params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; + } + transceiver.rtpSender.send(params); + } + if (recv && transceiver.rtpReceiver && params.codecs.length > 0) { + // remove RTX field in Edge 14942 + if (transceiver.kind === 'video' + && transceiver.recvEncodingParameters + && edgeVersion < 15019) { + transceiver.recvEncodingParameters.forEach(function(p) { + delete p.rtx; + }); + } + params.encodings = transceiver.recvEncodingParameters; + params.rtcp = { + cname: transceiver.rtcpParameters.cname, + compound: transceiver.rtcpParameters.compound + }; + if (transceiver.sendEncodingParameters.length) { + params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; + } + transceiver.rtpReceiver.receive(params); + } + }; + + RTCPeerConnection.prototype.setLocalDescription = function(description) { + var self = this; + var args = arguments; + + if (!isActionAllowedInSignalingState('setLocalDescription', + description.type, this.signalingState)) { + return new Promise(function(resolve, reject) { + var e = new Error('Can not set local ' + description.type + + ' in state ' + self.signalingState); + e.name = 'InvalidStateError'; + if (args.length > 2 && typeof args[2] === 'function') { + args[2].apply(null, [e]); + } + reject(e); + }); + } + + var sections; + var sessionpart; + if (description.type === 'offer') { + // VERY limited support for SDP munging. Limited to: + // * changing the order of codecs + sections = SDPUtils.splitSections(description.sdp); + sessionpart = sections.shift(); + sections.forEach(function(mediaSection, sdpMLineIndex) { + var caps = SDPUtils.parseRtpParameters(mediaSection); + self.transceivers[sdpMLineIndex].localCapabilities = caps; + }); + + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { + self._gather(transceiver.mid, sdpMLineIndex); + }); + } else if (description.type === 'answer') { + sections = SDPUtils.splitSections(self.remoteDescription.sdp); + sessionpart = sections.shift(); + var isIceLite = SDPUtils.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + sections.forEach(function(mediaSection, sdpMLineIndex) { + var transceiver = self.transceivers[sdpMLineIndex]; + var iceGatherer = transceiver.iceGatherer; + var iceTransport = transceiver.iceTransport; + var dtlsTransport = transceiver.dtlsTransport; + var localCapabilities = transceiver.localCapabilities; + var remoteCapabilities = transceiver.remoteCapabilities; + + // treat bundle-only as not-rejected. + var rejected = SDPUtils.isRejected(mediaSection) && + !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1; + + if (!rejected && !transceiver.isDatachannel) { + var remoteIceParameters = SDPUtils.getIceParameters( + mediaSection, sessionpart); + var remoteDtlsParameters = SDPUtils.getDtlsParameters( + mediaSection, sessionpart); + if (isIceLite) { + remoteDtlsParameters.role = 'server'; + } + + if (!self.usingBundle || sdpMLineIndex === 0) { + self._gather(transceiver.mid, sdpMLineIndex); + if (iceTransport.state === 'new') { + iceTransport.start(iceGatherer, remoteIceParameters, + isIceLite ? 'controlling' : 'controlled'); + } + if (dtlsTransport.state === 'new') { + dtlsTransport.start(remoteDtlsParameters); + } + } + + // Calculate intersection of capabilities. + var params = getCommonCapabilities(localCapabilities, + remoteCapabilities); + + // Start the RTCRtpSender. The RTCRtpReceiver for this + // transceiver has already been started in setRemoteDescription. + self._transceive(transceiver, + params.codecs.length > 0, + false); + } + }); + } + + this.localDescription = { + type: description.type, + sdp: description.sdp + }; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-local-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); + } + + // If a success callback was provided, emit ICE candidates after it + // has been executed. Otherwise, emit callback after the Promise is + // resolved. + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + return new Promise(function(resolve) { + if (cb) { + cb.apply(null); + } + resolve(); + }); + }; + + RTCPeerConnection.prototype.setRemoteDescription = function(description) { + var self = this; + var args = arguments; + + if (!isActionAllowedInSignalingState('setRemoteDescription', + description.type, this.signalingState)) { + return new Promise(function(resolve, reject) { + var e = new Error('Can not set remote ' + description.type + + ' in state ' + self.signalingState); + e.name = 'InvalidStateError'; + if (args.length > 2 && typeof args[2] === 'function') { + args[2].apply(null, [e]); + } + reject(e); + }); + } + + var streams = {}; + this.remoteStreams.forEach(function(stream) { + streams[stream.id] = stream; + }); + var receiverList = []; + var sections = SDPUtils.splitSections(description.sdp); + var sessionpart = sections.shift(); + var isIceLite = SDPUtils.matchPrefix(sessionpart, + 'a=ice-lite').length > 0; + var usingBundle = SDPUtils.matchPrefix(sessionpart, + 'a=group:BUNDLE ').length > 0; + this.usingBundle = usingBundle; + var iceOptions = SDPUtils.matchPrefix(sessionpart, + 'a=ice-options:')[0]; + if (iceOptions) { + this.canTrickleIceCandidates = iceOptions.substr(14).split(' ') + .indexOf('trickle') >= 0; + } else { + this.canTrickleIceCandidates = false; + } + + sections.forEach(function(mediaSection, sdpMLineIndex) { + var lines = SDPUtils.splitLines(mediaSection); + var kind = SDPUtils.getKind(mediaSection); + // treat bundle-only as not-rejected. + var rejected = SDPUtils.isRejected(mediaSection) && + !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1; + var protocol = lines[0].substr(2).split(' ')[2]; + + var direction = SDPUtils.getDirection(mediaSection, sessionpart); + var remoteMsid = SDPUtils.parseMsid(mediaSection); + + var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier(); + + // Reject datachannels which are not implemented yet. + if (kind === 'application' && protocol === 'DTLS/SCTP') { + self.transceivers[sdpMLineIndex] = { + mid: mid, + isDatachannel: true + }; + return; + } + + var transceiver; + var iceGatherer; + var iceTransport; + var dtlsTransport; + var rtpReceiver; + var sendEncodingParameters; + var recvEncodingParameters; + var localCapabilities; + + var track; + // FIXME: ensure the mediaSection has rtcp-mux set. + var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); + var remoteIceParameters; + var remoteDtlsParameters; + if (!rejected) { + remoteIceParameters = SDPUtils.getIceParameters(mediaSection, + sessionpart); + remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, + sessionpart); + remoteDtlsParameters.role = 'client'; + } + recvEncodingParameters = + SDPUtils.parseRtpEncodingParameters(mediaSection); + + var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection); + + var isComplete = SDPUtils.matchPrefix(mediaSection, + 'a=end-of-candidates', sessionpart).length > 0; + var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') + .map(function(cand) { + return SDPUtils.parseCandidate(cand); + }) + .filter(function(cand) { + return cand.component === 1; + }); + + // Check if we can use BUNDLE and dispose transports. + if ((description.type === 'offer' || description.type === 'answer') && + !rejected && usingBundle && sdpMLineIndex > 0 && + self.transceivers[sdpMLineIndex]) { + self._disposeIceAndDtlsTransports(sdpMLineIndex); + self.transceivers[sdpMLineIndex].iceGatherer = + self.transceivers[0].iceGatherer; + self.transceivers[sdpMLineIndex].iceTransport = + self.transceivers[0].iceTransport; + self.transceivers[sdpMLineIndex].dtlsTransport = + self.transceivers[0].dtlsTransport; + if (self.transceivers[sdpMLineIndex].rtpSender) { + self.transceivers[sdpMLineIndex].rtpSender.setTransport( + self.transceivers[0].dtlsTransport); + } + if (self.transceivers[sdpMLineIndex].rtpReceiver) { + self.transceivers[sdpMLineIndex].rtpReceiver.setTransport( + self.transceivers[0].dtlsTransport); + } + } + if (description.type === 'offer' && !rejected) { + transceiver = self.transceivers[sdpMLineIndex] || + self._createTransceiver(kind); + transceiver.mid = mid; + + if (!transceiver.iceGatherer) { + transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex, + usingBundle); + } + + if (cands.length && transceiver.iceTransport.state === 'new') { + if (isComplete && (!usingBundle || sdpMLineIndex === 0)) { + transceiver.iceTransport.setRemoteCandidates(cands); + } else { + cands.forEach(function(candidate) { + maybeAddCandidate(transceiver.iceTransport, candidate); + }); + } + } + + localCapabilities = window.RTCRtpReceiver.getCapabilities(kind); + + // filter RTX until additional stuff needed for RTX is implemented + // in adapter.js + if (edgeVersion < 15019) { + localCapabilities.codecs = localCapabilities.codecs.filter( + function(codec) { + return codec.name !== 'rtx'; + }); + } + + sendEncodingParameters = transceiver.sendEncodingParameters || [{ + ssrc: (2 * sdpMLineIndex + 2) * 1001 + }]; + + var isNewTrack = false; + if (direction === 'sendrecv' || direction === 'sendonly') { + isNewTrack = !transceiver.rtpReceiver; + rtpReceiver = transceiver.rtpReceiver || + new window.RTCRtpReceiver(transceiver.dtlsTransport, kind); + + if (isNewTrack) { + var stream; + track = rtpReceiver.track; + // FIXME: does not work with Plan B. + if (remoteMsid) { + if (!streams[remoteMsid.stream]) { + streams[remoteMsid.stream] = new window.MediaStream(); + Object.defineProperty(streams[remoteMsid.stream], 'id', { + get: function() { + return remoteMsid.stream; + } + }); + } + Object.defineProperty(track, 'id', { + get: function() { + return remoteMsid.track; + } + }); + stream = streams[remoteMsid.stream]; + } else { + if (!streams.default) { + streams.default = new window.MediaStream(); + } + stream = streams.default; + } + stream.addTrack(track); + receiverList.push([track, rtpReceiver, stream]); + } + } + + transceiver.localCapabilities = localCapabilities; + transceiver.remoteCapabilities = remoteCapabilities; + transceiver.rtpReceiver = rtpReceiver; + transceiver.rtcpParameters = rtcpParameters; + transceiver.sendEncodingParameters = sendEncodingParameters; + transceiver.recvEncodingParameters = recvEncodingParameters; + + // Start the RTCRtpReceiver now. The RTPSender is started in + // setLocalDescription. + self._transceive(self.transceivers[sdpMLineIndex], + false, + isNewTrack); + } else if (description.type === 'answer' && !rejected) { + transceiver = self.transceivers[sdpMLineIndex]; + iceGatherer = transceiver.iceGatherer; + iceTransport = transceiver.iceTransport; + dtlsTransport = transceiver.dtlsTransport; + rtpReceiver = transceiver.rtpReceiver; + sendEncodingParameters = transceiver.sendEncodingParameters; + localCapabilities = transceiver.localCapabilities; + + self.transceivers[sdpMLineIndex].recvEncodingParameters = + recvEncodingParameters; + self.transceivers[sdpMLineIndex].remoteCapabilities = + remoteCapabilities; + self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; + + if (cands.length && iceTransport.state === 'new') { + if ((isIceLite || isComplete) && + (!usingBundle || sdpMLineIndex === 0)) { + iceTransport.setRemoteCandidates(cands); + } else { + cands.forEach(function(candidate) { + maybeAddCandidate(transceiver.iceTransport, candidate); + }); + } + } + + if (!usingBundle || sdpMLineIndex === 0) { + if (iceTransport.state === 'new') { + iceTransport.start(iceGatherer, remoteIceParameters, + 'controlling'); + } + if (dtlsTransport.state === 'new') { + dtlsTransport.start(remoteDtlsParameters); + } + } + + self._transceive(transceiver, + direction === 'sendrecv' || direction === 'recvonly', + direction === 'sendrecv' || direction === 'sendonly'); + + if (rtpReceiver && + (direction === 'sendrecv' || direction === 'sendonly')) { + track = rtpReceiver.track; + if (remoteMsid) { + if (!streams[remoteMsid.stream]) { + streams[remoteMsid.stream] = new window.MediaStream(); + } + streams[remoteMsid.stream].addTrack(track); + receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]); + } else { + if (!streams.default) { + streams.default = new window.MediaStream(); + } + streams.default.addTrack(track); + receiverList.push([track, rtpReceiver, streams.default]); + } + } else { + // FIXME: actually the receiver should be created later. + delete transceiver.rtpReceiver; + } + } + }); + + if (this._dtlsRole === undefined) { + this._dtlsRole = description.type === 'offer' ? 'active' : 'passive'; + } + + this.remoteDescription = { + type: description.type, + sdp: description.sdp + }; + switch (description.type) { + case 'offer': + this._updateSignalingState('have-remote-offer'); + break; + case 'answer': + this._updateSignalingState('stable'); + break; + default: + throw new TypeError('unsupported type "' + description.type + + '"'); + } + Object.keys(streams).forEach(function(sid) { + var stream = streams[sid]; + if (stream.getTracks().length) { + if (self.remoteStreams.indexOf(stream) === -1) { + self.remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = stream; + window.setTimeout(function() { + self.dispatchEvent(event); + if (typeof self.onaddstream === 'function') { + self.onaddstream(event); + } + }); + } + + receiverList.forEach(function(item) { + var track = item[0]; + var receiver = item[1]; + if (stream.id !== item[2].id) { + return; + } + var trackEvent = new Event('track'); + trackEvent.track = track; + trackEvent.receiver = receiver; + trackEvent.transceiver = {receiver: receiver}; + trackEvent.streams = [stream]; + window.setTimeout(function() { + self.dispatchEvent(trackEvent); + if (typeof self.ontrack === 'function') { + self.ontrack(trackEvent); + } + }); + }); + } + }); + + // check whether addIceCandidate({}) was called within four seconds after + // setRemoteDescription. + window.setTimeout(function() { + if (!(self && self.transceivers)) { + return; + } + self.transceivers.forEach(function(transceiver) { + if (transceiver.iceTransport && + transceiver.iceTransport.state === 'new' && + transceiver.iceTransport.getRemoteCandidates().length > 0) { + console.warn('Timeout for addRemoteCandidate. Consider sending ' + + 'an end-of-candidates notification'); + transceiver.iceTransport.addRemoteCandidate({}); + } + }); + }, 4000); + + return new Promise(function(resolve) { + if (args.length > 1 && typeof args[1] === 'function') { + args[1].apply(null); + } + resolve(); + }); + }; + + RTCPeerConnection.prototype.close = function() { + this.transceivers.forEach(function(transceiver) { + /* not yet + if (transceiver.iceGatherer) { + transceiver.iceGatherer.close(); + } + */ + if (transceiver.iceTransport) { + transceiver.iceTransport.stop(); + } + if (transceiver.dtlsTransport) { + transceiver.dtlsTransport.stop(); + } + if (transceiver.rtpSender) { + transceiver.rtpSender.stop(); + } + if (transceiver.rtpReceiver) { + transceiver.rtpReceiver.stop(); + } + }); + // FIXME: clean up tracks, local streams, remote streams, etc + this._updateSignalingState('closed'); + }; + + // Update the signaling state. + RTCPeerConnection.prototype._updateSignalingState = function(newState) { + this.signalingState = newState; + var event = new Event('signalingstatechange'); + this.dispatchEvent(event); + if (typeof this.onsignalingstatechange === 'function') { + this.onsignalingstatechange(event); + } + }; + + // Determine whether to fire the negotiationneeded event. + RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() { + var self = this; + if (this.signalingState !== 'stable' || this.needNegotiation === true) { + return; + } + this.needNegotiation = true; + window.setTimeout(function() { + if (self.needNegotiation === false) { + return; + } + self.needNegotiation = false; + var event = new Event('negotiationneeded'); + self.dispatchEvent(event); + if (typeof self.onnegotiationneeded === 'function') { + self.onnegotiationneeded(event); + } + }, 0); + }; + + // Update the connection state. + RTCPeerConnection.prototype._updateConnectionState = function() { + var newState; + var states = { + 'new': 0, + closed: 0, + connecting: 0, + checking: 0, + connected: 0, + completed: 0, + disconnected: 0, + failed: 0 + }; + this.transceivers.forEach(function(transceiver) { + states[transceiver.iceTransport.state]++; + states[transceiver.dtlsTransport.state]++; + }); + // ICETransport.completed and connected are the same for this purpose. + states.connected += states.completed; + + newState = 'new'; + if (states.failed > 0) { + newState = 'failed'; + } else if (states.connecting > 0 || states.checking > 0) { + newState = 'connecting'; + } else if (states.disconnected > 0) { + newState = 'disconnected'; + } else if (states.new > 0) { + newState = 'new'; + } else if (states.connected > 0 || states.completed > 0) { + newState = 'connected'; + } + + if (newState !== this.iceConnectionState) { + this.iceConnectionState = newState; + var event = new Event('iceconnectionstatechange'); + this.dispatchEvent(event); + if (typeof this.oniceconnectionstatechange === 'function') { + this.oniceconnectionstatechange(event); + } + } + }; + + RTCPeerConnection.prototype.createOffer = function() { + var self = this; + var args = arguments; + + var offerOptions; + if (arguments.length === 1 && typeof arguments[0] !== 'function') { + offerOptions = arguments[0]; + } else if (arguments.length === 3) { + offerOptions = arguments[2]; + } + + var numAudioTracks = this.transceivers.filter(function(t) { + return t.kind === 'audio'; + }).length; + var numVideoTracks = this.transceivers.filter(function(t) { + return t.kind === 'video'; + }).length; + + // Determine number of audio and video tracks we need to send/recv. + if (offerOptions) { + // Reject Chrome legacy constraints. + if (offerOptions.mandatory || offerOptions.optional) { + throw new TypeError( + 'Legacy mandatory/optional constraints not supported.'); + } + if (offerOptions.offerToReceiveAudio !== undefined) { + if (offerOptions.offerToReceiveAudio === true) { + numAudioTracks = 1; + } else if (offerOptions.offerToReceiveAudio === false) { + numAudioTracks = 0; + } else { + numAudioTracks = offerOptions.offerToReceiveAudio; + } + } + if (offerOptions.offerToReceiveVideo !== undefined) { + if (offerOptions.offerToReceiveVideo === true) { + numVideoTracks = 1; + } else if (offerOptions.offerToReceiveVideo === false) { + numVideoTracks = 0; + } else { + numVideoTracks = offerOptions.offerToReceiveVideo; + } + } + } + + this.transceivers.forEach(function(transceiver) { + if (transceiver.kind === 'audio') { + numAudioTracks--; + if (numAudioTracks < 0) { + transceiver.wantReceive = false; + } + } else if (transceiver.kind === 'video') { + numVideoTracks--; + if (numVideoTracks < 0) { + transceiver.wantReceive = false; + } + } + }); + + // Create M-lines for recvonly streams. + while (numAudioTracks > 0 || numVideoTracks > 0) { + if (numAudioTracks > 0) { + this._createTransceiver('audio'); + numAudioTracks--; + } + if (numVideoTracks > 0) { + this._createTransceiver('video'); + numVideoTracks--; + } + } + + var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId, + this._sdpSessionVersion++); + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { + // For each track, create an ice gatherer, ice transport, + // dtls transport, potentially rtpsender and rtpreceiver. + var track = transceiver.track; + var kind = transceiver.kind; + var mid = SDPUtils.generateIdentifier(); + transceiver.mid = mid; + + if (!transceiver.iceGatherer) { + transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex, + self.usingBundle); + } + + var localCapabilities = window.RTCRtpSender.getCapabilities(kind); + // filter RTX until additional stuff needed for RTX is implemented + // in adapter.js + if (edgeVersion < 15019) { + localCapabilities.codecs = localCapabilities.codecs.filter( + function(codec) { + return codec.name !== 'rtx'; + }); + } + localCapabilities.codecs.forEach(function(codec) { + // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552 + // by adding level-asymmetry-allowed=1 + if (codec.name === 'H264' && + codec.parameters['level-asymmetry-allowed'] === undefined) { + codec.parameters['level-asymmetry-allowed'] = '1'; + } + }); + + // generate an ssrc now, to be used later in rtpSender.send + var sendEncodingParameters = transceiver.sendEncodingParameters || [{ + ssrc: (2 * sdpMLineIndex + 1) * 1001 + }]; + if (track) { + // add RTX + if (edgeVersion >= 15019 && kind === 'video' && + !sendEncodingParameters[0].rtx) { + sendEncodingParameters[0].rtx = { + ssrc: sendEncodingParameters[0].ssrc + 1 + }; + } + } + + if (transceiver.wantReceive) { + transceiver.rtpReceiver = new window.RTCRtpReceiver( + transceiver.dtlsTransport, kind); + } + + transceiver.localCapabilities = localCapabilities; + transceiver.sendEncodingParameters = sendEncodingParameters; + }); + + // always offer BUNDLE and dispose on return if not supported. + if (this._config.bundlePolicy !== 'max-compat') { + sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + sdp += 'a=ice-options:trickle\r\n'; + + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { + sdp += writeMediaSection(transceiver, transceiver.localCapabilities, + 'offer', transceiver.stream, self._dtlsRole); + sdp += 'a=rtcp-rsize\r\n'; + + if (transceiver.iceGatherer && self.iceGatheringState !== 'new' && + (sdpMLineIndex === 0 || !self.usingBundle)) { + transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) { + cand.component = 1; + sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n'; + }); + + if (transceiver.iceGatherer.state === 'completed') { + sdp += 'a=end-of-candidates\r\n'; + } + } + }); + + var desc = new window.RTCSessionDescription({ + type: 'offer', + sdp: sdp + }); + return new Promise(function(resolve) { + if (args.length > 0 && typeof args[0] === 'function') { + args[0].apply(null, [desc]); + resolve(); + return; + } + resolve(desc); + }); + }; + + RTCPeerConnection.prototype.createAnswer = function() { + var self = this; + var args = arguments; + + var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId, + this._sdpSessionVersion++); + if (this.usingBundle) { + sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { + return t.mid; + }).join(' ') + '\r\n'; + } + var mediaSectionsInOffer = SDPUtils.splitSections( + this.remoteDescription.sdp).length - 1; + this.transceivers.forEach(function(transceiver, sdpMLineIndex) { + if (sdpMLineIndex + 1 > mediaSectionsInOffer) { + return; + } + if (transceiver.isDatachannel) { + sdp += 'm=application 0 DTLS/SCTP 5000\r\n' + + 'c=IN IP4 0.0.0.0\r\n' + + 'a=mid:' + transceiver.mid + '\r\n'; + return; + } + + // FIXME: look at direction. + if (transceiver.stream) { + var localTrack; + if (transceiver.kind === 'audio') { + localTrack = transceiver.stream.getAudioTracks()[0]; + } else if (transceiver.kind === 'video') { + localTrack = transceiver.stream.getVideoTracks()[0]; + } + if (localTrack) { + // add RTX + if (edgeVersion >= 15019 && transceiver.kind === 'video' && + !transceiver.sendEncodingParameters[0].rtx) { + transceiver.sendEncodingParameters[0].rtx = { + ssrc: transceiver.sendEncodingParameters[0].ssrc + 1 + }; + } + } + } + + // Calculate intersection of capabilities. + var commonCapabilities = getCommonCapabilities( + transceiver.localCapabilities, + transceiver.remoteCapabilities); + + var hasRtx = commonCapabilities.codecs.filter(function(c) { + return c.name.toLowerCase() === 'rtx'; + }).length; + if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { + delete transceiver.sendEncodingParameters[0].rtx; + } + + sdp += writeMediaSection(transceiver, commonCapabilities, + 'answer', transceiver.stream, self._dtlsRole); + if (transceiver.rtcpParameters && + transceiver.rtcpParameters.reducedSize) { + sdp += 'a=rtcp-rsize\r\n'; + } + }); + + var desc = new window.RTCSessionDescription({ + type: 'answer', + sdp: sdp + }); + return new Promise(function(resolve) { + if (args.length > 0 && typeof args[0] === 'function') { + args[0].apply(null, [desc]); + resolve(); + return; + } + resolve(desc); + }); + }; + + RTCPeerConnection.prototype.addIceCandidate = function(candidate) { + var err; + var sections; + if (!candidate || candidate.candidate === '') { + for (var j = 0; j < this.transceivers.length; j++) { + if (this.transceivers[j].isDatachannel) { + continue; + } + this.transceivers[j].iceTransport.addRemoteCandidate({}); + sections = SDPUtils.splitSections(this.remoteDescription.sdp); + sections[j + 1] += 'a=end-of-candidates\r\n'; + this.remoteDescription.sdp = sections.join(''); + if (this.usingBundle) { + break; + } + } + } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) { + throw new TypeError('sdpMLineIndex or sdpMid required'); + } else if (!this.remoteDescription) { + err = new Error('Can not add ICE candidate without ' + + 'a remote description'); + err.name = 'InvalidStateError'; + } else { + var sdpMLineIndex = candidate.sdpMLineIndex; + if (candidate.sdpMid) { + for (var i = 0; i < this.transceivers.length; i++) { + if (this.transceivers[i].mid === candidate.sdpMid) { + sdpMLineIndex = i; + break; + } + } + } + var transceiver = this.transceivers[sdpMLineIndex]; + if (transceiver) { + if (transceiver.isDatachannel) { + return Promise.resolve(); + } + var cand = Object.keys(candidate.candidate).length > 0 ? + SDPUtils.parseCandidate(candidate.candidate) : {}; + // Ignore Chrome's invalid candidates since Edge does not like them. + if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { + return Promise.resolve(); + } + // Ignore RTCP candidates, we assume RTCP-MUX. + if (cand.component && cand.component !== 1) { + return Promise.resolve(); + } + // when using bundle, avoid adding candidates to the wrong + // ice transport. And avoid adding candidates added in the SDP. + if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 && + transceiver.iceTransport !== this.transceivers[0].iceTransport)) { + if (!maybeAddCandidate(transceiver.iceTransport, cand)) { + err = new Error('Can not add ICE candidate'); + err.name = 'OperationError'; + } + } + + if (!err) { + // update the remoteDescription. + var candidateString = candidate.candidate.trim(); + if (candidateString.indexOf('a=') === 0) { + candidateString = candidateString.substr(2); + } + sections = SDPUtils.splitSections(this.remoteDescription.sdp); + sections[sdpMLineIndex + 1] += 'a=' + + (cand.type ? candidateString : 'end-of-candidates') + + '\r\n'; + this.remoteDescription.sdp = sections.join(''); + } + } else { + err = new Error('Can not add ICE candidate'); + err.name = 'OperationError'; + } + } + var args = arguments; + return new Promise(function(resolve, reject) { + if (err) { + if (args.length > 2 && typeof args[2] === 'function') { + args[2].apply(null, [err]); + } + reject(err); + } else { + if (args.length > 1 && typeof args[1] === 'function') { + args[1].apply(null); + } + resolve(); + } + }); + }; + + RTCPeerConnection.prototype.getStats = function() { + var promises = []; + this.transceivers.forEach(function(transceiver) { + ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', + 'dtlsTransport'].forEach(function(method) { + if (transceiver[method]) { + promises.push(transceiver[method].getStats()); + } + }); + }); + var cb = arguments.length > 1 && typeof arguments[1] === 'function' && + arguments[1]; + var fixStatsType = function(stat) { + return { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[stat.type] || stat.type; + }; + return new Promise(function(resolve) { + // shim getStats with maplike support + var results = new Map(); + Promise.all(promises).then(function(res) { + res.forEach(function(result) { + Object.keys(result).forEach(function(id) { + result[id].type = fixStatsType(result[id]); + results.set(id, result[id]); + }); + }); + if (cb) { + cb.apply(null, results); + } + resolve(results); + }); + }); + }; + return RTCPeerConnection; +}; + +},{"sdp":2}],2:[function(require,module,exports){ + /* eslint-env node */ +'use strict'; + +// SDP helpers. +var SDPUtils = {}; + +// Generate an alphanumeric identifier for cname or mids. +// TODO: use UUIDs instead? https://gist.github.com/jed/982883 +SDPUtils.generateIdentifier = function() { + return Math.random().toString(36).substr(2, 10); +}; + +// The RTCP CNAME used by all peerconnections from the same JS. +SDPUtils.localCName = SDPUtils.generateIdentifier(); + +// Splits SDP into lines, dealing with both CRLF and LF. +SDPUtils.splitLines = function(blob) { + return blob.trim().split('\n').map(function(line) { + return line.trim(); + }); +}; +// Splits SDP into sessionpart and mediasections. Ensures CRLF. +SDPUtils.splitSections = function(blob) { + var parts = blob.split('\nm='); + return parts.map(function(part, index) { + return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; + }); +}; + +// Returns lines that start with a certain prefix. +SDPUtils.matchPrefix = function(blob, prefix) { + return SDPUtils.splitLines(blob).filter(function(line) { + return line.indexOf(prefix) === 0; + }); +}; + +// Parses an ICE candidate line. Sample input: +// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 +// rport 55996" +SDPUtils.parseCandidate = function(line) { + var parts; + // Parse both variants. + if (line.indexOf('a=candidate:') === 0) { + parts = line.substring(12).split(' '); + } else { + parts = line.substring(10).split(' '); + } + + var candidate = { + foundation: parts[0], + component: parseInt(parts[1], 10), + protocol: parts[2].toLowerCase(), + priority: parseInt(parts[3], 10), + ip: parts[4], + port: parseInt(parts[5], 10), + // skip parts[6] == 'typ' + type: parts[7] + }; + + for (var i = 8; i < parts.length; i += 2) { + switch (parts[i]) { + case 'raddr': + candidate.relatedAddress = parts[i + 1]; + break; + case 'rport': + candidate.relatedPort = parseInt(parts[i + 1], 10); + break; + case 'tcptype': + candidate.tcpType = parts[i + 1]; + break; + case 'ufrag': + candidate.ufrag = parts[i + 1]; // for backward compability. + candidate.usernameFragment = parts[i + 1]; + break; + default: // extension handling, in particular ufrag + candidate[parts[i]] = parts[i + 1]; + break; + } + } + return candidate; +}; + +// Translates a candidate object into SDP candidate attribute. +SDPUtils.writeCandidate = function(candidate) { + var sdp = []; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type !== 'host' && candidate.relatedAddress && + candidate.relatedPort) { + sdp.push('raddr'); + sdp.push(candidate.relatedAddress); // was: relAddr + sdp.push('rport'); + sdp.push(candidate.relatedPort); // was: relPort + } + if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + if (candidate.ufrag) { + sdp.push('ufrag'); + sdp.push(candidate.ufrag); + } + return 'candidate:' + sdp.join(' '); +}; + +// Parses an ice-options line, returns an array of option tags. +// a=ice-options:foo bar +SDPUtils.parseIceOptions = function(line) { + return line.substr(14).split(' '); +} + +// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: +// a=rtpmap:111 opus/48000/2 +SDPUtils.parseRtpMap = function(line) { + var parts = line.substr(9).split(' '); + var parsed = { + payloadType: parseInt(parts.shift(), 10) // was: id + }; + + parts = parts[0].split('/'); + + parsed.name = parts[0]; + parsed.clockRate = parseInt(parts[1], 10); // was: clockrate + // was: channels + parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; + return parsed; +}; + +// Generate an a=rtpmap line from RTCRtpCodecCapability or +// RTCRtpCodecParameters. +SDPUtils.writeRtpMap = function(codec) { + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + + (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; +}; + +// Parses an a=extmap line (headerextension from RFC 5285). Sample input: +// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset +// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset +SDPUtils.parseExtmap = function(line) { + var parts = line.substr(9).split(' '); + return { + id: parseInt(parts[0], 10), + direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', + uri: parts[1] + }; +}; + +// Generates a=extmap line from RTCRtpHeaderExtensionParameters or +// RTCRtpHeaderExtension. +SDPUtils.writeExtmap = function(headerExtension) { + return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + + (headerExtension.direction && headerExtension.direction !== 'sendrecv' + ? '/' + headerExtension.direction + : '') + + ' ' + headerExtension.uri + '\r\n'; +}; + +// Parses an ftmp line, returns dictionary. Sample input: +// a=fmtp:96 vbr=on;cng=on +// Also deals with vbr=on; cng=on +SDPUtils.parseFmtp = function(line) { + var parsed = {}; + var kv; + var parts = line.substr(line.indexOf(' ') + 1).split(';'); + for (var j = 0; j < parts.length; j++) { + kv = parts[j].trim().split('='); + parsed[kv[0].trim()] = kv[1]; + } + return parsed; +}; + +// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeFmtp = function(codec) { + var line = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.parameters && Object.keys(codec.parameters).length) { + var params = []; + Object.keys(codec.parameters).forEach(function(param) { + params.push(param + '=' + codec.parameters[param]); + }); + line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; + } + return line; +}; + +// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: +// a=rtcp-fb:98 nack rpsi +SDPUtils.parseRtcpFb = function(line) { + var parts = line.substr(line.indexOf(' ') + 1).split(' '); + return { + type: parts.shift(), + parameter: parts.join(' ') + }; +}; +// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. +SDPUtils.writeRtcpFb = function(codec) { + var lines = ''; + var pt = codec.payloadType; + if (codec.preferredPayloadType !== undefined) { + pt = codec.preferredPayloadType; + } + if (codec.rtcpFeedback && codec.rtcpFeedback.length) { + // FIXME: special handling for trr-int? + codec.rtcpFeedback.forEach(function(fb) { + lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + + '\r\n'; + }); + } + return lines; +}; + +// Parses an RFC 5576 ssrc media attribute. Sample input: +// a=ssrc:3735928559 cname:something +SDPUtils.parseSsrcMedia = function(line) { + var sp = line.indexOf(' '); + var parts = { + ssrc: parseInt(line.substr(7, sp - 7), 10) + }; + var colon = line.indexOf(':', sp); + if (colon > -1) { + parts.attribute = line.substr(sp + 1, colon - sp - 1); + parts.value = line.substr(colon + 1); + } else { + parts.attribute = line.substr(sp + 1); + } + return parts; +}; + +// Extracts the MID (RFC 5888) from a media section. +// returns the MID or undefined if no mid line was found. +SDPUtils.getMid = function(mediaSection) { + var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; + if (mid) { + return mid.substr(6); + } +} + +SDPUtils.parseFingerprint = function(line) { + var parts = line.substr(14).split(' '); + return { + algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. + value: parts[1] + }; +}; + +// Extracts DTLS parameters from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the fingerprint line as input. See also getIceParameters. +SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, + 'a=fingerprint:'); + // Note: a=setup line is ignored since we use the 'auto' role. + // Note2: 'algorithm' is not case sensitive except in Edge. + return { + role: 'auto', + fingerprints: lines.map(SDPUtils.parseFingerprint) + }; +}; + +// Serializes DTLS parameters to SDP. +SDPUtils.writeDtlsParameters = function(params, setupType) { + var sdp = 'a=setup:' + setupType + '\r\n'; + params.fingerprints.forEach(function(fp) { + sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; + }); + return sdp; +}; +// Parses ICE information from SDP media section or sessionpart. +// FIXME: for consistency with other functions this should only +// get the ice-ufrag and ice-pwd lines as input. +SDPUtils.getIceParameters = function(mediaSection, sessionpart) { + var lines = SDPUtils.splitLines(mediaSection); + // Search in session part, too. + lines = lines.concat(SDPUtils.splitLines(sessionpart)); + var iceParameters = { + usernameFragment: lines.filter(function(line) { + return line.indexOf('a=ice-ufrag:') === 0; + })[0].substr(12), + password: lines.filter(function(line) { + return line.indexOf('a=ice-pwd:') === 0; + })[0].substr(10) + }; + return iceParameters; +}; + +// Serializes ICE parameters to SDP. +SDPUtils.writeIceParameters = function(params) { + return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + + 'a=ice-pwd:' + params.password + '\r\n'; +}; + +// Parses the SDP media section and returns RTCRtpParameters. +SDPUtils.parseRtpParameters = function(mediaSection) { + var description = { + codecs: [], + headerExtensions: [], + fecMechanisms: [], + rtcp: [] + }; + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] + var pt = mline[i]; + var rtpmapline = SDPUtils.matchPrefix( + mediaSection, 'a=rtpmap:' + pt + ' ')[0]; + if (rtpmapline) { + var codec = SDPUtils.parseRtpMap(rtpmapline); + var fmtps = SDPUtils.matchPrefix( + mediaSection, 'a=fmtp:' + pt + ' '); + // Only the first a=fmtp:<pt> is considered. + codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; + codec.rtcpFeedback = SDPUtils.matchPrefix( + mediaSection, 'a=rtcp-fb:' + pt + ' ') + .map(SDPUtils.parseRtcpFb); + description.codecs.push(codec); + // parse FEC mechanisms from rtpmap lines. + switch (codec.name.toUpperCase()) { + case 'RED': + case 'ULPFEC': + description.fecMechanisms.push(codec.name.toUpperCase()); + break; + default: // only RED and ULPFEC are recognized as FEC mechanisms. + break; + } + } + } + SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { + description.headerExtensions.push(SDPUtils.parseExtmap(line)); + }); + // FIXME: parse rtcp. + return description; +}; + +// Generates parts of the SDP media section describing the capabilities / +// parameters. +SDPUtils.writeRtpDescription = function(kind, caps) { + var sdp = ''; + + // Build the mline. + sdp += 'm=' + kind + ' '; + sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. + sdp += ' UDP/TLS/RTP/SAVPF '; + sdp += caps.codecs.map(function(codec) { + if (codec.preferredPayloadType !== undefined) { + return codec.preferredPayloadType; + } + return codec.payloadType; + }).join(' ') + '\r\n'; + + sdp += 'c=IN IP4 0.0.0.0\r\n'; + sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; + + // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. + caps.codecs.forEach(function(codec) { + sdp += SDPUtils.writeRtpMap(codec); + sdp += SDPUtils.writeFmtp(codec); + sdp += SDPUtils.writeRtcpFb(codec); + }); + var maxptime = 0; + caps.codecs.forEach(function(codec) { + if (codec.maxptime > maxptime) { + maxptime = codec.maxptime; + } + }); + if (maxptime > 0) { + sdp += 'a=maxptime:' + maxptime + '\r\n'; + } + sdp += 'a=rtcp-mux\r\n'; + + caps.headerExtensions.forEach(function(extension) { + sdp += SDPUtils.writeExtmap(extension); + }); + // FIXME: write fecMechanisms. + return sdp; +}; + +// Parses the SDP media section and returns an array of +// RTCRtpEncodingParameters. +SDPUtils.parseRtpEncodingParameters = function(mediaSection) { + var encodingParameters = []; + var description = SDPUtils.parseRtpParameters(mediaSection); + var hasRed = description.fecMechanisms.indexOf('RED') !== -1; + var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; + + // filter a=ssrc:... cname:, ignore PlanB-msid + var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'cname'; + }); + var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; + var secondarySsrc; + + var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') + .map(function(line) { + var parts = line.split(' '); + parts.shift(); + return parts.map(function(part) { + return parseInt(part, 10); + }); + }); + if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { + secondarySsrc = flows[0][1]; + } + + description.codecs.forEach(function(codec) { + if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { + var encParam = { + ssrc: primarySsrc, + codecPayloadType: parseInt(codec.parameters.apt, 10), + rtx: { + ssrc: secondarySsrc + } + }; + encodingParameters.push(encParam); + if (hasRed) { + encParam = JSON.parse(JSON.stringify(encParam)); + encParam.fec = { + ssrc: secondarySsrc, + mechanism: hasUlpfec ? 'red+ulpfec' : 'red' + }; + encodingParameters.push(encParam); + } + } + }); + if (encodingParameters.length === 0 && primarySsrc) { + encodingParameters.push({ + ssrc: primarySsrc + }); + } + + // we support both b=AS and b=TIAS but interpret AS as TIAS. + var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); + if (bandwidth.length) { + if (bandwidth[0].indexOf('b=TIAS:') === 0) { + bandwidth = parseInt(bandwidth[0].substr(7), 10); + } else if (bandwidth[0].indexOf('b=AS:') === 0) { + // use formula from JSEP to convert b=AS to TIAS value. + bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 + - (50 * 40 * 8); + } else { + bandwidth = undefined; + } + encodingParameters.forEach(function(params) { + params.maxBitrate = bandwidth; + }); + } + return encodingParameters; +}; + +// parses http://draft.ortc.org/#rtcrtcpparameters* +SDPUtils.parseRtcpParameters = function(mediaSection) { + var rtcpParameters = {}; + + var cname; + // Gets the first SSRC. Note that with RTX there might be multiple + // SSRCs. + var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(obj) { + return obj.attribute === 'cname'; + })[0]; + if (remoteSsrc) { + rtcpParameters.cname = remoteSsrc.value; + rtcpParameters.ssrc = remoteSsrc.ssrc; + } + + // Edge uses the compound attribute instead of reducedSize + // compound is !reducedSize + var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); + rtcpParameters.reducedSize = rsize.length > 0; + rtcpParameters.compound = rsize.length === 0; + + // parses the rtcp-mux attrіbute. + // Note that Edge does not support unmuxed RTCP. + var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); + rtcpParameters.mux = mux.length > 0; + + return rtcpParameters; +}; + +// parses either a=msid: or a=ssrc:... msid lines and returns +// the id of the MediaStream and MediaStreamTrack. +SDPUtils.parseMsid = function(mediaSection) { + var parts; + var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); + if (spec.length === 1) { + parts = spec[0].substr(7).split(' '); + return {stream: parts[0], track: parts[1]}; + } + var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') + .map(function(line) { + return SDPUtils.parseSsrcMedia(line); + }) + .filter(function(parts) { + return parts.attribute === 'msid'; + }); + if (planB.length > 0) { + parts = planB[0].value.split(' '); + return {stream: parts[0], track: parts[1]}; + } +}; + +// Generate a session ID for SDP. +// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 +// recommends using a cryptographically random +ve 64-bit value +// but right now this should be acceptable and within the right range +SDPUtils.generateSessionId = function() { + return Math.random().toString().substr(2, 21); +}; + +// Write boilder plate for start of SDP +// sessId argument is optional - if not supplied it will +// be generated randomly +// sessVersion is optional and defaults to 2 +SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) { + var sessionId; + var version = sessVer !== undefined ? sessVer : 2; + if (sessId) { + sessionId = sessId; + } else { + sessionId = SDPUtils.generateSessionId(); + } + // FIXME: sess-id should be an NTP timestamp. + return 'v=0\r\n' + + 'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + + 's=-\r\n' + + 't=0 0\r\n'; +}; + +SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { + var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); + + // Map ICE parameters (ufrag, pwd) to SDP. + sdp += SDPUtils.writeIceParameters( + transceiver.iceGatherer.getLocalParameters()); + + // Map DTLS parameters to SDP. + sdp += SDPUtils.writeDtlsParameters( + transceiver.dtlsTransport.getLocalParameters(), + type === 'offer' ? 'actpass' : 'active'); + + sdp += 'a=mid:' + transceiver.mid + '\r\n'; + + if (transceiver.direction) { + sdp += 'a=' + transceiver.direction + '\r\n'; + } else if (transceiver.rtpSender && transceiver.rtpReceiver) { + sdp += 'a=sendrecv\r\n'; + } else if (transceiver.rtpSender) { + sdp += 'a=sendonly\r\n'; + } else if (transceiver.rtpReceiver) { + sdp += 'a=recvonly\r\n'; + } else { + sdp += 'a=inactive\r\n'; + } + + if (transceiver.rtpSender) { + // spec. + var msid = 'msid:' + stream.id + ' ' + + transceiver.rtpSender.track.id + '\r\n'; + sdp += 'a=' + msid; + + // for Chrome. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' ' + msid; + if (transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' ' + msid; + sdp += 'a=ssrc-group:FID ' + + transceiver.sendEncodingParameters[0].ssrc + ' ' + + transceiver.sendEncodingParameters[0].rtx.ssrc + + '\r\n'; + } + } + // FIXME: this should be written by writeRtpDescription. + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { + sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + + ' cname:' + SDPUtils.localCName + '\r\n'; + } + return sdp; +}; + +// Gets the direction from the mediaSection or the sessionpart. +SDPUtils.getDirection = function(mediaSection, sessionpart) { + // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. + var lines = SDPUtils.splitLines(mediaSection); + for (var i = 0; i < lines.length; i++) { + switch (lines[i]) { + case 'a=sendrecv': + case 'a=sendonly': + case 'a=recvonly': + case 'a=inactive': + return lines[i].substr(2); + default: + // FIXME: What should happen here? + } + } + if (sessionpart) { + return SDPUtils.getDirection(sessionpart); + } + return 'sendrecv'; +}; + +SDPUtils.getKind = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + return mline[0].substr(2); +}; + +SDPUtils.isRejected = function(mediaSection) { + return mediaSection.split(' ', 2)[1] === '0'; +}; + +SDPUtils.parseMLine = function(mediaSection) { + var lines = SDPUtils.splitLines(mediaSection); + var mline = lines[0].split(' '); + return { + kind: mline[0].substr(2), + port: parseInt(mline[1], 10), + protocol: mline[2], + fmt: mline.slice(3).join(' ') + }; +}; + +// Expose public methods. +if (typeof module === 'object') { + module.exports = SDPUtils; +} + +},{}],3:[function(require,module,exports){ +(function (global){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ + +'use strict'; + +var adapterFactory = require('./adapter_factory.js'); +module.exports = adapterFactory({window: global.window}); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./adapter_factory.js":4}],4:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ + +'use strict'; + +var utils = require('./utils'); +// Shimming starts here. +module.exports = function(dependencies, opts) { + var window = dependencies && dependencies.window; + + var options = { + shimChrome: true, + shimFirefox: true, + shimEdge: true, + shimSafari: true, + }; + + for (var key in opts) { + if (hasOwnProperty.call(opts, key)) { + options[key] = opts[key]; + } + } + + // Utils. + var logging = utils.log; + var browserDetails = utils.detectBrowser(window); + + // Export to the adapter global object visible in the browser. + var adapter = { + browserDetails: browserDetails, + extractVersion: utils.extractVersion, + disableLog: utils.disableLog, + disableWarnings: utils.disableWarnings + }; + + // Uncomment the line below if you want logging to occur, including logging + // for the switch statement below. Can also be turned on in the browser via + // adapter.disableLog(false), but then logging from the switch statement below + // will not appear. + // require('./utils').disableLog(false); + + // Browser shims. + var chromeShim = require('./chrome/chrome_shim') || null; + var edgeShim = require('./edge/edge_shim') || null; + var firefoxShim = require('./firefox/firefox_shim') || null; + var safariShim = require('./safari/safari_shim') || null; + var commonShim = require('./common_shim') || null; + + // Shim browser if found. + switch (browserDetails.browser) { + case 'chrome': + if (!chromeShim || !chromeShim.shimPeerConnection || + !options.shimChrome) { + logging('Chrome shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming chrome.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = chromeShim; + commonShim.shimCreateObjectURL(window); + + chromeShim.shimGetUserMedia(window); + chromeShim.shimMediaStream(window); + chromeShim.shimSourceObject(window); + chromeShim.shimPeerConnection(window); + chromeShim.shimOnTrack(window); + chromeShim.shimAddTrackRemoveTrack(window); + chromeShim.shimGetSendersWithDtmf(window); + + commonShim.shimRTCIceCandidate(window); + break; + case 'firefox': + if (!firefoxShim || !firefoxShim.shimPeerConnection || + !options.shimFirefox) { + logging('Firefox shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming firefox.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = firefoxShim; + commonShim.shimCreateObjectURL(window); + + firefoxShim.shimGetUserMedia(window); + firefoxShim.shimSourceObject(window); + firefoxShim.shimPeerConnection(window); + firefoxShim.shimOnTrack(window); + + commonShim.shimRTCIceCandidate(window); + break; + case 'edge': + if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) { + logging('MS edge shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming edge.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = edgeShim; + commonShim.shimCreateObjectURL(window); + + edgeShim.shimGetUserMedia(window); + edgeShim.shimPeerConnection(window); + edgeShim.shimReplaceTrack(window); + + // the edge shim implements the full RTCIceCandidate object. + break; + case 'safari': + if (!safariShim || !options.shimSafari) { + logging('Safari shim is not included in this adapter release.'); + return adapter; + } + logging('adapter.js shimming safari.'); + // Export to the adapter global object visible in the browser. + adapter.browserShim = safariShim; + commonShim.shimCreateObjectURL(window); + + safariShim.shimRTCIceServerUrls(window); + safariShim.shimCallbacksAPI(window); + safariShim.shimLocalStreamsAPI(window); + safariShim.shimRemoteStreamsAPI(window); + safariShim.shimTrackEventTransceiver(window); + safariShim.shimGetUserMedia(window); + safariShim.shimCreateOfferLegacy(window); + + commonShim.shimRTCIceCandidate(window); + break; + default: + logging('Unsupported browser!'); + break; + } + + return adapter; +}; + +},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],5:[function(require,module,exports){ + +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; +var utils = require('../utils.js'); +var logging = utils.log; + +var chromeShim = { + shimMediaStream: function(window) { + window.MediaStream = window.MediaStream || window.webkitMediaStream; + }, + + shimOnTrack: function(window) { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function() { + return this._ontrack; + }, + set: function(f) { + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + } + this.addEventListener('track', this._ontrack = f); + } + }); + var origSetRemoteDescription = + window.RTCPeerConnection.prototype.setRemoteDescription; + window.RTCPeerConnection.prototype.setRemoteDescription = function() { + var pc = this; + if (!pc._ontrackpoly) { + pc._ontrackpoly = function(e) { + // onaddstream does not fire when a track is added to an existing + // stream. But stream.onaddtrack is implemented so we use that. + e.stream.addEventListener('addtrack', function(te) { + var receiver; + if (window.RTCPeerConnection.prototype.getReceivers) { + receiver = pc.getReceivers().find(function(r) { + return r.track && r.track.id === te.track.id; + }); + } else { + receiver = {track: te.track}; + } + + var event = new Event('track'); + event.track = te.track; + event.receiver = receiver; + event.transceiver = {receiver: receiver}; + event.streams = [e.stream]; + pc.dispatchEvent(event); + }); + e.stream.getTracks().forEach(function(track) { + var receiver; + if (window.RTCPeerConnection.prototype.getReceivers) { + receiver = pc.getReceivers().find(function(r) { + return r.track && r.track.id === track.id; + }); + } else { + receiver = {track: track}; + } + var event = new Event('track'); + event.track = track; + event.receiver = receiver; + event.transceiver = {receiver: receiver}; + event.streams = [e.stream]; + pc.dispatchEvent(event); + }); + }; + pc.addEventListener('addstream', pc._ontrackpoly); + } + return origSetRemoteDescription.apply(pc, arguments); + }; + } + }, + + shimGetSendersWithDtmf: function(window) { + // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. + if (typeof window === 'object' && window.RTCPeerConnection && + !('getSenders' in window.RTCPeerConnection.prototype) && + 'createDTMFSender' in window.RTCPeerConnection.prototype) { + var shimSenderWithDtmf = function(pc, track) { + return { + track: track, + get dtmf() { + if (this._dtmf === undefined) { + if (track.kind === 'audio') { + this._dtmf = pc.createDTMFSender(track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + }, + _pc: pc + }; + }; + + // augment addTrack when getSenders is not available. + if (!window.RTCPeerConnection.prototype.getSenders) { + window.RTCPeerConnection.prototype.getSenders = function() { + this._senders = this._senders || []; + return this._senders.slice(); // return a copy of the internal state. + }; + var origAddTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addTrack = function(track, stream) { + var pc = this; + var sender = origAddTrack.apply(pc, arguments); + if (!sender) { + sender = shimSenderWithDtmf(pc, track); + pc._senders.push(sender); + } + return sender; + }; + + var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; + window.RTCPeerConnection.prototype.removeTrack = function(sender) { + var pc = this; + origRemoveTrack.apply(pc, arguments); + var idx = pc._senders.indexOf(sender); + if (idx !== -1) { + pc._senders.splice(idx, 1); + } + }; + } + var origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function(stream) { + var pc = this; + pc._senders = pc._senders || []; + origAddStream.apply(pc, [stream]); + stream.getTracks().forEach(function(track) { + pc._senders.push(shimSenderWithDtmf(pc, track)); + }); + }; + + var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = function(stream) { + var pc = this; + pc._senders = pc._senders || []; + origRemoveStream.apply(pc, [stream]); + + stream.getTracks().forEach(function(track) { + var sender = pc._senders.find(function(s) { + return s.track === track; + }); + if (sender) { + pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender + } + }); + }; + } else if (typeof window === 'object' && window.RTCPeerConnection && + 'getSenders' in window.RTCPeerConnection.prototype && + 'createDTMFSender' in window.RTCPeerConnection.prototype && + window.RTCRtpSender && + !('dtmf' in window.RTCRtpSender.prototype)) { + var origGetSenders = window.RTCPeerConnection.prototype.getSenders; + window.RTCPeerConnection.prototype.getSenders = function() { + var pc = this; + var senders = origGetSenders.apply(pc, []); + senders.forEach(function(sender) { + sender._pc = pc; + }); + return senders; + }; + + Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { + get: function() { + if (this._dtmf === undefined) { + if (this.track.kind === 'audio') { + this._dtmf = this._pc.createDTMFSender(this.track); + } else { + this._dtmf = null; + } + } + return this._dtmf; + } + }); + } + }, + + shimSourceObject: function(window) { + var URL = window && window.URL; + + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + return this._srcObject; + }, + set: function(stream) { + var self = this; + // Use _srcObject as a private property for this shim + this._srcObject = stream; + if (this.src) { + URL.revokeObjectURL(this.src); + } + + if (!stream) { + this.src = ''; + return undefined; + } + this.src = URL.createObjectURL(stream); + // We need to recreate the blob url when a track is added or + // removed. Doing it manually since we want to avoid a recursion. + stream.addEventListener('addtrack', function() { + if (self.src) { + URL.revokeObjectURL(self.src); + } + self.src = URL.createObjectURL(stream); + }); + stream.addEventListener('removetrack', function() { + if (self.src) { + URL.revokeObjectURL(self.src); + } + self.src = URL.createObjectURL(stream); + }); + } + }); + } + } + }, + + shimAddTrackRemoveTrack: function(window) { + var browserDetails = utils.detectBrowser(window); + // shim addTrack and removeTrack. + if (window.RTCPeerConnection.prototype.addTrack && + browserDetails.version >= 63) { + return; + } + + // also shim pc.getLocalStreams when addTrack is shimmed + // to return the original streams. + var origGetLocalStreams = window.RTCPeerConnection.prototype + .getLocalStreams; + window.RTCPeerConnection.prototype.getLocalStreams = function() { + var self = this; + var nativeStreams = origGetLocalStreams.apply(this); + self._reverseStreams = self._reverseStreams || {}; + return nativeStreams.map(function(stream) { + return self._reverseStreams[stream.id]; + }); + }; + + var origAddStream = window.RTCPeerConnection.prototype.addStream; + window.RTCPeerConnection.prototype.addStream = function(stream) { + var pc = this; + pc._streams = pc._streams || {}; + pc._reverseStreams = pc._reverseStreams || {}; + + stream.getTracks().forEach(function(track) { + var alreadyExists = pc.getSenders().find(function(s) { + return s.track === track; + }); + if (alreadyExists) { + throw new DOMException('Track already exists.', + 'InvalidAccessError'); + } + }); + // Add identity mapping for consistency with addTrack. + // Unless this is being used with a stream from addTrack. + if (!pc._reverseStreams[stream.id]) { + var newStream = new window.MediaStream(stream.getTracks()); + pc._streams[stream.id] = newStream; + pc._reverseStreams[newStream.id] = stream; + stream = newStream; + } + origAddStream.apply(pc, [stream]); + }; + + var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; + window.RTCPeerConnection.prototype.removeStream = function(stream) { + var pc = this; + pc._streams = pc._streams || {}; + pc._reverseStreams = pc._reverseStreams || {}; + + origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]); + delete pc._reverseStreams[(pc._streams[stream.id] ? + pc._streams[stream.id].id : stream.id)]; + delete pc._streams[stream.id]; + }; + + window.RTCPeerConnection.prototype.addTrack = function(track, stream) { + var pc = this; + if (pc.signalingState === 'closed') { + throw new DOMException( + 'The RTCPeerConnection\'s signalingState is \'closed\'.', + 'InvalidStateError'); + } + var streams = [].slice.call(arguments, 1); + if (streams.length !== 1 || + !streams[0].getTracks().find(function(t) { + return t === track; + })) { + // this is not fully correct but all we can manage without + // [[associated MediaStreams]] internal slot. + throw new DOMException( + 'The adapter.js addTrack polyfill only supports a single ' + + ' stream which is associated with the specified track.', + 'NotSupportedError'); + } + + var alreadyExists = pc.getSenders().find(function(s) { + return s.track === track; + }); + if (alreadyExists) { + throw new DOMException('Track already exists.', + 'InvalidAccessError'); + } + + pc._streams = pc._streams || {}; + pc._reverseStreams = pc._reverseStreams || {}; + var oldStream = pc._streams[stream.id]; + if (oldStream) { + // this is using odd Chrome behaviour, use with caution: + // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 + // Note: we rely on the high-level addTrack/dtmf shim to + // create the sender with a dtmf sender. + oldStream.addTrack(track); + + // Trigger ONN async. + Promise.resolve().then(function() { + pc.dispatchEvent(new Event('negotiationneeded')); + }); + } else { + var newStream = new window.MediaStream([track]); + pc._streams[stream.id] = newStream; + pc._reverseStreams[newStream.id] = stream; + pc.addStream(newStream); + } + return pc.getSenders().find(function(s) { + return s.track === track; + }); + }; + + // replace the internal stream id with the external one and + // vice versa. + function replaceInternalStreamId(pc, description) { + var sdp = description.sdp; + Object.keys(pc._reverseStreams || []).forEach(function(internalId) { + var externalStream = pc._reverseStreams[internalId]; + var internalStream = pc._streams[externalStream.id]; + sdp = sdp.replace(new RegExp(internalStream.id, 'g'), + externalStream.id); + }); + return new RTCSessionDescription({ + type: description.type, + sdp: sdp + }); + } + function replaceExternalStreamId(pc, description) { + var sdp = description.sdp; + Object.keys(pc._reverseStreams || []).forEach(function(internalId) { + var externalStream = pc._reverseStreams[internalId]; + var internalStream = pc._streams[externalStream.id]; + sdp = sdp.replace(new RegExp(externalStream.id, 'g'), + internalStream.id); + }); + return new RTCSessionDescription({ + type: description.type, + sdp: sdp + }); + } + ['createOffer', 'createAnswer'].forEach(function(method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + window.RTCPeerConnection.prototype[method] = function() { + var pc = this; + var args = arguments; + var isLegacyCall = arguments.length && + typeof arguments[0] === 'function'; + if (isLegacyCall) { + return nativeMethod.apply(pc, [ + function(description) { + var desc = replaceInternalStreamId(pc, description); + args[0].apply(null, [desc]); + }, + function(err) { + if (args[1]) { + args[1].apply(null, err); + } + }, arguments[2] + ]); + } + return nativeMethod.apply(pc, arguments) + .then(function(description) { + return replaceInternalStreamId(pc, description); + }); + }; + }); + + var origSetLocalDescription = + window.RTCPeerConnection.prototype.setLocalDescription; + window.RTCPeerConnection.prototype.setLocalDescription = function() { + var pc = this; + if (!arguments.length || !arguments[0].type) { + return origSetLocalDescription.apply(pc, arguments); + } + arguments[0] = replaceExternalStreamId(pc, arguments[0]); + return origSetLocalDescription.apply(pc, arguments); + }; + + // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier + + var origLocalDescription = Object.getOwnPropertyDescriptor( + window.RTCPeerConnection.prototype, 'localDescription'); + Object.defineProperty(window.RTCPeerConnection.prototype, + 'localDescription', { + get: function() { + var pc = this; + var description = origLocalDescription.get.apply(this); + if (description.type === '') { + return description; + } + return replaceInternalStreamId(pc, description); + } + }); + + window.RTCPeerConnection.prototype.removeTrack = function(sender) { + var pc = this; + if (pc.signalingState === 'closed') { + throw new DOMException( + 'The RTCPeerConnection\'s signalingState is \'closed\'.', + 'InvalidStateError'); + } + // We can not yet check for sender instanceof RTCRtpSender + // since we shim RTPSender. So we check if sender._pc is set. + if (!sender._pc) { + throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + + 'does not implement interface RTCRtpSender.', 'TypeError'); + } + var isLocal = sender._pc === pc; + if (!isLocal) { + throw new DOMException('Sender was not created by this connection.', + 'InvalidAccessError'); + } + + // Search for the native stream the senders track belongs to. + pc._streams = pc._streams || {}; + var stream; + Object.keys(pc._streams).forEach(function(streamid) { + var hasTrack = pc._streams[streamid].getTracks().find(function(track) { + return sender.track === track; + }); + if (hasTrack) { + stream = pc._streams[streamid]; + } + }); + + if (stream) { + if (stream.getTracks().length === 1) { + // if this is the last track of the stream, remove the stream. This + // takes care of any shimmed _senders. + pc.removeStream(pc._reverseStreams[stream.id]); + } else { + // relying on the same odd chrome behaviour as above. + stream.removeTrack(sender.track); + } + pc.dispatchEvent(new Event('negotiationneeded')); + } + }; + }, + + shimPeerConnection: function(window) { + var browserDetails = utils.detectBrowser(window); + + // The RTCPeerConnection object. + if (!window.RTCPeerConnection) { + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + // Translate iceTransportPolicy to iceTransports, + // see https://code.google.com/p/webrtc/issues/detail?id=4869 + // this was fixed in M56 along with unprefixing RTCPeerConnection. + logging('PeerConnection'); + if (pcConfig && pcConfig.iceTransportPolicy) { + pcConfig.iceTransports = pcConfig.iceTransportPolicy; + } + + return new window.webkitRTCPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = + window.webkitRTCPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + if (window.webkitRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return window.webkitRTCPeerConnection.generateCertificate; + } + }); + } + } else { + // migrate from non-spec RTCIceServer.url to RTCIceServer.urls + var OrigPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (!server.hasOwnProperty('urls') && + server.hasOwnProperty('url')) { + utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); + server = JSON.parse(JSON.stringify(server)); + server.urls = server.url; + newIceServers.push(server); + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + return new OrigPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return OrigPeerConnection.generateCertificate; + } + }); + } + + var origGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function(selector, + successCallback, errorCallback) { + var self = this; + var args = arguments; + + // If selector is a function then we are in the old style stats so just + // pass back the original getStats format to avoid breaking old users. + if (arguments.length > 0 && typeof selector === 'function') { + return origGetStats.apply(this, arguments); + } + + // When spec-style getStats is supported, return those when called with + // either no arguments or the selector argument is null. + if (origGetStats.length === 0 && (arguments.length === 0 || + typeof arguments[0] !== 'function')) { + return origGetStats.apply(this, []); + } + + var fixChromeStats_ = function(response) { + var standardReport = {}; + var reports = response.result(); + reports.forEach(function(report) { + var standardStats = { + id: report.id, + timestamp: report.timestamp, + type: { + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }[report.type] || report.type + }; + report.names().forEach(function(name) { + standardStats[name] = report.stat(name); + }); + standardReport[standardStats.id] = standardStats; + }); + + return standardReport; + }; + + // shim getStats with maplike support + var makeMapStats = function(stats) { + return new Map(Object.keys(stats).map(function(key) { + return [key, stats[key]]; + })); + }; + + if (arguments.length >= 2) { + var successCallbackWrapper_ = function(response) { + args[1](makeMapStats(fixChromeStats_(response))); + }; + + return origGetStats.apply(this, [successCallbackWrapper_, + arguments[0]]); + } + + // promise-support + return new Promise(function(resolve, reject) { + origGetStats.apply(self, [ + function(response) { + resolve(makeMapStats(fixChromeStats_(response))); + }, reject]); + }).then(successCallback, errorCallback); + }; + + // add promise support -- natively available in Chrome 51 + if (browserDetails.version < 51) { + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + window.RTCPeerConnection.prototype[method] = function() { + var args = arguments; + var self = this; + var promise = new Promise(function(resolve, reject) { + nativeMethod.apply(self, [args[0], resolve, reject]); + }); + if (args.length < 2) { + return promise; + } + return promise.then(function() { + args[1].apply(null, []); + }, + function(err) { + if (args.length >= 3) { + args[2].apply(null, [err]); + } + }); + }; + }); + } + + // promise support for createOffer and createAnswer. Available (without + // bugs) since M52: crbug/619289 + if (browserDetails.version < 52) { + ['createOffer', 'createAnswer'].forEach(function(method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + window.RTCPeerConnection.prototype[method] = function() { + var self = this; + if (arguments.length < 1 || (arguments.length === 1 && + typeof arguments[0] === 'object')) { + var opts = arguments.length === 1 ? arguments[0] : undefined; + return new Promise(function(resolve, reject) { + nativeMethod.apply(self, [resolve, reject, opts]); + }); + } + return nativeMethod.apply(this, arguments); + }; + }); + } + + // shim implicit creation of RTCSessionDescription/RTCIceCandidate + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + window.RTCPeerConnection.prototype[method] = function() { + arguments[0] = new ((method === 'addIceCandidate') ? + window.RTCIceCandidate : + window.RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }; + }); + + // support for addIceCandidate(null or undefined) + var nativeAddIceCandidate = + window.RTCPeerConnection.prototype.addIceCandidate; + window.RTCPeerConnection.prototype.addIceCandidate = function() { + if (!arguments[0]) { + if (arguments[1]) { + arguments[1].apply(null); + } + return Promise.resolve(); + } + return nativeAddIceCandidate.apply(this, arguments); + }; + } +}; + + +// Expose public methods. +module.exports = { + shimMediaStream: chromeShim.shimMediaStream, + shimOnTrack: chromeShim.shimOnTrack, + shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack, + shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf, + shimSourceObject: chromeShim.shimSourceObject, + shimPeerConnection: chromeShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia') +}; + +},{"../utils.js":13,"./getusermedia":6}],6:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; +var utils = require('../utils.js'); +var logging = utils.log; + +// Expose public methods. +module.exports = function(window) { + var browserDetails = utils.detectBrowser(window); + var navigator = window && window.navigator; + + var constraintsToChrome_ = function(c) { + if (typeof c !== 'object' || c.mandatory || c.optional) { + return c; + } + var cc = {}; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; + if (r.exact !== undefined && typeof r.exact === 'number') { + r.min = r.max = r.exact; + } + var oldname_ = function(prefix, name) { + if (prefix) { + return prefix + name.charAt(0).toUpperCase() + name.slice(1); + } + return (name === 'deviceId') ? 'sourceId' : name; + }; + if (r.ideal !== undefined) { + cc.optional = cc.optional || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[oldname_('min', key)] = r.ideal; + cc.optional.push(oc); + oc = {}; + oc[oldname_('max', key)] = r.ideal; + cc.optional.push(oc); + } else { + oc[oldname_('', key)] = r.ideal; + cc.optional.push(oc); + } + } + if (r.exact !== undefined && typeof r.exact !== 'number') { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_('', key)] = r.exact; + } else { + ['min', 'max'].forEach(function(mix) { + if (r[mix] !== undefined) { + cc.mandatory = cc.mandatory || {}; + cc.mandatory[oldname_(mix, key)] = r[mix]; + } + }); + } + }); + if (c.advanced) { + cc.optional = (cc.optional || []).concat(c.advanced); + } + return cc; + }; + + var shimConstraints_ = function(constraints, func) { + constraints = JSON.parse(JSON.stringify(constraints)); + if (constraints && typeof constraints.audio === 'object') { + var remap = function(obj, a, b) { + if (a in obj && !(b in obj)) { + obj[b] = obj[a]; + delete obj[a]; + } + }; + constraints = JSON.parse(JSON.stringify(constraints)); + remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); + remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); + constraints.audio = constraintsToChrome_(constraints.audio); + } + if (constraints && typeof constraints.video === 'object') { + // Shim facingMode for mobile & surface pro. + var face = constraints.video.facingMode; + face = face && ((typeof face === 'object') ? face : {ideal: face}); + var getSupportedFacingModeLies = browserDetails.version < 66; + + if ((face && (face.exact === 'user' || face.exact === 'environment' || + face.ideal === 'user' || face.ideal === 'environment')) && + !(navigator.mediaDevices.getSupportedConstraints && + navigator.mediaDevices.getSupportedConstraints().facingMode && + !getSupportedFacingModeLies)) { + delete constraints.video.facingMode; + var matches; + if (face.exact === 'environment' || face.ideal === 'environment') { + matches = ['back', 'rear']; + } else if (face.exact === 'user' || face.ideal === 'user') { + matches = ['front']; + } + if (matches) { + // Look for matches in label, or use last cam for back (typical). + return navigator.mediaDevices.enumerateDevices() + .then(function(devices) { + devices = devices.filter(function(d) { + return d.kind === 'videoinput'; + }); + var dev = devices.find(function(d) { + return matches.some(function(match) { + return d.label.toLowerCase().indexOf(match) !== -1; + }); + }); + if (!dev && devices.length && matches.indexOf('back') !== -1) { + dev = devices[devices.length - 1]; // more likely the back cam + } + if (dev) { + constraints.video.deviceId = face.exact ? {exact: dev.deviceId} : + {ideal: dev.deviceId}; + } + constraints.video = constraintsToChrome_(constraints.video); + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }); + } + } + constraints.video = constraintsToChrome_(constraints.video); + } + logging('chrome: ' + JSON.stringify(constraints)); + return func(constraints); + }; + + var shimError_ = function(e) { + return { + name: { + PermissionDeniedError: 'NotAllowedError', + InvalidStateError: 'NotReadableError', + DevicesNotFoundError: 'NotFoundError', + ConstraintNotSatisfiedError: 'OverconstrainedError', + TrackStartError: 'NotReadableError', + MediaDeviceFailedDueToShutdown: 'NotReadableError', + MediaDeviceKillSwitchOn: 'NotReadableError' + }[e.name] || e.name, + message: e.message, + constraint: e.constraintName, + toString: function() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + var getUserMedia_ = function(constraints, onSuccess, onError) { + shimConstraints_(constraints, function(c) { + navigator.webkitGetUserMedia(c, onSuccess, function(e) { + if (onError) { + onError(shimError_(e)); + } + }); + }); + }; + + navigator.getUserMedia = getUserMedia_; + + // Returns the result of getUserMedia as a Promise. + var getUserMediaPromise_ = function(constraints) { + return new Promise(function(resolve, reject) { + navigator.getUserMedia(constraints, resolve, reject); + }); + }; + + if (!navigator.mediaDevices) { + navigator.mediaDevices = { + getUserMedia: getUserMediaPromise_, + enumerateDevices: function() { + return new Promise(function(resolve) { + var kinds = {audio: 'audioinput', video: 'videoinput'}; + return window.MediaStreamTrack.getSources(function(devices) { + resolve(devices.map(function(device) { + return {label: device.label, + kind: kinds[device.kind], + deviceId: device.id, + groupId: ''}; + })); + }); + }); + }, + getSupportedConstraints: function() { + return { + deviceId: true, echoCancellation: true, facingMode: true, + frameRate: true, height: true, width: true + }; + } + }; + } + + // A shim for getUserMedia method on the mediaDevices object. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (!navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia = function(constraints) { + return getUserMediaPromise_(constraints); + }; + } else { + // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia + // function which returns a Promise, it does not accept spec-style + // constraints. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(cs) { + return shimConstraints_(cs, function(c) { + return origGetUserMedia(c).then(function(stream) { + if (c.audio && !stream.getAudioTracks().length || + c.video && !stream.getVideoTracks().length) { + stream.getTracks().forEach(function(track) { + track.stop(); + }); + throw new DOMException('', 'NotFoundError'); + } + return stream; + }, function(e) { + return Promise.reject(shimError_(e)); + }); + }); + }; + } + + // Dummy devicechange event methods. + // TODO(KaptenJansson) remove once implemented in Chrome stable. + if (typeof navigator.mediaDevices.addEventListener === 'undefined') { + navigator.mediaDevices.addEventListener = function() { + logging('Dummy mediaDevices.addEventListener called.'); + }; + } + if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { + navigator.mediaDevices.removeEventListener = function() { + logging('Dummy mediaDevices.removeEventListener called.'); + }; + } +}; + +},{"../utils.js":13}],7:[function(require,module,exports){ +/* + * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var SDPUtils = require('sdp'); +var utils = require('./utils'); + +// Wraps the peerconnection event eventNameToWrap in a function +// which returns the modified event object. +function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { + if (!window.RTCPeerConnection) { + return; + } + var proto = window.RTCPeerConnection.prototype; + var nativeAddEventListener = proto.addEventListener; + proto.addEventListener = function(nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap) { + return nativeAddEventListener.apply(this, arguments); + } + var wrappedCallback = function(e) { + cb(wrapper(e)); + }; + this._eventMap = this._eventMap || {}; + this._eventMap[cb] = wrappedCallback; + return nativeAddEventListener.apply(this, [nativeEventName, + wrappedCallback]); + }; + + var nativeRemoveEventListener = proto.removeEventListener; + proto.removeEventListener = function(nativeEventName, cb) { + if (nativeEventName !== eventNameToWrap || !this._eventMap + || !this._eventMap[cb]) { + return nativeRemoveEventListener.apply(this, arguments); + } + var unwrappedCb = this._eventMap[cb]; + delete this._eventMap[cb]; + return nativeRemoveEventListener.apply(this, [nativeEventName, + unwrappedCb]); + }; + + Object.defineProperty(proto, 'on' + eventNameToWrap, { + get: function() { + return this['_on' + eventNameToWrap]; + }, + set: function(cb) { + if (this['_on' + eventNameToWrap]) { + this.removeEventListener(eventNameToWrap, + this['_on' + eventNameToWrap]); + delete this['_on' + eventNameToWrap]; + } + if (cb) { + this.addEventListener(eventNameToWrap, + this['_on' + eventNameToWrap] = cb); + } + } + }); +} + +module.exports = { + shimRTCIceCandidate: function(window) { + // foundation is arbitrarily chosen as an indicator for full support for + // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface + if (window.RTCIceCandidate && 'foundation' in + window.RTCIceCandidate.prototype) { + return; + } + + var NativeRTCIceCandidate = window.RTCIceCandidate; + window.RTCIceCandidate = function(args) { + // Remove the a= which shouldn't be part of the candidate string. + if (typeof args === 'object' && args.candidate && + args.candidate.indexOf('a=') === 0) { + args = JSON.parse(JSON.stringify(args)); + args.candidate = args.candidate.substr(2); + } + + // Augment the native candidate with the parsed fields. + var nativeCandidate = new NativeRTCIceCandidate(args); + var parsedCandidate = SDPUtils.parseCandidate(args.candidate); + var augmentedCandidate = Object.assign(nativeCandidate, + parsedCandidate); + + // Add a serializer that does not serialize the extra attributes. + augmentedCandidate.toJSON = function() { + return { + candidate: augmentedCandidate.candidate, + sdpMid: augmentedCandidate.sdpMid, + sdpMLineIndex: augmentedCandidate.sdpMLineIndex, + usernameFragment: augmentedCandidate.usernameFragment, + }; + }; + return augmentedCandidate; + }; + + // Hook up the augmented candidate in onicecandidate and + // addEventListener('icecandidate', ...) + wrapPeerConnectionEvent(window, 'icecandidate', function(e) { + if (e.candidate) { + Object.defineProperty(e, 'candidate', { + value: new window.RTCIceCandidate(e.candidate), + writable: 'false' + }); + } + return e; + }); + }, + + // shimCreateObjectURL must be called before shimSourceObject to avoid loop. + + shimCreateObjectURL: function(window) { + var URL = window && window.URL; + + if (!(typeof window === 'object' && window.HTMLMediaElement && + 'srcObject' in window.HTMLMediaElement.prototype && + URL.createObjectURL && URL.revokeObjectURL)) { + // Only shim CreateObjectURL using srcObject if srcObject exists. + return undefined; + } + + var nativeCreateObjectURL = URL.createObjectURL.bind(URL); + var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL); + var streams = new Map(), newId = 0; + + URL.createObjectURL = function(stream) { + if ('getTracks' in stream) { + var url = 'polyblob:' + (++newId); + streams.set(url, stream); + utils.deprecated('URL.createObjectURL(stream)', + 'elem.srcObject = stream'); + return url; + } + return nativeCreateObjectURL(stream); + }; + URL.revokeObjectURL = function(url) { + nativeRevokeObjectURL(url); + streams.delete(url); + }; + + var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype, + 'src'); + Object.defineProperty(window.HTMLMediaElement.prototype, 'src', { + get: function() { + return dsc.get.apply(this); + }, + set: function(url) { + this.srcObject = streams.get(url) || null; + return dsc.set.apply(this, [url]); + } + }); + + var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute; + window.HTMLMediaElement.prototype.setAttribute = function() { + if (arguments.length === 2 && + ('' + arguments[0]).toLowerCase() === 'src') { + this.srcObject = streams.get(arguments[1]) || null; + } + return nativeSetAttribute.apply(this, arguments); + }; + } +}; + +},{"./utils":13,"sdp":2}],8:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var utils = require('../utils'); +var shimRTCPeerConnection = require('rtcpeerconnection-shim'); + +module.exports = { + shimGetUserMedia: require('./getusermedia'), + shimPeerConnection: function(window) { + var browserDetails = utils.detectBrowser(window); + + if (window.RTCIceGatherer) { + // ORTC defines an RTCIceCandidate object but no constructor. + // Not implemented in Edge. + if (!window.RTCIceCandidate) { + window.RTCIceCandidate = function(args) { + return args; + }; + } + // ORTC does not have a session description object but + // other browsers (i.e. Chrome) that will support both PC and ORTC + // in the future might have this defined already. + if (!window.RTCSessionDescription) { + window.RTCSessionDescription = function(args) { + return args; + }; + } + // this adds an additional event listener to MediaStrackTrack that signals + // when a tracks enabled property was changed. Workaround for a bug in + // addStream, see below. No longer required in 15025+ + if (browserDetails.version < 15025) { + var origMSTEnabled = Object.getOwnPropertyDescriptor( + window.MediaStreamTrack.prototype, 'enabled'); + Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', { + set: function(value) { + origMSTEnabled.set.call(this, value); + var ev = new Event('enabled'); + ev.enabled = value; + this.dispatchEvent(ev); + } + }); + } + } + + // ORTC defines the DTMF sender a bit different. + // https://github.com/w3c/ortc/issues/714 + if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { + Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { + get: function() { + if (this._dtmf === undefined) { + if (this.track.kind === 'audio') { + this._dtmf = new window.RTCDtmfSender(this); + } else if (this.track.kind === 'video') { + this._dtmf = null; + } + } + return this._dtmf; + } + }); + } + + window.RTCPeerConnection = + shimRTCPeerConnection(window, browserDetails.version); + }, + shimReplaceTrack: function(window) { + // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614 + if (window.RTCRtpSender && + !('replaceTrack' in window.RTCRtpSender.prototype)) { + window.RTCRtpSender.prototype.replaceTrack = + window.RTCRtpSender.prototype.setTrack; + } + } +}; + +},{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +// Expose public methods. +module.exports = function(window) { + var navigator = window && window.navigator; + + var shimError_ = function(e) { + return { + name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, + message: e.message, + constraint: e.constraint, + toString: function() { + return this.name; + } + }; + }; + + // getUserMedia error shim. + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).catch(function(e) { + return Promise.reject(shimError_(e)); + }); + }; +}; + +},{}],10:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var utils = require('../utils'); + +var firefoxShim = { + shimOnTrack: function(window) { + if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in + window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { + get: function() { + return this._ontrack; + }, + set: function(f) { + if (this._ontrack) { + this.removeEventListener('track', this._ontrack); + this.removeEventListener('addstream', this._ontrackpoly); + } + this.addEventListener('track', this._ontrack = f); + this.addEventListener('addstream', this._ontrackpoly = function(e) { + e.stream.getTracks().forEach(function(track) { + var event = new Event('track'); + event.track = track; + event.receiver = {track: track}; + event.transceiver = {receiver: event.receiver}; + event.streams = [e.stream]; + this.dispatchEvent(event); + }.bind(this)); + }.bind(this)); + } + }); + } + if (typeof window === 'object' && window.RTCTrackEvent && + ('receiver' in window.RTCTrackEvent.prototype) && + !('transceiver' in window.RTCTrackEvent.prototype)) { + Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { + get: function() { + return {receiver: this.receiver}; + } + }); + } + }, + + shimSourceObject: function(window) { + // Firefox has supported mozSrcObject since FF22, unprefixed in 42. + if (typeof window === 'object') { + if (window.HTMLMediaElement && + !('srcObject' in window.HTMLMediaElement.prototype)) { + // Shim the srcObject property, once, when HTMLMediaElement is found. + Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { + get: function() { + return this.mozSrcObject; + }, + set: function(stream) { + this.mozSrcObject = stream; + } + }); + } + } + }, + + shimPeerConnection: function(window) { + var browserDetails = utils.detectBrowser(window); + + if (typeof window !== 'object' || !(window.RTCPeerConnection || + window.mozRTCPeerConnection)) { + return; // probably media.peerconnection.enabled=false in about:config + } + // The RTCPeerConnection object. + if (!window.RTCPeerConnection) { + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (browserDetails.version < 38) { + // .urls is not supported in FF < 38. + // create RTCIceServers with a single url. + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (server.hasOwnProperty('urls')) { + for (var j = 0; j < server.urls.length; j++) { + var newServer = { + url: server.urls[j] + }; + if (server.urls[j].indexOf('turn') === 0) { + newServer.username = server.username; + newServer.credential = server.credential; + } + newIceServers.push(newServer); + } + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + } + return new window.mozRTCPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = + window.mozRTCPeerConnection.prototype; + + // wrap static methods. Currently just generateCertificate. + if (window.mozRTCPeerConnection.generateCertificate) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return window.mozRTCPeerConnection.generateCertificate; + } + }); + } + + window.RTCSessionDescription = window.mozRTCSessionDescription; + window.RTCIceCandidate = window.mozRTCIceCandidate; + } + + // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. + ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] + .forEach(function(method) { + var nativeMethod = window.RTCPeerConnection.prototype[method]; + window.RTCPeerConnection.prototype[method] = function() { + arguments[0] = new ((method === 'addIceCandidate') ? + window.RTCIceCandidate : + window.RTCSessionDescription)(arguments[0]); + return nativeMethod.apply(this, arguments); + }; + }); + + // support for addIceCandidate(null or undefined) + var nativeAddIceCandidate = + window.RTCPeerConnection.prototype.addIceCandidate; + window.RTCPeerConnection.prototype.addIceCandidate = function() { + if (!arguments[0]) { + if (arguments[1]) { + arguments[1].apply(null); + } + return Promise.resolve(); + } + return nativeAddIceCandidate.apply(this, arguments); + }; + + // shim getStats with maplike support + var makeMapStats = function(stats) { + var map = new Map(); + Object.keys(stats).forEach(function(key) { + map.set(key, stats[key]); + map[key] = stats[key]; + }); + return map; + }; + + var modernStatsTypes = { + inboundrtp: 'inbound-rtp', + outboundrtp: 'outbound-rtp', + candidatepair: 'candidate-pair', + localcandidate: 'local-candidate', + remotecandidate: 'remote-candidate' + }; + + var nativeGetStats = window.RTCPeerConnection.prototype.getStats; + window.RTCPeerConnection.prototype.getStats = function( + selector, + onSucc, + onErr + ) { + return nativeGetStats.apply(this, [selector || null]) + .then(function(stats) { + if (browserDetails.version < 48) { + stats = makeMapStats(stats); + } + if (browserDetails.version < 53 && !onSucc) { + // Shim only promise getStats with spec-hyphens in type names + // Leave callback version alone; misc old uses of forEach before Map + try { + stats.forEach(function(stat) { + stat.type = modernStatsTypes[stat.type] || stat.type; + }); + } catch (e) { + if (e.name !== 'TypeError') { + throw e; + } + // Avoid TypeError: "type" is read-only, in old versions. 34-43ish + stats.forEach(function(stat, i) { + stats.set(i, Object.assign({}, stat, { + type: modernStatsTypes[stat.type] || stat.type + })); + }); + } + } + return stats; + }) + .then(onSucc, onErr); + }; + } +}; + +// Expose public methods. +module.exports = { + shimOnTrack: firefoxShim.shimOnTrack, + shimSourceObject: firefoxShim.shimSourceObject, + shimPeerConnection: firefoxShim.shimPeerConnection, + shimGetUserMedia: require('./getusermedia') +}; + +},{"../utils":13,"./getusermedia":11}],11:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var utils = require('../utils'); +var logging = utils.log; + +// Expose public methods. +module.exports = function(window) { + var browserDetails = utils.detectBrowser(window); + var navigator = window && window.navigator; + var MediaStreamTrack = window && window.MediaStreamTrack; + + var shimError_ = function(e) { + return { + name: { + InternalError: 'NotReadableError', + NotSupportedError: 'TypeError', + PermissionDeniedError: 'NotAllowedError', + SecurityError: 'NotAllowedError' + }[e.name] || e.name, + message: { + 'The operation is insecure.': 'The request is not allowed by the ' + + 'user agent or the platform in the current context.' + }[e.message] || e.message, + constraint: e.constraint, + toString: function() { + return this.name + (this.message && ': ') + this.message; + } + }; + }; + + // getUserMedia constraints shim. + var getUserMedia_ = function(constraints, onSuccess, onError) { + var constraintsToFF37_ = function(c) { + if (typeof c !== 'object' || c.require) { + return c; + } + var require = []; + Object.keys(c).forEach(function(key) { + if (key === 'require' || key === 'advanced' || key === 'mediaSource') { + return; + } + var r = c[key] = (typeof c[key] === 'object') ? + c[key] : {ideal: c[key]}; + if (r.min !== undefined || + r.max !== undefined || r.exact !== undefined) { + require.push(key); + } + if (r.exact !== undefined) { + if (typeof r.exact === 'number') { + r. min = r.max = r.exact; + } else { + c[key] = r.exact; + } + delete r.exact; + } + if (r.ideal !== undefined) { + c.advanced = c.advanced || []; + var oc = {}; + if (typeof r.ideal === 'number') { + oc[key] = {min: r.ideal, max: r.ideal}; + } else { + oc[key] = r.ideal; + } + c.advanced.push(oc); + delete r.ideal; + if (!Object.keys(r).length) { + delete c[key]; + } + } + }); + if (require.length) { + c.require = require; + } + return c; + }; + constraints = JSON.parse(JSON.stringify(constraints)); + if (browserDetails.version < 38) { + logging('spec: ' + JSON.stringify(constraints)); + if (constraints.audio) { + constraints.audio = constraintsToFF37_(constraints.audio); + } + if (constraints.video) { + constraints.video = constraintsToFF37_(constraints.video); + } + logging('ff37: ' + JSON.stringify(constraints)); + } + return navigator.mozGetUserMedia(constraints, onSuccess, function(e) { + onError(shimError_(e)); + }); + }; + + // Returns the result of getUserMedia as a Promise. + var getUserMediaPromise_ = function(constraints) { + return new Promise(function(resolve, reject) { + getUserMedia_(constraints, resolve, reject); + }); + }; + + // Shim for mediaDevices on older versions. + if (!navigator.mediaDevices) { + navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, + addEventListener: function() { }, + removeEventListener: function() { } + }; + } + navigator.mediaDevices.enumerateDevices = + navigator.mediaDevices.enumerateDevices || function() { + return new Promise(function(resolve) { + var infos = [ + {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, + {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} + ]; + resolve(infos); + }); + }; + + if (browserDetails.version < 41) { + // Work around http://bugzil.la/1169665 + var orgEnumerateDevices = + navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); + navigator.mediaDevices.enumerateDevices = function() { + return orgEnumerateDevices().then(undefined, function(e) { + if (e.name === 'NotFoundError') { + return []; + } + throw e; + }); + }; + } + if (browserDetails.version < 49) { + var origGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + return origGetUserMedia(c).then(function(stream) { + // Work around https://bugzil.la/802326 + if (c.audio && !stream.getAudioTracks().length || + c.video && !stream.getVideoTracks().length) { + stream.getTracks().forEach(function(track) { + track.stop(); + }); + throw new DOMException('The object can not be found here.', + 'NotFoundError'); + } + return stream; + }, function(e) { + return Promise.reject(shimError_(e)); + }); + }; + } + if (!(browserDetails.version > 55 && + 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { + var remap = function(obj, a, b) { + if (a in obj && !(b in obj)) { + obj[b] = obj[a]; + delete obj[a]; + } + }; + + var nativeGetUserMedia = navigator.mediaDevices.getUserMedia. + bind(navigator.mediaDevices); + navigator.mediaDevices.getUserMedia = function(c) { + if (typeof c === 'object' && typeof c.audio === 'object') { + c = JSON.parse(JSON.stringify(c)); + remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); + remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); + } + return nativeGetUserMedia(c); + }; + + if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { + var nativeGetSettings = MediaStreamTrack.prototype.getSettings; + MediaStreamTrack.prototype.getSettings = function() { + var obj = nativeGetSettings.apply(this, arguments); + remap(obj, 'mozAutoGainControl', 'autoGainControl'); + remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); + return obj; + }; + } + + if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { + var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints; + MediaStreamTrack.prototype.applyConstraints = function(c) { + if (this.kind === 'audio' && typeof c === 'object') { + c = JSON.parse(JSON.stringify(c)); + remap(c, 'autoGainControl', 'mozAutoGainControl'); + remap(c, 'noiseSuppression', 'mozNoiseSuppression'); + } + return nativeApplyConstraints.apply(this, [c]); + }; + } + } + navigator.getUserMedia = function(constraints, onSuccess, onError) { + if (browserDetails.version < 44) { + return getUserMedia_(constraints, onSuccess, onError); + } + // Replace Firefox 44+'s deprecation warning with unprefixed version. + utils.deprecated('navigator.getUserMedia', + 'navigator.mediaDevices.getUserMedia'); + navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); + }; +}; + +},{"../utils":13}],12:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ +'use strict'; +var utils = require('../utils'); + +var safariShim = { + // TODO: DrAlex, should be here, double check against LayoutTests + + // TODO: once the back-end for the mac port is done, add. + // TODO: check for webkitGTK+ + // shimPeerConnection: function() { }, + + shimLocalStreamsAPI: function(window) { + if (typeof window !== 'object' || !window.RTCPeerConnection) { + return; + } + if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getLocalStreams = function() { + if (!this._localStreams) { + this._localStreams = []; + } + return this._localStreams; + }; + } + if (!('getStreamById' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getStreamById = function(id) { + var result = null; + if (this._localStreams) { + this._localStreams.forEach(function(stream) { + if (stream.id === id) { + result = stream; + } + }); + } + if (this._remoteStreams) { + this._remoteStreams.forEach(function(stream) { + if (stream.id === id) { + result = stream; + } + }); + } + return result; + }; + } + if (!('addStream' in window.RTCPeerConnection.prototype)) { + var _addTrack = window.RTCPeerConnection.prototype.addTrack; + window.RTCPeerConnection.prototype.addStream = function(stream) { + if (!this._localStreams) { + this._localStreams = []; + } + if (this._localStreams.indexOf(stream) === -1) { + this._localStreams.push(stream); + } + var self = this; + stream.getTracks().forEach(function(track) { + _addTrack.call(self, track, stream); + }); + }; + + window.RTCPeerConnection.prototype.addTrack = function(track, stream) { + if (stream) { + if (!this._localStreams) { + this._localStreams = [stream]; + } else if (this._localStreams.indexOf(stream) === -1) { + this._localStreams.push(stream); + } + } + _addTrack.call(this, track, stream); + }; + } + if (!('removeStream' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.removeStream = function(stream) { + if (!this._localStreams) { + this._localStreams = []; + } + var index = this._localStreams.indexOf(stream); + if (index === -1) { + return; + } + this._localStreams.splice(index, 1); + var self = this; + var tracks = stream.getTracks(); + this.getSenders().forEach(function(sender) { + if (tracks.indexOf(sender.track) !== -1) { + self.removeTrack(sender); + } + }); + }; + } + }, + shimRemoteStreamsAPI: function(window) { + if (typeof window !== 'object' || !window.RTCPeerConnection) { + return; + } + if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { + window.RTCPeerConnection.prototype.getRemoteStreams = function() { + return this._remoteStreams ? this._remoteStreams : []; + }; + } + if (!('onaddstream' in window.RTCPeerConnection.prototype)) { + Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { + get: function() { + return this._onaddstream; + }, + set: function(f) { + if (this._onaddstream) { + this.removeEventListener('addstream', this._onaddstream); + this.removeEventListener('track', this._onaddstreampoly); + } + this.addEventListener('addstream', this._onaddstream = f); + this.addEventListener('track', this._onaddstreampoly = function(e) { + var stream = e.streams[0]; + if (!this._remoteStreams) { + this._remoteStreams = []; + } + if (this._remoteStreams.indexOf(stream) >= 0) { + return; + } + this._remoteStreams.push(stream); + var event = new Event('addstream'); + event.stream = e.streams[0]; + this.dispatchEvent(event); + }.bind(this)); + } + }); + } + }, + shimCallbacksAPI: function(window) { + if (typeof window !== 'object' || !window.RTCPeerConnection) { + return; + } + var prototype = window.RTCPeerConnection.prototype; + var createOffer = prototype.createOffer; + var createAnswer = prototype.createAnswer; + var setLocalDescription = prototype.setLocalDescription; + var setRemoteDescription = prototype.setRemoteDescription; + var addIceCandidate = prototype.addIceCandidate; + + prototype.createOffer = function(successCallback, failureCallback) { + var options = (arguments.length >= 2) ? arguments[2] : arguments[0]; + var promise = createOffer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + + prototype.createAnswer = function(successCallback, failureCallback) { + var options = (arguments.length >= 2) ? arguments[2] : arguments[0]; + var promise = createAnswer.apply(this, [options]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + + var withCallback = function(description, successCallback, failureCallback) { + var promise = setLocalDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.setLocalDescription = withCallback; + + withCallback = function(description, successCallback, failureCallback) { + var promise = setRemoteDescription.apply(this, [description]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.setRemoteDescription = withCallback; + + withCallback = function(candidate, successCallback, failureCallback) { + var promise = addIceCandidate.apply(this, [candidate]); + if (!failureCallback) { + return promise; + } + promise.then(successCallback, failureCallback); + return Promise.resolve(); + }; + prototype.addIceCandidate = withCallback; + }, + shimGetUserMedia: function(window) { + var navigator = window && window.navigator; + + if (!navigator.getUserMedia) { + if (navigator.webkitGetUserMedia) { + navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator); + } else if (navigator.mediaDevices && + navigator.mediaDevices.getUserMedia) { + navigator.getUserMedia = function(constraints, cb, errcb) { + navigator.mediaDevices.getUserMedia(constraints) + .then(cb, errcb); + }.bind(navigator); + } + } + }, + shimRTCIceServerUrls: function(window) { + // migrate from non-spec RTCIceServer.url to RTCIceServer.urls + var OrigPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function(pcConfig, pcConstraints) { + if (pcConfig && pcConfig.iceServers) { + var newIceServers = []; + for (var i = 0; i < pcConfig.iceServers.length; i++) { + var server = pcConfig.iceServers[i]; + if (!server.hasOwnProperty('urls') && + server.hasOwnProperty('url')) { + utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); + server = JSON.parse(JSON.stringify(server)); + server.urls = server.url; + delete server.url; + newIceServers.push(server); + } else { + newIceServers.push(pcConfig.iceServers[i]); + } + } + pcConfig.iceServers = newIceServers; + } + return new OrigPeerConnection(pcConfig, pcConstraints); + }; + window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; + // wrap static methods. Currently just generateCertificate. + if ('generateCertificate' in window.RTCPeerConnection) { + Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { + get: function() { + return OrigPeerConnection.generateCertificate; + } + }); + } + }, + shimTrackEventTransceiver: function(window) { + // Add event.transceiver member over deprecated event.receiver + if (typeof window === 'object' && window.RTCPeerConnection && + ('receiver' in window.RTCTrackEvent.prototype) && + // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is + // defined for some reason even when window.RTCTransceiver is not. + !window.RTCTransceiver) { + Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { + get: function() { + return {receiver: this.receiver}; + } + }); + } + }, + + shimCreateOfferLegacy: function(window) { + var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; + window.RTCPeerConnection.prototype.createOffer = function(offerOptions) { + var pc = this; + if (offerOptions) { + var audioTransceiver = pc.getTransceivers().find(function(transceiver) { + return transceiver.sender.track && + transceiver.sender.track.kind === 'audio'; + }); + if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { + if (audioTransceiver.direction === 'sendrecv') { + audioTransceiver.setDirection('sendonly'); + } else if (audioTransceiver.direction === 'recvonly') { + audioTransceiver.setDirection('inactive'); + } + } else if (offerOptions.offerToReceiveAudio === true && + !audioTransceiver) { + pc.addTransceiver('audio'); + } + + var videoTransceiver = pc.getTransceivers().find(function(transceiver) { + return transceiver.sender.track && + transceiver.sender.track.kind === 'video'; + }); + if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { + if (videoTransceiver.direction === 'sendrecv') { + videoTransceiver.setDirection('sendonly'); + } else if (videoTransceiver.direction === 'recvonly') { + videoTransceiver.setDirection('inactive'); + } + } else if (offerOptions.offerToReceiveVideo === true && + !videoTransceiver) { + pc.addTransceiver('video'); + } + } + return origCreateOffer.apply(pc, arguments); + }; + } +}; + +// Expose public methods. +module.exports = { + shimCallbacksAPI: safariShim.shimCallbacksAPI, + shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI, + shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI, + shimGetUserMedia: safariShim.shimGetUserMedia, + shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls, + shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver, + shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy + // TODO + // shimPeerConnection: safariShim.shimPeerConnection +}; + +},{"../utils":13}],13:[function(require,module,exports){ +/* + * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. + */ + /* eslint-env node */ +'use strict'; + +var logDisabled_ = true; +var deprecationWarnings_ = true; + +// Utility methods. +var utils = { + disableLog: function(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + typeof bool + + '. Please use a boolean.'); + } + logDisabled_ = bool; + return (bool) ? 'adapter.js logging disabled' : + 'adapter.js logging enabled'; + }, + + /** + * Disable or enable deprecation warnings + * @param {!boolean} bool set to true to disable warnings. + */ + disableWarnings: function(bool) { + if (typeof bool !== 'boolean') { + return new Error('Argument type: ' + typeof bool + + '. Please use a boolean.'); + } + deprecationWarnings_ = !bool; + return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); + }, + + log: function() { + if (typeof window === 'object') { + if (logDisabled_) { + return; + } + if (typeof console !== 'undefined' && typeof console.log === 'function') { + console.log.apply(console, arguments); + } + } + }, + + /** + * Shows a deprecation warning suggesting the modern and spec-compatible API. + */ + deprecated: function(oldMethod, newMethod) { + if (!deprecationWarnings_) { + return; + } + console.warn(oldMethod + ' is deprecated, please use ' + newMethod + + ' instead.'); + }, + + /** + * Extract browser version out of the provided user agent string. + * + * @param {!string} uastring userAgent string. + * @param {!string} expr Regular expression used as match criteria. + * @param {!number} pos position in the version string to be returned. + * @return {!number} browser version. + */ + extractVersion: function(uastring, expr, pos) { + var match = uastring.match(expr); + return match && match.length >= pos && parseInt(match[pos], 10); + }, + + /** + * Browser detector. + * + * @return {object} result containing browser and version + * properties. + */ + detectBrowser: function(window) { + var navigator = window && window.navigator; + + // Returned result object. + var result = {}; + result.browser = null; + result.version = null; + + // Fail early if it's not a browser + if (typeof window === 'undefined' || !window.navigator) { + result.browser = 'Not a browser.'; + return result; + } + + // Firefox. + if (navigator.mozGetUserMedia) { + result.browser = 'firefox'; + result.version = this.extractVersion(navigator.userAgent, + /Firefox\/(\d+)\./, 1); + } else if (navigator.webkitGetUserMedia) { + // Chrome, Chromium, Webview, Opera, all use the chrome shim for now + if (window.webkitRTCPeerConnection) { + result.browser = 'chrome'; + result.version = this.extractVersion(navigator.userAgent, + /Chrom(e|ium)\/(\d+)\./, 2); + } else { // Safari (in an unpublished version) or unknown webkit-based. + if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) { + result.browser = 'safari'; + result.version = this.extractVersion(navigator.userAgent, + /AppleWebKit\/(\d+)\./, 1); + } else { // unknown webkit-based browser. + result.browser = 'Unsupported webkit-based browser ' + + 'with GUM support but no WebRTC support.'; + return result; + } + } + } else if (navigator.mediaDevices && + navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge. + result.browser = 'edge'; + result.version = this.extractVersion(navigator.userAgent, + /Edge\/(\d+).(\d+)$/, 2); + } else if (navigator.mediaDevices && + navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { + // Safari, with webkitGetUserMedia removed. + result.browser = 'safari'; + result.version = this.extractVersion(navigator.userAgent, + /AppleWebKit\/(\d+)\./, 1); + } else { // Default fallthrough: not supported. + result.browser = 'Not a supported browser.'; + return result; + } + + return result; + }, + +}; + +// Export. +module.exports = { + log: utils.log, + deprecated: utils.deprecated, + disableLog: utils.disableLog, + disableWarnings: utils.disableWarnings, + extractVersion: utils.extractVersion, + shimCreateObjectURL: utils.shimCreateObjectURL, + detectBrowser: utils.detectBrowser.bind(utils) +}; + +},{}]},{},[3])(3) +}); \ No newline at end of file diff --git a/bigbluebutton-html5/public/js/adjust-videos.js b/bigbluebutton-html5/public/js/adjust-videos.js new file mode 100644 index 0000000000000000000000000000000000000000..bdacb7f984e4311ff039eefd18e3eb72d0bb2836 --- /dev/null +++ b/bigbluebutton-html5/public/js/adjust-videos.js @@ -0,0 +1,89 @@ + +(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); + } + + window.adjustVideos = adjustVideos; +})(); diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index 1dab09e2879dc5ec2278e623e6703d710a03efe3..ac582ccc4631a68deb3ddab454687fd7155d3a1e 100644 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -14,6 +14,7 @@ import '/imports/api/chat/server'; import '/imports/api/screenshare/server'; import '/imports/api/voice-users/server'; import '/imports/api/whiteboard-multi-user/server'; +import '/imports/api/video/server'; // Commons import '/imports/api/log-client/server'; diff --git a/labs/kurento-screenshare/README.md b/labs/bbb-webrtc-sfu/README.md similarity index 100% rename from labs/kurento-screenshare/README.md rename to labs/bbb-webrtc-sfu/README.md diff --git a/labs/bbb-webrtc-sfu/config/default.example.yml b/labs/bbb-webrtc-sfu/config/default.example.yml new file mode 100644 index 0000000000000000000000000000000000000000..3b9bbe8a29fa7cbbb171da6fcd190eeb7d7e5760 --- /dev/null +++ b/labs/bbb-webrtc-sfu/config/default.example.yml @@ -0,0 +1,15 @@ +kurentoUrl: "ws://HOST/kurento" +kurentoIp: "" +localIpAddress: "" +acceptSelfSignedCertificate: false +redisHost : "127.0.0.1" +redisPort : "6379" +clientPort : "3008" +minVideoPort: 30000 +maxVideoPort: 33000 +from-screenshare: "from-screenshare-sfu" +to-screenshare: "to-screenshare-sfu" +from-video: "from-video-sfu" +to-video: "to-video-sfu" +from-audio: "from-audio-sfu" +to-audio: "to-audio-sfu" diff --git a/labs/kurento-screenshare/debug-start.sh b/labs/bbb-webrtc-sfu/debug-start.sh similarity index 100% rename from labs/kurento-screenshare/debug-start.sh rename to labs/bbb-webrtc-sfu/debug-start.sh diff --git a/labs/kurento-screenshare/lib/bbb/messages/Constants.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js similarity index 88% rename from labs/kurento-screenshare/lib/bbb/messages/Constants.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js index 6cccf19f93cadf15684b9a09c2f2ddde37909dbd..3b2a77052aefe4154435efb76a485935e4ee9e43 100644 --- a/labs/kurento-screenshare/lib/bbb/messages/Constants.js +++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Constants.js @@ -1,4 +1,6 @@ "use strict"; + +const config = require('config'); /** * @classdesc * Message constants for the communication with BigBlueButton @@ -17,9 +19,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: config.get('from-screenshare'), + TO_SCREENSHARE: config.get('to-screenshare'), + FROM_VIDEO: config.get('from-video'), + TO_VIDEO: config.get('to-video'), + FROM_AUDIO: config.get('from-audio'), + TO_AUDIO: config.get('to-audio'), // RedisWrapper events REDIS_MESSAGE : "redis_message", + WEBSOCKET_MESAGE: "ws_message", + GATEWAY_MESSAGE: "gateway_message", // Message identifiers 1x START_TRANSCODER_REQUEST: "start_transcoder_request_message", diff --git a/labs/kurento-screenshare/lib/bbb/messages/Messaging.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js similarity index 69% rename from labs/kurento-screenshare/lib/bbb/messages/Messaging.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js index 92bab79944122fb88e16bb7c30152da68a0f80b7..a352acf1e5f8cfd736e48011316209995bf2a87d 100644 --- a/labs/kurento-screenshare/lib/bbb/messages/Messaging.js +++ b/labs/bbb-webrtc-sfu/lib/bbb/messages/Messaging.js @@ -1,24 +1,24 @@ -var Constants = require('./Constants.js'); +const Constants = require('./Constants.js'); // Messages -var OutMessage = require('./OutMessage.js'); +let OutMessage = require('./OutMessage.js'); -var StartTranscoderRequestMessage = +let StartTranscoderRequestMessage = require('./transcode/StartTranscoderRequestMessage.js')(Constants); -var StopTranscoderRequestMessage = +let StopTranscoderRequestMessage = require('./transcode/StopTranscoderRequestMessage.js')(Constants); -var StartTranscoderSysReqMsg = +let StartTranscoderSysReqMsg = require('./transcode/StartTranscoderSysReqMsg.js')(); -var StopTranscoderSysReqMsg = +let StopTranscoderSysReqMsg = require('./transcode/StopTranscoderSysReqMsg.js')(); -var DeskShareRTMPBroadcastStartedEventMessage = +let DeskShareRTMPBroadcastStartedEventMessage = require('./screenshare/DeskShareRTMPBroadcastStartedEventMessage.js')(Constants); -var DeskShareRTMPBroadcastStoppedEventMessage = +let DeskShareRTMPBroadcastStoppedEventMessage = require('./screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js')(Constants); -var ScreenshareRTMPBroadcastStartedEventMessage2x = +let ScreenshareRTMPBroadcastStartedEventMessage2x = require('./screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js')(Constants); -var ScreenshareRTMPBroadcastStoppedEventMessage2x = +let ScreenshareRTMPBroadcastStoppedEventMessage2x = require('./screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js')(Constants); @@ -31,39 +31,38 @@ function Messaging() {} Messaging.prototype.generateStartTranscoderRequestMessage = function(meetingId, transcoderId, params) { - var statrm = new StartTranscoderSysReqMsg(meetingId, transcoderId, params); + let statrm = new StartTranscoderSysReqMsg(meetingId, transcoderId, params); return statrm.toJson(); } Messaging.prototype.generateStopTranscoderRequestMessage = function(meetingId, transcoderId) { - var stotrm = new StopTranscoderSysReqMsg(meetingId, transcoderId); + let stotrm = new StopTranscoderSysReqMsg(meetingId, transcoderId); return stotrm.toJson(); } Messaging.prototype.generateDeskShareRTMPBroadcastStartedEvent = function(conferenceName, streamUrl, vw, vh, timestamp) { - var stadrbem = new DeskShareRTMPBroadcastStartedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); + let stadrbem = new DeskShareRTMPBroadcastStartedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); return stadrbem.toJson(); } Messaging.prototype.generateDeskShareRTMPBroadcastStoppedEvent = function(conferenceName, streamUrl, vw, vh, timestamp) { - var stodrbem = new DeskShareRTMPBroadcastStoppedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); + let stodrbem = new DeskShareRTMPBroadcastStoppedEventMessage(conferenceName, streamUrl, vw, vh, timestamp); return stodrbem.toJson(); } Messaging.prototype.generateScreenshareRTMPBroadcastStartedEvent2x = function(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp) { - var stadrbem = new ScreenshareRTMPBroadcastStartedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); + let stadrbem = new ScreenshareRTMPBroadcastStartedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); return stadrbem.toJson(); } Messaging.prototype.generateScreenshareRTMPBroadcastStoppedEvent2x = function(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp) { - var stodrbem = new ScreenshareRTMPBroadcastStoppedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); + let stodrbem = new ScreenshareRTMPBroadcastStoppedEventMessage2x(conferenceName, screenshareConf, streamUrl, vw, vh, timestamp); return stodrbem.toJson(); } module.exports = new Messaging(); -module.exports.Constants = Constants; diff --git a/labs/kurento-screenshare/lib/bbb/messages/OutMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/OutMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/OutMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage2x.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/OutMessage2x.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/OutMessage2x.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStartedEventMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/DeskShareRTMPBroadcastStoppedEventMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStartedEventMessage2x.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/screenshare/ScreenshareRTMPBroadcastStoppedEventMessage2x.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderRequestMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StartTranscoderSysReqMsg.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderRequestMessage.js diff --git a/labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js b/labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js similarity index 100% rename from labs/kurento-screenshare/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js rename to labs/bbb-webrtc-sfu/lib/bbb/messages/transcode/StopTranscoderSysReqMsg.js diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..94acc7db4eeea467c5133a3aaabbb92603f5373a --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/RedisWrapper.js @@ -0,0 +1,129 @@ +/** + * @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("connect", () => { + // console.log(" [RedisWrapper] Connected to Redis Server."); + // DO SOMETHING + }); + + this.redisCli.on("error", (e) => { + console.error(" [RedisWrapper] " + e); + }); + + this.redisCli.on("reconnecting", (e) => { + // DO SOMETHING + }); + + 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) { + this.redisPub.publish(channel, message); + } + } + + /* Private members */ + + _onMessage (pattern, channel, _message) { + let message = (typeof _message !== 'object')?JSON.parse(_message):_message; + // use event emitter to throw new message + this.emit(Constants.REDIS_MESSAGE, message); + } + + static _redisRetry (options) { + // if (options.error && options.error.code === 'ECONNREFUSED') { + // return new Error('The server refused the connection'); + // } + if (options.total_retry_time > RedisWrapper._retryThreshold) { + return new Error('Retry time exhausted'); + } + if (options.times_connected > RedisWrapper._maxRetries) { + return undefined; + } + return Math.max(options.attempt * 100, 3000); + } +} diff --git a/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js new file mode 100644 index 0000000000000000000000000000000000000000..b99b9b855539ba1b7f3d600da522b7e89308f372 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/bbb/pubsub/bbb-gw.js @@ -0,0 +1,117 @@ +/** + * @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: + this.emit(C.GATEWAY_MESSAGE, msg); + } + } + else { + 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) { + // Here this is the 'ws' instance + this.sendMessage(data); + } +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js new file mode 100644 index 0000000000000000000000000000000000000000..653aada87fa2f5483643880c0e3fc6226ccccf80 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/ConnectionManager.js @@ -0,0 +1,101 @@ +/* + * 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) => { + 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) => { + this._emitter.emit('response', data); + }); + + video.on(C.REDIS_MESSAGE, (data) => { + this._emitter.emit('response', data); + }); + + console.log(' [ConnectionManager] Successfully subscribed to processes redis channels'); + } + catch (err) { + console.log(' [ConnectionManager] ' + err); + this._stopAll; + } + } + + _stopSession(sessionId) { + } + + _stopAll() { + } +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js b/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js new file mode 100644 index 0000000000000000000000000000000000000000..8ec5a30fac3abeb8eda002809665d17662757929 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/HttpServer.js @@ -0,0 +1,30 @@ +"use strict"; + +const http = require("http"); +const fs = require("fs"); +const config = require('config'); + +module.exports = class HttpServer { + + constructor() { + //const privateKey = fs.readFileSync('sslcert/server.key', 'utf8'); + //const certificate = fs.readFileSync('sslcert/server.crt', 'utf8'); + //const credentials = {key: privateKey, cert: certificate}; + + this.port = config.get('clientPort'); + + this.server = http.createServer((req,res) => { + // + }); + } + + getServerObject() { + return this.server; + } + + listen(callback) { + console.log(' [HttpServer] Listening in port ' + this.port); + this.server.listen(this.port, callback); + } + +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js b/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js new file mode 100644 index 0000000000000000000000000000000000000000..84022e268838c571893d3bf0c2b6f5d092584dbf --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/MessageValidator.js @@ -0,0 +1,81 @@ +const Joi = require('joi'); + +let instance = null; + +module.exports = class MessageParser { + constructor() { + if(!instance){ + instance = this; + } + return instance; + } + + static const schema { + startScreenshare: Joi.object().keys({ + sdpOffer : Joi.string().required(), + vh: Joi.number().required(), + vw: Joi.number().required() + }), + + startVideo: Joi.object().keys({ + internalMeetingId: joi.string().required(), + callerName : Joi.string().required(), + }), + + startAudio: Joi.object().keys({ + internalMeetingId: joi.string().required(), + callerName : Joi.string().required(), + }), + + playStart: Joi.object().keys({ + }), + + playStop: Joi.object().keys.({ + }), + + stop: Joi.object().keys({ + }), + + onIceCandidate: Joi.object().keys({ + internalMeetingId: joi.string().required(), + candidate: Joi.object().required(), + }), + } + + static const messageTemplate Joi.object().keys({ + id: Joi.string().required(), + type: joi.string().required(), + role: joi.string().required(), + }) + + static const validateMessage (msg) { + let res = Joi.validate(msg, messageTemplate, {allowUnknown: true}); + + if (!res.error) { + res = Joi.validate(msg, schema[msg.id]); + } + + return res; + } + + _parse (message) { + let parsed = { id: '' }; + + try { + parsed = JSON.parse(message); + } catch (e) { + console.error(e); + } + + let res = validateMessage(parsed); + + if (res.error) { + parsed.validMessage = false; + parsed.errors = res.error; + } else { + parsed.validMessage = true; + } + + return parsed; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js new file mode 100644 index 0000000000000000000000000000000000000000..6c109baf75ac71b9c54a12f1ccca3f988a46ff90 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/RedisConnectionManager.js @@ -0,0 +1,34 @@ +'use strict'; + +// incomplete + +module.exports = class RedisConnectionManager { + + constructor(options) { + + this._client = redis.createClient({options}); + this._pubchannel = options.pubchannel; + this._subchannel = optiosn.subchannel; + + if (options.pubchannel) { + this._client.on() + } + + if (options.subchannel) { + this._client.on() + } + + this._client.on() + // pub + + } + + setEventEmitter(emitter) { + this.emitter = emitter; + } + + _onMessage() { + + } + +} diff --git a/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js new file mode 100644 index 0000000000000000000000000000000000000000..17ea6c3a1d0b672b48600fec053361eeb056bdc7 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/connection-manager/WebsocketConnectionManager.js @@ -0,0 +1,170 @@ +'use strict'; + +const ws = require('ws'); +const C = require('../bbb/messages/Constants'); + +// initialization +let connectionIDCounter = 0; + +// when handling a new connection + + +ws.prototype.setErrorCallback = function(callback) { + + this._errorCallback = callback; +}; + +ws.prototype.sendMessage = function(json) { + + let websocket = this; + + if (this._closeCode === 1000) { + console.log(" [WebsocketConnectionManager] Websocket closed, not sending"); + this._errorCallback("Error: not opened"); + } + + return this.send(JSON.stringify(json), function(error) { + if(error) { + console.log(' [WebsocketConnectionManager] 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.id = connectionIDCounter++; + + console.log(" [WebsocketConnectionManager] New connection with id [ " + ws.id + " ]"); + + ws.on('message', (data) => { + let message = {}; + + try { + message = JSON.parse(data); + message.connectionId = ws.id; + + if (!ws.sessionId) { + ws.sessionId = message.voiceBridge; + } + + if (!ws.route) { + ws.route = message.type; + } + + if (!ws.role) { + ws.role = message.role; + } + } 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); + } + }); + + //ws.on('message', this._onMessage.bind(this)); + ws.setErrorCallback(this._onError.bind(this)); + + ws.on('close', (ev) => { + console.log(' [WebsocketConnectionManager] Closed connection on [' + ws.id + ']'); + let message = { + id: 'close', + type: ws.route, + role: ws.role, + voiceBridge: ws.sessionId, + connectionId: ws.id + } + + this.emitter.emit(C.WEBSOCKET_MESSAGE, message); + + ws = null; + }); + + ws.on('error', (err) => { + console.log(' [WebsocketConnectionManager] Connection error [' + ws.id + ']'); + let message = { + id: 'error', + type: ws.route, + role: ws.role, + voiceBridge: ws.sessionId, + connectionId: ws.id + } + + this.emitter.emit(C.WEBSOCKET_MESSAGE, message); + + ws = null; + }); + + // TODO: should we delete this listener after websocket dies? + this.emitter.on('response', (data) => { + if (ws && ws.id == data.connectionId) { + ws.sendMessage(data); + } + }); + }); + } + + setEventEmitter (emitter) { + this.emitter = emitter; + } + + _onServerResponse (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'); + let message = { + id: 'error', + voiceBridge: ws.sessionId, + connectionId: ws.id + } + this.emitter.emit(C.WEBSOCKET_MESSAGE, message); + } + + _onClose (err) { + console.log(' [WebsocketConnectionManager] Closed connection [' + this.id + ']'); + let message = { + id: 'close', + voiceBridge: this.sessionId, + connectionId: this.id + } + + this.emitter.emit(C.WEBSOCKET_MESSAGE, message); + } + + _stop () { + } +} diff --git a/labs/kurento-screenshare/lib/h264-sdp.js b/labs/bbb-webrtc-sfu/lib/h264-sdp.js similarity index 100% rename from labs/kurento-screenshare/lib/h264-sdp.js rename to labs/bbb-webrtc-sfu/lib/h264-sdp.js diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js b/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..ee03958e42a0d3d54b74e7c76a2a1fef19234b7a --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/CoreProcess.js @@ -0,0 +1,12 @@ +const MCSApiStub = require('./media/MCSApiStub'); + +process.on('uncaughtException', function (error) { + console.log(error.stack); +}); + +process.on('disconnect',function() { + console.log("Parent exited!"); + process.kill(); +}); + +core = new MCSApiStub(); diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js new file mode 100644 index 0000000000000000000000000000000000000000..94f7acc9dc9f89aa14094232af1bb300e47468e7 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/constants/Constants.js @@ -0,0 +1,86 @@ +/* + * (C) Copyright 2016 Mconf Tecnologia (http://mconf.com/) + */ + +/** + * @classdesc + * Message constants for the communication with BigBlueButton + * @constructor + */ + +'use strict' + +exports.ALL = 'ALL' + +exports.LOG_LEVEL = {} +exports.LOG_LEVEL.DEBUG = 0 +exports.LOG_LEVEL.INFO = 1 +exports.LOG_LEVEL.WARN = 2 +exports.LOG_LEVEL.ERROR = 3 +exports.LOG_LEVEL.OFF = 100 + +exports.STATUS = {} +exports.STATUS.STARTED = "STARTED" +exports.STATUS.STOPPED = "STOPPED" +exports.STATUS.RUNNING = "RUNNING'" +exports.STATUS.STARTING = "STARTING" +exports.STATUS.STOPPING = "STOPPING" +exports.STATUS.RESTARTING = "RESTARTING" + +exports.USERS = {} +exports.USERS.SFU = "SFU" +exports.USERS.MCU = "MCU" + +exports.MEDIA_TYPE = {} +exports.MEDIA_TYPE.WEBRTC = "WebRtcEndpoint" +exports.MEDIA_TYPE.RTP= "RtpEndpoint" +exports.MEDIA_TYPE.URI = "PlayerEndpoint" + +// Observer Constants +exports.EVENT = {} +exports.EVENT.DIAL_EVENT = "BRIDGE_DIAL" +exports.EVENT.HANGUP_EVENT = "BRIDGE_HANGUP" +exports.EVENT.SESSION_ID_EVENT = "SESSION_ID" +exports.EVENT.AUDIO_SESSION_TERMINATED = "AUDIO_SESSION_TERMINATED" + +// Media server state changes +exports.EVENT.NEW_SESSION = "NewSession" +exports.EVENT.MEDIA_STATE = {}; +exports.EVENT.MEDIA_STATE.MEDIA_EVENT = "MediaEvent" +exports.EVENT.MEDIA_STATE.CHANGED = "MediaStateChanged" +exports.EVENT.MEDIA_STATE.FLOW_OUT = "MediaFlowOutStateChange" +exports.EVENT.MEDIA_STATE.FLOW_IN = "MediaFlowInStateChange" +exports.EVENT.MEDIA_STATE.ENDOFSTREAM = "EndOfStream" +exports.EVENT.MEDIA_STATE.ICE = "OnIceCandidate" + + + +// RTP params +exports.SDP = {}; +exports.SDP.PARAMS = "params" +exports.SDP.MEDIA_DESCRIPTION = "media_description" +exports.SDP.LOCAL_IP_ADDRESS = "local_ip_address" +exports.SDP.LOCAL_VIDEO_PORT = "local_video_port" +exports.SDP.DESTINATION_IP_ADDRESS = "destination_ip_address" +exports.SDP.DESTINATION_VIDEO_PORT = "destination_video_port" +exports.SDP.REMOTE_VIDEO_PORT = "remote_video_port" +exports.SDP.CODEC_NAME = "codec_name" +exports.SDP.CODEC_ID = "codec_id" +exports.SDP.CODEC_RATE = "codec_rate" +exports.SDP.RTP_PROFILE = "rtp_profile" +exports.SDP.SEND_RECEIVE = "send_receive" +exports.SDP.FRAME_RATE = "frame_rate" + +// Strings +exports.STRING = {} +exports.STRING.ANONYMOUS = "ANONYMOUS" +exports.STRING.FS_USER_AGENT_STRING = "Freeswitch_User_Agent" +exports.STRING.XML_MEDIA_FAST_UPDATE = '<?xml version=\"1.0\" encoding=\"utf-8\" ?>' + + '<media_control>' + + '<vc_primitive>' + + '<to_encoder>' + + '<picture_fast_update>' + + '</picture_fast_update>' + + '</to_encoder>' + + '</vc_primitive>' + + '</media_control>' diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js new file mode 100644 index 0000000000000000000000000000000000000000..9c42ec7efe33f34cbba1536c409fd0943408d4c3 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MCSApiStub.js @@ -0,0 +1,143 @@ +'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) { + try { + const answer = await this._mediaController.join(room, type, params); + return Promise.resolve(answer); + } + catch (err) { + console.log(err); + Promise.reject(err); + } + } + + async leave (roomId, userId) { + try { + const answer = await this._mediaController.leave(roomId, 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 { + await this._mediaController.unpublish(mediaId); + return Promise.resolve(); + } + 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, mediaId) { + try { + await this._mediaController.unsubscribe(user, mediaId); + return Promise.resolve(); + } + 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 { + const ack = await this._mediaController.addIceCandidate(mediaId, candidate); + return Promise.resolve(ack); + } + catch (err) { + console.log(err); + Promise.reject(); + } + } + setStrategy (strategy) { + // TODO + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js new file mode 100644 index 0000000000000000000000000000000000000000..b446aa2d8ebd1433568a10a934dad0f7988dc92f --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/MediaController.js @@ -0,0 +1,360 @@ +'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[roomId]; + } + + async join (roomId, type, params) { + console.log("[mcs] Join room => " + roomId + ' as ' + type); + try { + let session; + const room = await this.createRoomMCS(roomId); + this._rooms[roomId] = room; + const user = await this.createUserMCS(roomId, type, params); + room.setUser(user.id); + this._users[user.id] = user; + if (params.sdp) { + session = user.addSdp(params.sdp); + } + if (params.uri) { + session = user.addUri(params.sdp); + } + + console.log("[mcs] Resolving user " + user.id); + return Promise.resolve(user.id); + } + catch (err) { + console.log("[mcs] JOIN ERROR " + err); + return Promise.reject(new Error(err)); + } + } + + async leave (roomId, userId) { + try { + console.log(" [mcs] User => " + userId + " wants to leave "); + const room = this.getRoom(roomId); + const user = this.getUserMCS(userId); + + if (!user || !room) { + return Promise.resolve(); + } + + const killedSessions = await user.leave(); + + for (var session in killedSessions) { + this._mediaSessions[killedSessions[session]] = null; + } + + room.destroyUser(user.id); + this._users[user.id] = null; + + + return Promise.resolve(); + } + catch (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); + session.on('SESSION_STOPPED', (pubId) => { + console.log(" [mcs] SESSION ", session.id, " STOPPED "); + if(pubId === session.id) { + for (var sub in session.subscribedSessions) { + console.log(" [mcs] Unsubscribing session ", sub); + let subSession = this._mediaSessions[sub]; + if (subSession) { + subSession.stop(); + this._mediaSessions[sub] = null; + } + } + } + }); + + 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, sourceId, type, 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); + sourceSession.subscribedSessions.push(session.id); + console.log(" [mcs] ", sourceSession.id, " subscribers list ", sourceSession.subscribedSessions); + 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 session = this._mediaSessions[mediaId]; + const user = this.getUserMCS(userId); + + if(typeof session === 'undefined' || !session) { + return Promise.resolve(); + } + + + 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); + if (user) { + const answer = await user.unsubscribe(mediaId); + this._mediaSessions[mediaId] = null; + } + return Promise.resolve(); + } + 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 { + const ack = await session.addIceCandidate(candidate); + return Promise.resolve(ack); + } + catch (err) { + console.log(err); + return Promise.reject(err); + } + } + + /** + * Creates an empty {Room} room and indexes it + * @param {String} roomId + */ + async createRoomMCS (roomId) { + let self = this; + + console.log(" [media] Creating new room with ID " + roomId); + + if(!self._rooms[roomId]) { + self._rooms[roomId] = new Room(roomId); + } + + return Promise.resolve(self._rooms[roomId]); + } + + /** + * Creates an {User} of type @type + * @param {String} roomId + */ + createUserMCS (roomId, type, params) { + let self = this; + let user; + console.log(" [media] Creating a new user[" + type + "]"); + + switch (type) { + case C.USERS.SFU: + user = new SfuUser(roomId, type, this.emitter, params.userAgentString, params.sdp); + break; + case C.USERS.MCU: + console.log(" [media] createUserMCS MCU TODO"); + break; + default: + console.log(" [controller] Unrecognized user type"); + } + + if(!self._users[user.id]) { + self._users[user.id] = user; + } + + return Promise.resolve(user); + } + + getUserMCS (userId) { + return this._users[userId]; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js new file mode 100644 index 0000000000000000000000000000000000000000..6693beb97e4df4f160bc1b3beee78edb17bf532c --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/media/media-server.js @@ -0,0 +1,272 @@ +'use strict' + +const C = require('../constants/Constants.js'); +const config = require('config'); +const mediaServerClient = require('kurento-client'); +const util = require('util'); +const EventEmitter = require('events').EventEmitter; + +let instance = null; + +/* Public members */ +module.exports = class MediaServer extends EventEmitter { + constructor(serverUri) { + if(!instance){ + super(); + this._serverUri = serverUri; + this._mediaPipelines = {}; + this._mediaElements= {}; + this._mediaServer; + instance = this; + } + + return instance; + } + + async init () { + if (typeof this._mediaServer === 'undefined' || !this._mediaServer) { + this._mediaServer = await this._getMediaServerClient(this._serverUri); + } + } + + _getMediaServerClient (serverUri) { + return new Promise((resolve, reject) => { + mediaServerClient(serverUri, (error, client) => { + if (error) { + reject(error); + } + console.log(" [media] Retrieved media server client => " + client); + resolve(client); + }); + }); + } + + _getMediaPipeline (conference) { + return new Promise((resolve, reject) => { + if (this._mediaPipelines[conference]) { + console.log(' [media] Pipeline already exists. ' + JSON.stringify(this._mediaPipelines, null, 2)); + resolve(this._mediaPipelines[conference]); + } + else { + this._mediaServer.create('MediaPipeline', (error, pipeline) => { + if (error) { + console.log(error); + reject(error); + } + this._mediaPipelines[conference] = pipeline; + resolve(pipeline); + }); + } + }); + } + + _releasePipeline (pipelineId) { + let mediaPipeline = this._mediaPipelines[pipelineId]; + + if (typeof mediaPipeline !== 'undefined' && typeof mediaPipeline.release === 'function') { + mediaElement.release(); + } + } + + _createElement (pipeline, type) { + return new Promise((resolve, reject) => { + pipeline.create(type, (error, mediaElement) => { + if (error) { + return reject(error); + } + console.log(" [MediaController] Created [" + type + "] media element: " + mediaElement.id); + this._mediaElements[mediaElement.id] = mediaElement; + return resolve(mediaElement); + }); + }); + } + + + async createMediaElement (conference, type) { + try { + const pipeline = await this._getMediaPipeline(conference); + const mediaElement = await this._createElement(pipeline, type); + return Promise.resolve(mediaElement.id); + } + catch (err) { + return Promise.reject(new Error(err)); + } + } + + async connect (sourceId, sinkId, type) { + let source = this._mediaElements[sourceId]; + let sink = this._mediaElements[sinkId]; + + if (source && sink) { + return new Promise((resolve, reject) => { + switch (type) { + case 'ALL': + source.connect(sink, (error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + break; + + + case 'AUDIO': + case 'VIDEO': + source.connect(sink, (error) => { + if (error) { + return reject(error); + } + return resolve(); + }); + break; + + default: return reject(" [media] Invalid connect type"); + } + }); + } + else { + return Promise.reject(" [media] Failed to connect " + type + ": " + sourceId + " to " + sinkId); + } + } + + stop (elementId) { + let mediaElement = this._mediaElements[elementId]; + if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') { + console.log(" [media] Releasing endpoint => " + elementId); + mediaElement.release(); + this._mediaElements[elementId] = null; + } + } + + + 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) { + event.candidate = mediaServerClient.getComplexType('IceCandidate')(event.candidate); + } + this.emit(C.EVENT.MEDIA_STATE.MEDIA_EVENT+elementId , {eventTag, event}); + }); + } + } + + notifyMediaState (elementId, eventTag, event) { + this.emit(C.MEDIA_STATE.MEDIA_EVENT , {elementId, eventTag, event}); + } +}; diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9eb77cbea7e652595f8d8ff023f6b997ad5b1c --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/Room.js @@ -0,0 +1,30 @@ +/** + * @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]; + } + + setUser (user) { + if (typeof this._users[user.id] == 'undefined' || + !this._users[user.id]) { + this._users[user.id] = {}; + } + this._users[user.id] = user; + } + + destroyUser(userId) { + this._users[userId] = null;; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js new file mode 100644 index 0000000000000000000000000000000000000000..e532a3d7089e2dfafe12f1842bd7d3da10e31a03 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SdpSession.js @@ -0,0 +1,119 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const C = require('../constants/Constants'); +const SdpWrapper = require('../utils/SdpWrapper'); +const rid = require('readable-id'); +const EventEmitter = require('events').EventEmitter; +const MediaServer = require('../media/media-server'); +const config = require('config'); +const kurentoUrl = config.get('kurentoUrl'); + +module.exports = class SdpSession extends EventEmitter { + constructor(emitter, sdp = null, room, type = 'WebRtcEndpoint') { + super(); + this.id = rid(); + 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; + this.subscribedSessions = []; + } + + 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(() => { + 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.getPlainSdp()); + + 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._mediaElement); + this._status = C.STATUS.STOPPED; + console.log(" [SdpSession] Session ", this.id, " is going to stop..."); + this.emit('SESSION_STOPPED', this.id); + 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 { + await this._MediaServer.addIceCandidate(this._mediaElement, candidate); + Promise.resolve(); + } + catch (err) { + Promise.reject(err); + } + } + + addMediaEventListener (type, mediaId) { + this._MediaServer.addMediaEventListener (type, mediaId); + } + + handleError (err) { + console.log(err); + this._status = C.STATUS.STOPPED; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js new file mode 100644 index 0000000000000000000000000000000000000000..86cced861c78564e0ac7789395a7e599593a503d --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/SfuUser.js @@ -0,0 +1,187 @@ +/** + * @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); + session.on("SESSION_STOPPED", (sessId) => { + console.log(" [SfuUser] Session ", sessId, "stopped, cleaning it..."); + if (sessId === session.id) { + this._mediaSessions[sessId] = null; + } + }); + + 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(); + return Promise.resolve(answer); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + async subscribe (sdp, type, mediaId) { + try { + const session = await this.addSdp(sdp, type); + await this.startSession(session.id); + await this.connect(session.id, mediaId); + Promise.resolve(session); + } + 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 (mediaId) { + try { + await this.stopSession(mediaId); + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async unpublish (mediaId) { + try { + await this.stopSession(mediaId); + Promise.resolve(); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + async stopSession (sessionId) { + console.log(" [SfuUser] Stopping session => " + sessionId); + let session = this._mediaSessions[sessionId]; + + try { + await session.stop(); + this._mediaSessions[sessionId] = 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")); + } + } + + async leave () { + let sessions = Object.keys(this._mediaSessions); + console.log(" [SfuUser] User sessions will be killed"); + console.log(sessions); + + try { + for (var session in sessions) { + await this.stopSession(sessions[session]); + } + + return Promise.resolve(sessions); + } + catch (err) { + this.handleError(err); + Promise.reject(new Error(err)); + } + } + + handleError (err) { + console.log(err); + this._status = C.STATUS.STOPPED; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js new file mode 100644 index 0000000000000000000000000000000000000000..8193a56524e10a0958248e3abb95a0106e6312c2 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/UriSession.js @@ -0,0 +1,73 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const C = require('../constants/Constants'); +const rid = require('readable-id'); +const EventEmitter = require('events').EventEmitter; +const MediaServer = require('../media/media-server'); + +module.exports = class UriSession extends EventEmitter { + constructor(uri = null) { + super(); + this.id = rid(); + this._status = C.STATUS.STOPPED; + this._uri; + if (uri) { + this.setUri(uri); + } + } + + setUri (uri) { + this._uri = uri; + } + + async start () { + this._status = C.STATUS.STARTING; + try { + const mediaElement = await MediaServer.createMediaElement(this.id, C.MEDIA_TYPE.URI); + console.log("start/cme"); + await MediaServer.play(this.id); + this._status = C.STATUS.STARTED; + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + // TODO move to parent Session + async stop () { + this._status = C.STATUS.STOPPING; + try { + await MediaServer.stop(this.id); + this._status = C.STATUS.STOPPED; + return Promise.resolve(); + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + // TODO move to parent Session + async connect (sinkId) { + try { + await MediaServer.connect(this.id, sinkId); + return Promise.resolve() + } + catch (err) { + this.handleError(err); + return Promise.reject(new Error(err)); + } + } + + handleError (err) { + console.log(err); + this._status = C.STATUS.STOPPED; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js new file mode 100644 index 0000000000000000000000000000000000000000..919a505189bcbe977d9e0801310ff6382a351bd5 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/model/User.js @@ -0,0 +1,18 @@ +/** + * @classdesc + * Model class for external devices + */ + +'use strict' + +const rid = require('readable-id'); +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 = rid(); + this.userAgentString = userAgentString; + } +} diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..eea0d58895e7424e145df30a93d7a20fe6db1a29 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/SdpWrapper.js @@ -0,0 +1,256 @@ +/** + * @classdesc + * Utils class for manipulating SDP + */ + +'use strict' + +var config = require('config'); +var transform = require('sdp-transform'); + +module.exports = class SdpWrapper { + constructor(sdp) { + this._plainSdp = sdp; + this._jsonSdp = transform.parse(sdp); + this._mediaLines = {}; + this._mediaCapabilities = {}; + this._profileThreshold = "ffffff"; + } + + setSdp (sdp) { + this._plainSdp = sdp; + this._jsonSdp = transform.parse(sdp); + } + + getPlainSdp () { + return this._plainSdp; + } + + getJsonSdp () { + return this._jsonSdp; + } + + removeFmtp () { + return this._plainSdp.replace(/(a=fmtp:).*/g, ''); + } + + replaceServerIpv4 (ipv4) { + return this._plainSdp.replace(/(IP4\s[0-9.]*)/g, 'IP4 ' + ipv4); + } + + getCallId () { + return this._plainSdp.match(/(call-id|i):\s(.*)/i)[2]; + } + + /** + * Given a SDP, test if there is more than on video description + * @param {string} sdp The Session Descriptor + * @return {boolean} true if there is more than one video description, else false + */ + hasAudio () { + return /(m=audio)/i.test(this._plainSdp); + } + + /** + * Given a SDP, test if there is a video description in it + * @param {string} sdp The Session Descriptor + * @return {boolean} true if there is a video description, else false + */ + hasVideo (sdp) { + return /(m=video)/i.test(sdp); + } + + /** + * Given a SDP, test if there is more than on video description + * @param {string} sdp The Session Descriptor + * @return {boolean} true if there is more than one video description, else false + */ + hasMultipleVideo (sdp) { + return /(m=video)([\s\S]*\1){1,}/i.test(sdp); + } + + /** + * Given a SDP, return its Session Description + * @param {string} sdp The Session Descriptor + * @return {string} Session description (SDP until the first media line) + */ + getSessionDescription (sdp) { + return sdp.match(/[\s\S]+?(?=m=audio|m=video)/i); + } + + removeSessionDescription (sdp) { + return sdp.match(/(?=[\s\S]+?)(m=audio[\s\S]+|m=video[\s\S]+)/i)[1]; + } + + getVideoParameters (sdp) { + var res = transform.parse(sdp); + console.log(" [sdp] getVideoParameters => " + JSON.stringify(res, null, 2)); + var params = {}; + params.fmtp = ""; + params.codecId = 96; + var pt = 0; + for(var ml of res.media) { + if(ml.type == 'video') { + if (typeof ml.fmtp[0] != 'undefined' && ml.fmtp) { + params.codecId = ml.fmtp[0].payload; + params.fmtp = ml.fmtp[0].config; + console.log(" [sdp] getVideoParameters fmtp => " + JSON.stringify(params)); + return params; + } + } + } + return params; + } + + /** + * Given a SDP, return its Content Description + * @param {string} sdp The Session Descriptor + * @return {string} Content Description (SDP after first media description) + */ + getContentDescription (sdp) { + var res = transform.parse(sdp); + res.media = res.media.filter(function (ml) { return ml.type == "video" }); + var mangledSdp = transform.write(res); + if(typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") { + return mangledSdp; + } + else + return sdp; + } + + /** + * Given a SDP, return its first Media Description + * @param {string} sdp The Session Descriptor + * @return {string} Content Description (SDP after first media description) + */ + getAudioDescription (sdp) { + var res = transform.parse(sdp); + res.media = res.media.filter(function (ml) { return ml.type == "audio" }); + // Hack: Some devices (Snom, Pexip) send crypto with RTP/AVP + // That is forbidden according to RFC3711 and FreeSWITCH rebukes it + res = this.removeTransformCrypto(res); + var mangledSdp = transform.write(res); + this.getSessionDescription(mangledSdp); + if(typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") { + return mangledSdp; + } + else { + return sdp; + } + } + + /** + * Given a SDP, return its first Media Description + * @param {string} sdp The Session Descriptor + * @return {string} Content Description (SDP after first media description) + */ + getMainDescription () { + var res = transform.parse(this._plainSdp); + // Filter should also carry && ml.invalid[0].value != 'content:slides'; + // when content is enabled + res.media = res.media.filter(function (ml) { return ml.type == "video"}); //&& ml.invalid[0].value != 'content:slides'}); + var mangledSdp = transform.write(res); + if (typeof mangledSdp != undefined && mangledSdp && mangledSdp != "") { + console.log(" [sdp] MAIN VIDEO SDP => " + mangledSdp); + return mangledSdp; + } + else { + return sdp; + } + } + + /** + * Given a JSON SDP, remove associated crypto 'a=' lines from media lines + * WARNING: HACK MADE FOR FreeSWITCH ~1.4 COMPATIBILITY + * @param {Object} sdp The Session Descriptor JSON + * @return {Object} JSON SDP without crypto lines + */ + removeTransformCrypto (sdp) { + for(var ml of sdp.media) { + delete ml['crypto']; + } + return sdp; + } + + removeHighQualityFmtps (sdp) { + let res = transform.parse(sdp); + let maxProfileLevel = config.get('kurento.maximum_profile_level_hex'); + let pt = 0; + let idx = 0; + for(var ml of res.media) { + if(ml.type == 'video') { + for(var fmtp of ml.fmtp) { + let fmtpConfig = transform.parseParams(fmtp.config); + let profileId = fmtpConfig['profile-level-id']; + if(typeof profileId !== 'undefined' && parseInt(profileId, 16) > parseInt(maxProfileLevel, 16)) { + console.log(" [sdp] Filtering profile " + parseInt(profileId, 16) + ". Higher than max "+ parseInt(maxProfileLevel, 16)); + pt = fmtp.payload; + delete ml.fmtp[idx]; + ml.rtp = ml.rtp.filter((rtp) => { return rtp.payload != pt}); + } + else { + // Remove fmtp further specifications + //let configProfile = "profile-level-id="+profileId; + //fmtp.config = configProfile; + } + idx++; + } + } + } + var mangledSdp = transform.write(res); + return mangledSdp; + } + + async processSdp () { + let description = this._plainSdp; + //if(config.get('kurento.force_low_resolution')) { + // description = this.removeFmtp(description); + //} + + description = description.toString().replace(/telephone-event/, "TELEPHONE-EVENT"); + + this._mediaCapabilities.hasVideo = this.hasVideo(description); + this._mediaCapabilities.hasAudio = this.hasAudio(description); + this._mediaCapabilities.hasContent = this.hasMultipleVideo(description); + this.sdpSessionDescription = this.getSessionDescription(description); + this.audioSdp = this.getAudioDescription(description); + this.mainVideoSdp = this.getMainDescription(description); + //this.mainVideoSdp = this.removeHighQualityFmtps(this.mainVideoSdp); + this.contentVideoSdp = this.getContentDescription(description); + + return; + } + + /* DEVELOPMENT METHODS */ + _disableMedia (sdp) { + return sdp.replace(/(m=application\s)\d*/g, "$10"); + }; + + /** + * Given a SDP, add Floor Control response + * @param {string} sdp The Session Descriptor + * @return {string} A new Session Descriptor with Floor Control + */ + _addFloorControl (sdp) { + return sdp.replace(/a=inactive/i, 'a=sendrecv\r\na=floorctrl:c-only\r\na=setup:active\r\na=connection:new'); + } + + /** + * Given a SDP, add Floor Control response to reinvite + * @param {string} sdp The Session Descriptor + * @return {string} A new Session Descriptor with Floor Control Id + */ + _addFloorId (sdp) { + sdp = sdp.replace(/(a=floorctrl:c-only)/i, '$1\r\na=floorid:1 m-stream:3'); + return sdp.replace(/(m=video.*)([\s\S]*?m=video.*)([\s\S]*)/i, '$1\r\na=content:main\r\na=label:1$2\r\na=content:slides\r\na=label:3$3'); + } + + /** + * Given the string representation of a Session Descriptor, remove it's video + * @param {string} sdp The Session Descriptor + * @return {string} A new Session Descriptor without the video + */ + _removeVideoSdp (sdp) { + return sdp.replace(/(m=video[\s\S]+)/g,''); + }; +}; diff --git a/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..11b06b0bcf49d721be79203de85fe307962b74a7 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/mcs-core/lib/utils/sdp-utils.js @@ -0,0 +1,37 @@ +/** + * @classdesc + * Utils class for SDP generation + */ + +module.exports.generateSdp = function(remote_ip_address, remote_video_port) { + return "v=0\r\n" + + "o=- 0 0 IN IP4 " + remote_ip_address + "\r\n" + + "s=No Name\r\n" + + "c=IN IP4 " + remote_ip_address + "\r\n" + + "t=0 0\r\n" + + "m=video " + remote_video_port + " RTP/AVP 96\r\n" + + "a=rtpmap:96 H264/90000\r\n" + + "a=ftmp:96 packetization-mode=0\r\n"; +} + +/** + * Generates a video SDP given the media specs + * @param {string} sourceIpAddress The source IP address of the media + * @param {string} sourceVideoPort The source video port of the media + * @param {string} codecId The ID of the codec + * @param {string} sendReceive The SDP flag of the media flow + * direction, 'sendonly', 'recvonly' or 'sendrecv' + * @param {String} rtpProfile The RTP profile of the RTP Endpoint + * @param {String} codecName The name of the codec used for the RTP + * Endpoint + * @param {String} codecRate The codec rate + * @return {string} The Session Descriptor for the media + */ +module.exports.generateVideoSdp = function (sourceIpAddress, sourceVideoPort, codecId, sendReceive, rtpProfile, codecName, codecRate, fmtp) { + return 'm=video ' + sourceVideoPort + ' ' + rtpProfile + ' ' + codecId + '\r\n' + + 'a=' + sendReceive + '\r\n' + + 'c=IN IP4 ' + sourceIpAddress + '\r\n' + + 'a=rtpmap:' + codecId + ' ' + codecName + '/' + codecRate + '\r\n' + + 'a=fmtp:' + codecId + ' ' + fmtp + '\r\n'; +}; + diff --git a/labs/kurento-screenshare/lib/media-handler.js b/labs/bbb-webrtc-sfu/lib/media-handler.js similarity index 69% rename from labs/kurento-screenshare/lib/media-handler.js rename to labs/bbb-webrtc-sfu/lib/media-handler.js index 2d1ab3815cb3d583be7a115c12ab6a1c6626fa92..5c611ec395d7a994c162f5e1b0e11088f5e6b233 100644 --- a/labs/kurento-screenshare/lib/media-handler.js +++ b/labs/bbb-webrtc-sfu/lib/media-handler.js @@ -1,42 +1,6 @@ var config = require('config'); -var kurento = require('kurento-client'); var Constants = require('./bbb/messages/Constants'); -var kurentoClient = null; -var mediaPipelines = {}; - -module.exports.getKurentoClient = function(kurentoUrl, callback) { - if (kurentoClient !== null) { - return callback(null, kurentoClient); - } - - kurento(kurentoUrl, function(error, _kurentoClient) { - if (error) { - console.log("Could not find media server at address " + kurentoUrl); - return callback("Could not find media server at address" + kurentoUrl + ". Exiting with error " + error); - } - - console.log(" [MediaHandler] Initiating kurento client. Connecting to: " + kurentoUrl); - - kurentoClient = _kurentoClient; - callback(null, kurentoClient); - }); -} - -module.exports.getMediaPipeline = function(id, callback) { - console.log(' [MediaHandler] Creating media pipeline for ' + id); - - if (mediaPipelines[id]) { - console.log(' [media] Pipeline already exists.'); - callback(null, mediaPipelines[id]); - } else { - kurentoClient.create('MediaPipeline', function(err, pipeline) { - mediaPipelines[id] = pipeline; - return callback(err, pipeline); - }); - } -} - 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" @@ -76,7 +40,7 @@ module.exports.generateStreamUrl = function (address, meeting, path) { return "rtmp://" + address + "/video-broadcast/" + meeting + "/" + path; } -module.exports.generateTranscoderParams = function (localIp, destIp, sendPort, recvPort, input, streamType, transcoderType, codec, callername) { +module.exports.generateTranscoderParams = function (localIp, destIp, sendPort, recvPort, input, streamType, transcoderType, codec, callername, voiceConf) { var rtpParams = {}; rtpParams[Constants.LOCAL_IP_ADDRESS] = localIp; rtpParams[Constants.LOCAL_VIDEO_PORT] = sendPort; @@ -87,6 +51,7 @@ module.exports.generateTranscoderParams = function (localIp, destIp, sendPort, r rtpParams[Constants.TRANSCODER_TYPE] = transcoderType; rtpParams[Constants.TRANSCODER_CODEC] = codec; rtpParams[Constants.CALLERNAME] = callername; + rtpParams[Constants.VOICE_CONF] = voiceConf; return rtpParams; } diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js new file mode 100644 index 0000000000000000000000000000000000000000..024bb7bf4e9eaf5a1ff8dcf5a7a888bcf9f6e27b --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareManager.js @@ -0,0 +1,190 @@ +/* + * Lucas Fialho Zawacki + * Paulo Renato Lanzarin + * (C) Copyright 2017 Bigbluebutton + * + */ + +"use strict"; + +const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw'); +const Screenshare = require('./screenshare'); +const C = require('../bbb/messages/Constants'); +// Global variables + +module.exports = class ScreenshareManager { + constructor (logger) { + this._logger = logger; + this._clientId = 0; + + this._sessions = {}; + this._screenshareSessions = {}; + + this._bbbGW = new BigBlueButtonGW("MANAGER"); + this._redisGateway; + } + + async start() { + try { + this._redisGateway = await this._bbbGW.addSubscribeChannel(C.TO_SCREENSHARE); + const transcode = await this._bbbGW.addSubscribeChannel(C.FROM_BBB_TRANSCODE_SYSTEM_CHAN); + this._redisGateway.on(C.REDIS_MESSAGE, this._onMessage.bind(this)); + console.log(' [ScreenshareManager] Successfully subscribed to redis channel'); + } + catch (error) { + console.log(' [ScreenshareManager] Could not connect to transcoder redis channel, finishing app...'); + console.log(error); + this.stopAll(); + } + } + + _onMessage(_message) { + console.log(' [ScreenshareManager] Received message => '); + let session; + let message = _message; + + let sessionId = message.voiceBridge; + let connectionId = message.connectionId; + + if(this._screenshareSessions[sessionId]) { + session = this._screenshareSessions[sessionId]; + } + + switch (message.id) { + + case 'presenter': + + // Checking if there's already a Screenshare session started + // because we shouldn't overwrite it + + if (!this._screenshareSessions[message.voiceBridge]) { + this._screenshareSessions[message.voiceBridge] = {} + this._screenshareSessions[message.voiceBridge] = session; + } + + if(session) { + break; + } + + session = new Screenshare(connectionId, this._bbbGW, + sessionId, connectionId, message.vh, message.vw, + message.internalMeetingId); + + this._screenshareSessions[sessionId] = {} + this._screenshareSessions[sessionId] = session; + + // starts presenter by sending sessionID, websocket and sdpoffer + session._startPresenter(sessionId, message.sdpOffer, (error, sdpAnswer) => { + console.log(" [ScreenshareManager] Started presenter " + sessionId); + if (error) { + this._bbbGW.publish(JSON.stringify({ + connectionId: session._id, + id : 'presenterResponse', + response : 'rejected', + message : error + }), C.FROM_SCREENSHARE); + return error; + } + + this._bbbGW.publish(JSON.stringify({ + connectionId: session._id, + id : 'presenterResponse', + response : 'accepted', + sdpAnswer : sdpAnswer + }), C.FROM_SCREENSHARE); + + console.log(" [ScreenshareManager] [websocket] Sending presenterResponse \n" + sdpAnswer); + }); + break; + + case 'viewer': + console.log(" [ScreenshareManager][viewer] Session output \n " + session); + if (message.sdpOffer && message.voiceBridge) { + if (session) { + session._startViewer(message.connectionId, message.voiceBridge, message.sdpOffer, connectionId, + this._screenshareSessions[message.voiceBridge]._presenterEndpoint); + } else { + // TODO ERROR HANDLING + } + } + break; + + case 'stop': + console.log('[' + message.id + '] connection ' + sessionId); + + if (session) { + session._stop(sessionId); + } else { + console.log(" [stop] Why is there no session on STOP?"); + } + break; + + case 'onIceCandidate': + if (session) { + session.onIceCandidate(message.candidate); + } else { + console.log(" [iceCandidate] Why is there no session on ICE CANDIDATE?"); + } + break; + + case 'viewerIceCandidate': + console.log("[viewerIceCandidate] Session output => " + session); + if (session) { + session.onViewerIceCandidate(message.candidate, connectionId); + } else { + console.log("[iceCandidate] Why is there no session on ICE CANDIDATE?"); + } + break; + + case 'close': + console.log(' [ScreenshareManager] Connection ' + connectionId + ' closed'); + + if (message.role === 'presenter' && this._screenshareSessions[sessionId]) { + console.log(" [ScreenshareManager] Stopping presenter " + sessionId); + this._stopSession(sessionId); + } + if (message.role === 'viewer' && typeof session !== 'undefined') { + console.log(" [ScreenshareManager] Stopping viewer " + sessionId); + session.stopViewer(message.connectionId); + } + break; + + default: + this._bbbGW.publish(JSON.stringify({ + connectionId: session._id? session._id : 'none', + id : 'error', + message: 'Invald message ' + message + }), C.FROM_SCREENSHARE); + break; + } + } + + _stopSession(sessionId) { + console.log(' [>] Stopping session ' + sessionId); + + if (typeof this._screenshareSessions === 'undefined' || typeof sessionId === 'undefined') { + return; + } + + let session = this._screenshareSessions[sessionId]; + if(typeof session !== 'undefined' && typeof session._stop === 'function') { + session._stop(); + } + + delete this._screenshareSessions[sessionId]; + } + + stopAll() { + console.log('\n [x] Stopping everything! '); + + if (typeof this._screenshareSessions === 'undefined') { + return; + } + + let sessionIds = Object.keys(this._screenshareSessions); + + for (let i = 0; i < sessionIds.length; i++) { + this._stopSession(sessionIds[i]); + } + } +}; diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..223bde09dfb129381ca98bb591e1ecb83291af16 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/screenshare/ScreenshareProcess.js @@ -0,0 +1,10 @@ +const ScreenshareManager = require('./ScreenshareManager'); + +let c = new ScreenshareManager(); +c.start(); + +process.on('uncaughtException', function (error) { + console.log(error.stack); +}); + +process.on('disconnect', c.stopAll); diff --git a/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js new file mode 100644 index 0000000000000000000000000000000000000000..df3df06f1c5470a449e64c899ea9995f8e1d4ae8 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/screenshare/screenshare.js @@ -0,0 +1,357 @@ +/* +* Lucas Fialho Zawacki + * Paulo Renato Lanzarin + * (C) Copyright 2017 Bigbluebutton + * + */ + +'use strict' + +// Imports +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 now = moment(); +const MCSApi = require('../mcs-core/lib/media/MCSApiStub'); +const config = require('config'); +const kurentoIp = config.get('kurentoIp'); +const localIpAddress = config.get('localIpAddress'); + +// Global stuff +var sharedScreens = {}; +var rtpEndpoints = {}; + +if (config.get('acceptSelfSignedCertificate')) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED=0; +} + +module.exports = class Screenshare { + constructor(id, bbbgw, voiceBridge, caller = 'caller', vh, vw, meetingId) { + this.mcs = new MCSApi(); + this._id = id; + this._BigBlueButtonGW = bbbgw; + this._presenterEndpoint = null; + this._ffmpegEndpoint = null; + this._voiceBridge = voiceBridge; + this._meetingId = meetingId; + this._caller = caller; + this._streamUrl = ""; + this._vw = vw; + this._vh = vh; + this._presenterCandidatesQueue = []; + this._viewersEndpoint = []; + this._viewersCandidatesQueue = []; + } + + onIceCandidate (_candidate) { + if (this._presenterEndpoint) { + try { + this.flushCandidatesQueue(this._presenterEndpoint, this._presenterCandidatesQueue); + this.mcs.addIceCandidate(this._presenterEndpoint, _candidate); + } + catch (err) { + console.log(err); + } + } + else { + this._presenterCandidatesQueue.push(_candidate); + } + }; + + flushCandidatesQueue (mediaId, queue) { + if (this.mediaId) { + try { + while(queue.length) { + let candidate = queue.shift(); + this.mcs.addIceCandidate(mediaId, candidate); + } + } + catch (err) { + console.log(err); + } + } + } + + mediaStateRtp (event) { + let msEvent = event.event; + + console.log(' [screenshare] ' + msEvent.type + '[' + msEvent.state + ']' + ' for endpoint ' + this._id); + + switch (event.eventTag) { + case "MediaStateChanged": + break; + + case "MediaFlowOutStateChange": + break; + + case "MediaFlowInStateChange": + if (msEvent.state === 'FLOWING') { + this._onRtpMediaFlowing(); + } + else { + this._onRtpMediaNotFlowing(); + } + break; + + default: console.log(" [video] Unrecognized event"); + } + } + + mediaStateWebRtc (event, id) { + let msEvent = event.event; + + console.log(' [screenshare] ' + msEvent.type + '[' + msEvent.state + ']' + ' for endpoint ' + this._id); + + switch (event.eventTag) { + case "OnIceCandidate": + let candidate = msEvent.candidate; + this._BigBlueButtonGW.publish(JSON.stringify({ + connectionId: id, + id : 'iceCandidate', + cameraId: this._id, + candidate : candidate + }), C.FROM_SCREENSHARE); + + break; + + case "MediaStateChanged": + break; + + case "MediaFlowOutStateChange": + break; + + case "MediaFlowInStateChange": + break; + + default: console.log(" [video] Unrecognized event"); + } + } + + async _startPresenter(id, sdpOffer, callback) { + let presenterSdpAnswer, rtpSdpAnswer; + let _callback = callback; + + // Force H264 on Firefox and Chrome + sdpOffer = h264_sdp.transform(sdpOffer); + console.log(" [screenshare] Starting presenter " + id + " at voiceBridge " + this._voiceBridge); + + try { + this.userId = await this.mcs.join(this._meetingId, 'SFU', {}); + console.log(" [video] Join returned => " + this.userId); + } + catch (err) { + console.log(" [video] MCS join returned error => " + err); + return callback(err); + } + + try { + const retSource = await this.mcs.publish(this.userId, this._meetingId, 'WebRtcEndpoint', {descriptor: sdpOffer}); + + this._presenterEndpoint = retSource.sessionId; + sharedScreens[id] = this._presenterEndpoint; + presenterSdpAnswer = retSource.answer; + this.flushCandidatesQueue(this._presenterEndpoint, this._presenterCandidatesQueue); + + this.mcs.on('MediaEvent' + this._presenterEndpoint, (event) => { + this.mediaStateWebRtc(event, this._id) + }); + + console.log(" [video] Publish returned => " + this._presenterEndpoint); + + } + catch (err) { + console.log(" [video] MCS publish returned error => " + err); + return callback(err); + } + + try { + let sendVideoPort = MediaHandler.getVideoPort(); + let rtpSdpOffer = MediaHandler.generateVideoSdp(localIpAddress, sendVideoPort); + + const retRtp = await this.mcs.subscribe(this.userId, sharedScreens[id], 'RtpEndpoint', {descriptor: rtpSdpOffer}); + + this._ffmpegEndpoint = retRtp.sessionId; + rtpEndpoints[id] = this._ffmpegEndpoint; + + let recvVideoPort = retRtp.answer.match(/m=video\s(\d*)/)[1]; + this._rtpParams = MediaHandler.generateTranscoderParams(kurentoIp, localIpAddress, + sendVideoPort, recvVideoPort, this._meetingId, "stream_type_video", C.RTP_TO_RTMP, "copy", this._caller, this._voiceBridge); + + this.mcs.on('MediaEvent' + this._ffmpegEndpoint, this.mediaStateRtp.bind(this)); + + console.log(" [video] Subscribe returned => " + this._ffmpegEndpoint); + + return callback(null, presenterSdpAnswer); + } + catch (err) { + console.log(" [video] MCS subscribe returned error => " + err); + return callback(err); + } + } + + onViewerIceCandidate(candidate, callerName) { + if (this._viewersEndpoint[callerName]) { + try { + this.flushCandidatesQueue(this._viewersEndpoint[callerName], this._viewersCandidatesQueue[callerName]); + this.mcs.addIceCandidate(this._viewersEndpoint[callerName], candidate); + } + catch (err) { + console.log(err); + } + } + else { + if (!this._viewersCandidatesQueue[callerName]) { + this._viewersCandidatesQueue[callerName] = []; + } + this._viewersCandidatesQueue[callerName].push(candidate); + } + } + + async _startViewer(connectionId, voiceBridge, sdp, callerName, presenterEndpoint, callback) { + let _callback = function(){}; + let sdpAnswer, sdpOffer; + console.log(" [screenshare] Starting viewer " + callerName + " for voiceBridge " + this._voiceBridge); + + sdpOffer = h264_sdp.transform(sdp); + sdpOffer = sdp; + + this._viewersCandidatesQueue[callerName] = []; + + + try { + const retSource = await this.mcs.subscribe(this.userId, sharedScreens[voiceBridge], 'WebRtcEndpoint', {descriptor: sdpOffer}); + + this._viewersEndpoint[callerName] = retSource.sessionId; + sdpAnswer = retSource.answer; + this.flushCandidatesQueue(this._viewersEndpoint[callerName], this._viewersCandidatesQueue[callerName]); + this.mcs.on('MediaEvent' + this._viewersEndpoint[callerName], (event) => { + this.mediaStateWebRtc(event, connectionId); + }); + + this._BigBlueButtonGW.publish(JSON.stringify({ + connectionId: connectionId, + id: "viewerResponse", + sdpAnswer: sdpAnswer, + response: "accepted" + }), C.FROM_SCREENSHARE); + + console.log(" Sent sdp message to client with callerName:" + callerName); + console.log(" [screenshare] Subscribe returned => " + this._viewersEndpoint[callerName]); + } + catch (err) { + console.log(" [screenshare] MCS publish returned error => " + err); + return _callback(err); + } + } + + async _stop() { + console.log(' [stop] Releasing endpoints for ' + this.userId); + + this._stopScreensharing(); + + if (this._presenterEndpoint) { + try { + await this.mcs.leave(this._meetingId, this.userId); + sharedScreens[this._presenterEndpoint] = null; + this._candidatesQueue = null; + this._presenterEndpoint = null; + this._ffmpegEndpoint = null; + return; + } + catch (err) { + console.log(err); + return; + } + } + return; + } + + _stopScreensharing() { + let strm = Messaging.generateStopTranscoderRequestMessage(this._meetingId, this._meetingId); + + this._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {}); + + // Interoperability: capturing 1.1 stop_transcoder_reply messages + this._BigBlueButtonGW.once(C.STOP_TRANSCODER_REPLY, (payload) => { + let meetingId = payload[C.MEETING_ID]; + this._stopRtmpBroadcast(meetingId); + }); + + // Capturing stop transcoder responses from the 2x model + this._BigBlueButtonGW.once(C.STOP_TRANSCODER_RESP_2x, (payload) => { + let meetingId = payload[C.MEETING_ID_2x]; + this._stopRtmpBroadcast(meetingId); + }); + + } + + _onRtpMediaFlowing() { + console.log(" [screenshare] Media FLOWING for meeting => " + this._meetingId); + let strm = Messaging.generateStartTranscoderRequestMessage(this._meetingId, this._meetingId, this._rtpParams); + + // Interoperability: capturing 1.1 start_transcoder_reply messages + this._BigBlueButtonGW.once(C.START_TRANSCODER_REPLY, (payload) => { + let meetingId = payload[C.MEETING_ID]; + let output = payload["params"].output; + this._startRtmpBroadcast(meetingId, output); + }); + + // Capturing stop transcoder responses from the 2x model + this._BigBlueButtonGW.once(C.START_TRANSCODER_RESP_2x, (payload) => { + let meetingId = payload[C.MEETING_ID_2x]; + let output = payload["params"].output; + this._startRtmpBroadcast(meetingId, output); + }); + + + this._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {}); + }; + + _stopRtmpBroadcast (meetingId) { + console.log(" [screenshare] _stopRtmpBroadcast for meeting => " + meetingId); + if(this._meetingId === meetingId) { + // TODO correctly assemble this timestamp + let timestamp = now.format('hhmmss'); + let dsrstom = Messaging.generateScreenshareRTMPBroadcastStoppedEvent2x(this._voiceBridge, + this._voiceBridge, this._streamUrl, this._vw, this._vh, timestamp); + this._BigBlueButtonGW.publish(dsrstom, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {}); + } + } + + _startRtmpBroadcast (meetingId, output) { + console.log(" [screenshare] _startRtmpBroadcast for meeting => " + meetingId); + if(this._meetingId === meetingId) { + // TODO correctly assemble this timestamp + let timestamp = now.format('hhmmss'); + this._streamUrl = MediaHandler.generateStreamUrl(localIpAddress, meetingId, output); + let dsrbstam = Messaging.generateScreenshareRTMPBroadcastStartedEvent2x(this._voiceBridge, + this._voiceBridge, this._streamUrl, this._vw, this._vh, timestamp); + + this._BigBlueButtonGW.publish(dsrbstam, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {}); + } + } + + _onRtpMediaNotFlowing() { + console.log(" [screenshare] TODO RTP NOT_FLOWING"); + } + + async stopViewer(id) { + let viewer = this._viewersEndpoint[id]; + console.log(' [stop] Releasing endpoints for ' + viewer); + + if (viewer) { + try { + await this.mcs.unsubscribe(this.userId, this.viewer); + this._viewersCandidatesQueue[id] = null; + this._viewersEndpoint[id] = null; + return; + } + catch (err) { + console.log(err); + return; + } + } + } +}; diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoManager.js b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js new file mode 100755 index 0000000000000000000000000000000000000000..a42a3454e80b40c8909cc7342359207d54f6fef1 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/video/VideoManager.js @@ -0,0 +1,214 @@ +/* + * Lucas Fialho Zawacki + * (C) Copyright 2017 Bigbluebutton + * + */ + +'use strict'; + +const BigBlueButtonGW = require('../bbb/pubsub/bbb-gw'); +const Video = require('./video'); +const C = require('../bbb/messages/Constants'); + +let sessions = {}; + +var clientId = 0; + +let bbbGW = new BigBlueButtonGW("MANAGER"); +let redisGateway; + +bbbGW.addSubscribeChannel(C.TO_VIDEO).then((gw) => { + redisGateway = gw; + redisGateway.on(C.REDIS_MESSAGE, _onMessage); + console.log(' [VideoManager] Successfully subscribed to redis channel ' + C.TO_VIDEO); + +}); + +var _onMessage = function (_message) { + let message = _message; + let sessionId = message.connectionId; + let video; + let role = message.role? message.role : 'any'; + let cameraId = message.cameraId; + let shared = false; + let iceQueue = {}; + + if (message.role == 'share') { + shared = true; + } + + if (!sessions[sessionId]) { + sessions[sessionId] = {}; + } + + logAvailableSessions(); + + switch (role) { + case 'share': + if (message.cameraId && typeof sessions[sessionId][message.cameraId+'shared'] !== 'undefined' && sessions[sessionId][message.cameraId+'shared']) { + video = sessions[sessionId][message.cameraId+'shared']; + } + break; + case 'viewer': + if (message.cameraId && sessions[sessionId][message.cameraId]) { + video = sessions[sessionId][message.cameraId]; + } + case 'any': + if (message.cameraId && typeof sessions[sessionId][message.cameraId+'shared'] !== 'undefined' && sessions[sessionId][message.cameraId+'shared']) { + video = sessions[sessionId][message.cameraId+'shared']; + } + else if (message.cameraId && sessions[sessionId][message.cameraId]) { + video = sessions[sessionId][message.cameraId]; + } + + break; + } + + switch (message.id) { + case 'start': + console.log('[' + message.id + '] connection ' + sessionId + " message => " + JSON.stringify(message, null, 2)); + + video = new Video(bbbGW, message.cameraId, shared, message.connectionId); + + // Empty ice queue after starting video + if (iceQueue[message.cameraId]) { + let candidate; + while(candidate = iceQueue[message.cameraId].pop()) { + video.onIceCandidate(cand); + } + } + + switch (role) { + case 'share': + sessions[sessionId][message.cameraId+'shared']= video; + break; + case 'viewer': + sessions[sessionId][message.cameraId] = video; + break; + default: console.log(" [VideoManager] Unknown role? ", role); + } + + video.start(message.sdpOffer, (error, sdpAnswer) => { + if (error) { + return bbbGW.publish(JSON.stringify({ + connectionId: sessionId, + type: 'video', + role: role, + id : 'error', + response : 'rejected', + message : error + }), C.FROM_VIDEO); + } + + bbbGW.publish(JSON.stringify({ + connectionId: sessionId, + type: 'video', + role: role, + id : 'startResponse', + cameraId: message.cameraId, + sdpAnswer : sdpAnswer + }), C.FROM_VIDEO); + }); + break; + + case 'stop': + + console.log('[' + message.id + '] connection ' + sessionId + " with message => " + JSON.stringify(message, null, 2)); + + if (video) { + stopSession(sessionId, role, cameraId); + } else { + console.log(" [stop] Why is there no video on STOP?"); + } + break; + + case 'onIceCandidate': + + if (video) { + video.onIceCandidate(message.candidate); + } else { + console.log(" [iceCandidate] Queueing ice candidate for later in video " + message.cameraId); + + if (!iceQueue[message.cameraId]) { + iceQueue[message.cameraId] = []; + } + iceQueue[message.cameraId].push(message.candidate); + } + break; + + case 'close': + console.log(" [vide] Closing session for sessionId: " + sessionId); + + stopSession(sessionId); + + break; + + default: + bbbGW.publish(JSON.stringify({ + connectionId: sessionId, + type: 'video', + id : 'error', + response : 'rejected', + message : 'Invalid message ' + JSON.stringify(message) + }), C.FROM_VIDEO); + break; + } +}; + +let stopSession = async function(sessionId, role, cameraId) { + console.log(' [VideoManager/x] Stopping session ' + sessionId + " with role " + role + " for camera " + cameraId); + + let videoIds = Object.keys(sessions[sessionId]); + + try { + if (role === 'share') { + var sharedVideo = sessions[sessionId][cameraId+'shared']; + await sharedVideo.stop(); + delete sessions[sessionId][cameraId+'shared']; + console.log(' [VideoManager] Stopping sharer [', sessionId, '][', cameraId,'] with IDs' , videoIds); + } + else if (role === 'viewer') { + var video = sessions[sessionId][cameraId]; + await video.stop(); + delete sessions[sessionId][cameraId]; + console.log(' [VideoManager] Stopping viewer [', sessionId, '][', cameraId,'] with IDs ', sessions[sessionId][cameraId]); + } + + logAvailableSessions(); + } + catch (err) { + console.log(" [VideoManager] Stop error => ", err); + } +} + +let stopAll = function() { + console.log(' [Video/x] Stopping everything! '); + + if (sessions == null) { + return; + } + + let sessionIds = Object.keys(sessions); + + for (var i = 0; i < sessionIds.length; i++) { + + stopSession(sessionIds[i]); + } + + setTimeout(process.exit, 100); +} + +let logAvailableSessions = function() { + if(typeof sessions !== 'undefined' && sessions) { + console.log(" [VideoManager] Available sessions are =>"); + let sessionMainKeys = Object.keys(sessions); + for (var k in sessions) { + if(typeof sessions[k] !== 'undefined' && sessions[k]) { + console.log(' [VideoManager] Session[', k,'] => ', Object.keys(sessions[k])); + } + } + } +} + +process.on('SIGTERM', stopAll); +process.on('SIGINT', stopAll); diff --git a/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js new file mode 100644 index 0000000000000000000000000000000000000000..b6add57d4fbd68c5ce2de4cf12a4fd710016be06 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/video/VideoProcess.js @@ -0,0 +1,9 @@ +const VideoManager = require('./VideoManager'); + +process.on('uncaughtException', function (error) { + console.log(error.stack); +}); + +process.on('disconnect',function() { + console.log("Parent exited!"); +}); diff --git a/labs/bbb-webrtc-sfu/lib/video/video.js b/labs/bbb-webrtc-sfu/lib/video/video.js new file mode 100644 index 0000000000000000000000000000000000000000..b99077321b18855d86db5ecc5301aa09af7cc744 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/video/video.js @@ -0,0 +1,166 @@ +'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'); +const C = require('../bbb/messages/Constants'); + +if (config.get('acceptSelfSignedCertificate')) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED=0; +} + +module.exports = class Video { + constructor(_bbbGW, _id, _shared, _sessionId) { + this.mcs = new MCSApi(); + this.bbbGW = _bbbGW; + this.id = _id; + this.sessionId = _sessionId; + this.meetingId = _id; + this.shared = _shared; + this.role = this.shared? 'share' : 'view' + 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.bbbGW.publish(JSON.stringify({ + connectionId: this.sessionId, + type: 'video', + role: this.role, + id : 'iceCandidate', + cameraId: this.id, + candidate: candidate + }), C.FROM_VIDEO); + 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.bbbGW.publish(JSON.stringify({ + connectionId: this.sessionId, + type: 'video', + role: this.role, + id : 'playStop', + cameraId: this.id, + }), C.FROM_VIDEO); + } + else if (msEvent.state === 'FLOWING') { + this.bbbGW.publish(JSON.stringify({ + connectionId: this.sessionId, + type: 'video', + role: this.role, + id : 'playStart', + cameraId: this.id, + }), C.FROM_VIDEO); + } + + 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, sharedWebcams[this.id], 'WebRtcEndpoint', {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 for user ", this.userId, " returned => " + this.mediaId); + + return callback(null, sdpAnswer); + } + } + catch (err) { + console.log(" [video] MCS returned error => " + err); + return callback(err); + } + }; + + async stop () { + console.log(' [stop] Releasing endpoints for user ' + this.userId + ' at room ' + this.meetingId); + + try { + await this.mcs.leave(this.meetingId, this.userId); + if (this.shared) { + sharedWebcams[this.id] = null; + } + this._candidatesQueue = null; + Promise.resolve(); + } + catch (err) { + // TODO error handling + Promise.reject(); + } + return; + }; +}; diff --git a/labs/kurento-screenshare/lib/websocket.js b/labs/bbb-webrtc-sfu/lib/video/websocket.js similarity index 100% rename from labs/kurento-screenshare/lib/websocket.js rename to labs/bbb-webrtc-sfu/lib/video/websocket.js diff --git a/labs/bbb-webrtc-sfu/lib/websocket.js b/labs/bbb-webrtc-sfu/lib/websocket.js new file mode 100644 index 0000000000000000000000000000000000000000..c4fe9f6f18b220e8b4c43be58ec75004a6430124 --- /dev/null +++ b/labs/bbb-webrtc-sfu/lib/websocket.js @@ -0,0 +1,18 @@ +/* + * Simple wrapper around the ws library + * + */ + +var ws = require('ws'); + +ws.prototype.sendMessage = function(json) { + + return this.send(JSON.stringify(json), function(error) { + if(error) + console.log(' [server] Websocket error "' + error + '" on message "' + json.id + '"'); + }); + +}; + + +module.exports = ws; \ No newline at end of file diff --git a/labs/bbb-webrtc-sfu/package-lock.json b/labs/bbb-webrtc-sfu/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..8fbfe4227510eb9e880d5bf83dc0de9fa24fb80e --- /dev/null +++ b/labs/bbb-webrtc-sfu/package-lock.json @@ -0,0 +1,371 @@ +{ + "name": "bbb-webrtc-sfu", + "version": "0.0.2", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "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" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "backoff": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.3.0.tgz", + "integrity": "sha1-7nx+OAk/kuRyhZ22NedlJFT8Ieo=" + }, + "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=", + "requires": { + "json5": "0.4.0", + "os-homedir": "1.0.2" + } + }, + "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=" + }, + "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==" + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==" + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "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=" + }, + "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.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.0.2.tgz", + "integrity": "sha512-kVka3LaHQyENvcMW4WJPSepGM43oCofcKxfs9HbbKd/FrwBAAt4lNNTPKOzSMmV53GIspmNO4U3O2TzoGvxxCA==", + "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==", + "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=" + }, + "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" + }, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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=" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "moment": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.2.tgz", + "integrity": "sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA==" + }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, + "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=" + }, + "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=" + }, + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + }, + "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=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sdp-transform": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz", + "integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY=" + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "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" + } + }, + "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=" + } + } + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "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": "3.3.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz", + "integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.1", + "ultron": "1.1.1" + }, + "dependencies": { + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + } + } + } + } +} diff --git a/labs/bbb-webrtc-sfu/package.json b/labs/bbb-webrtc-sfu/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e7ed691d7824275af6e6c07153d9b7c8b5a00ba7 --- /dev/null +++ b/labs/bbb-webrtc-sfu/package.json @@ -0,0 +1,23 @@ +{ + "name": "bbb-webrtc-sfu", + "version": "0.0.2", + "private": true, + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "joi": "^13.0.2", + "kurento-client": "git+https://github.com/Kurento/kurento-client-js.git#master", + "moment": "^2.19.2", + "redis": "^2.8.0", + "sdp-transform": "^2.3.0", + "uuid": "^3.1.0", + "ws": "^3.3.2", + "config": "^1.26.1", + "js-yaml": "^3.8.3" + }, + "devDependencies": { + "readable-id": "^1.0.0" + }, + "optionalDependencies": {} +} diff --git a/labs/bbb-webrtc-sfu/server.js b/labs/bbb-webrtc-sfu/server.js new file mode 100755 index 0000000000000000000000000000000000000000..3252a2b96febc93847754de12567273fafc6893f --- /dev/null +++ b/labs/bbb-webrtc-sfu/server.js @@ -0,0 +1,67 @@ +/* + * Lucas Fialho Zawacki + * Paulo Renato Lanzarin + * (C) Copyright 2017 Bigbluebutton + * + */ + +const ConnectionManager = require('./lib/connection-manager/ConnectionManager'); +const HttpServer = require('./lib/connection-manager/HttpServer'); +const server = new HttpServer(); +const WebsocketConnectionManager = require('./lib/connection-manager/WebsocketConnectionManager'); +const cp = require('child_process'); + +let screenshareProc = cp.fork('./lib/screenshare/ScreenshareProcess', { + // Pass over all of the environment. + env: process.ENV, + // Share stdout/stderr, so we can hear the inevitable errors. + silent: false +}); + +let videoProc = cp.fork('./lib/video/VideoProcess.js', { + // Pass over all of the environment. + env: process.ENV, + // Share stdout/stderr, so we can hear the inevitable errors. + silent: false +}); + +let onMessage = function (message) { + console.log('event','child message',this.pid,message); +}; + +let onError = function(e) { + console.log('event','child error',this.pid,e); +}; + +let onDisconnect = function(e) { + console.log(e); + console.log('event','child disconnect',this.pid,'killing...'); + this.kill(); +}; + +screenshareProc.on('message',onMessage); +screenshareProc.on('error',onError); +screenshareProc.on('disconnect',onDisconnect); + +videoProc.on('message',onMessage); +videoProc.on('error',onError); +videoProc.on('disconnect',onDisconnect); + +const CM = new ConnectionManager(screenshareProc, videoProc); + +let websocketManager = new WebsocketConnectionManager(server.getServerObject(), '/bbb-webrtc-sfu'); + +process.on('SIGTERM', process.exit) +process.on('SIGINT', process.exit) +process.on('uncaughtException', function (error) { + console.log(error.stack); + process.exit('1'); +}); + + +CM.setHttpServer(server); +CM.addAdapter(websocketManager); + +CM.listen(() => { + console.log(" [SERVER] Server started"); +}); diff --git a/labs/kurento-screenshare/.gitignore b/labs/kurento-screenshare/.gitignore deleted file mode 100644 index 40b878db5b1c97fc77049537a71bb2e249abe5dc..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ \ No newline at end of file diff --git a/labs/kurento-screenshare/config/default.yml b/labs/kurento-screenshare/config/default.yml deleted file mode 100644 index 46b6e2629ecf6e34603f5a34c1288266d0583918..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/config/default.yml +++ /dev/null @@ -1,8 +0,0 @@ -kurentoUrl: "KURENTOURL" -kurentoIp: "KURENTOIP" -localIpAddress: "HOST" -acceptSelfSignedCertificate: false -redisHost : "127.0.0.1" -redisPort : "6379" -minVideoPort: 30000 -maxVideoPort: 33000 diff --git a/labs/kurento-screenshare/keys/README.md b/labs/kurento-screenshare/keys/README.md deleted file mode 100644 index 5bc681a1c8d2ece88651b6ee63d410536eae50f6..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This folder contains a dummy self-signed certificate only for demo purposses, -**DON'T USE IT IN PRODUCTION**. diff --git a/labs/kurento-screenshare/keys/server.crt b/labs/kurento-screenshare/keys/server.crt deleted file mode 100644 index 65e608dad5d9fb19f68ac486e6189dfc67dcd2ff..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/server.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDBjCCAe4CCQCuf5QfyX2oDDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB -VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTE0MDkyOTA5NDczNVoXDTE1MDkyOTA5NDczNVowRTELMAkG -A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5lFi3pBYWIY6kTN/iUaxJLROFo -FhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP1UitWzVO6pVvBaIt5IKlhhfm -YA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5VjKYc3OtEhcG8dgLAnOjbbk2Hr -8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo9gs56urvVDWG4rhdGybj1uwU -ZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0NjaU+MpSdEbB82z4b2NiN8Wq+ -rFA/JbvyeoWWHMoa7wkVs1MCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYLRwV9fo -AOhJfeK199Tv6oXoNSSSe10pVLnYxPcczCVQ4b9SomKFJFbmwtPVGi6w3m+8mV7F -9I2WKyeBHzmzfW2utZNupVybxgzEjuFLOVytSPdsB+DcJomOi8W/Cf2Vk8Wykb/t -Ctr1gfOcI8rwEGKxm279spBs0u1snzoLyoimbMbiXbC82j1IiN3Jus08U07m/j7N -hRBCpeHjUHT3CRpvYyTRnt+AyBd8BiyJB7nWmcNI1DksXPfehd62MAFS9e1ZE+dH -Aavg/U8VpS7pcCQcPJvIJ2hehrt8L6kUk3YUYqZ0OeRZK27f2R5+wFlDF33esm3N -dCSsLJlXyqAQFg== ------END CERTIFICATE----- diff --git a/labs/kurento-screenshare/keys/server.csr b/labs/kurento-screenshare/keys/server.csr deleted file mode 100644 index 6615b130471ce23cf8d980df5a308694ed06695b..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/server.csr +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN CERTIFICATE REQUEST----- -MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx -ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAMJOyOHJ+rJWJEQ7P7kKoWa31ff7hKNZxF6sYE5l -Fi3pBYWIY6kTN/iUaxJLROFoFhoC/M/STY76rIryix474v/6cRoG8N+GQBEn4IAP -1UitWzVO6pVvBaIt5IKlhhfmYA1IMweCd03vLcaHTddNmFDBTks7QDwfenTaR5Vj -KYc3OtEhcG8dgLAnOjbbk2Hr8wter2IeNgkhya3zyoXnTLT8m8IMg2mQaJs62Xlo -9gs56urvVDWG4rhdGybj1uwUZiDYyP4CFCUHS6UVt12vADP8vjbwmss2ScGsIf0N -jaU+MpSdEbB82z4b2NiN8Wq+rFA/JbvyeoWWHMoa7wkVs1MCAwEAAaAAMA0GCSqG -SIb3DQEBCwUAA4IBAQBMszYHMpklgTF/3h1zAzKXUD9NrtZp8eWhL06nwVjQX8Ai -EaCUiW0ypstokWcH9+30chd2OD++67NbxYUEucH8HrKpOoy6gs5L/mqgQ9Npz3OT -TB1HI4kGtpVuUQ5D7L0596tKzMX/CgW/hRcHWl+PDkwGhQs1qZcJ8QN+YP6AkRrO -5sDdDB/BLrB9PtBQbPrYIQcHQ7ooYWz/G+goqRxzZ6rt0aU2uAB6l7c82ADLAqFJ -qlw+xqVzEETVfqM5TXKK/wV3hgm4oSX5Q4SHLKF94ODOkWcnV4nfIKz7y+5XcQ3p -PrGimI1br07okC5rO9cgLCR0Ks20PPFcM0FvInW/ ------END CERTIFICATE REQUEST----- diff --git a/labs/kurento-screenshare/keys/server.key b/labs/kurento-screenshare/keys/server.key deleted file mode 100644 index a69a0a279daf6a68b9eff057204cd05af1b27a5a..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/keys/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAwk7I4cn6slYkRDs/uQqhZrfV9/uEo1nEXqxgTmUWLekFhYhj -qRM3+JRrEktE4WgWGgL8z9JNjvqsivKLHjvi//pxGgbw34ZAESfggA/VSK1bNU7q -lW8Foi3kgqWGF+ZgDUgzB4J3Te8txodN102YUMFOSztAPB96dNpHlWMphzc60SFw -bx2AsCc6NtuTYevzC16vYh42CSHJrfPKhedMtPybwgyDaZBomzrZeWj2Cznq6u9U -NYbiuF0bJuPW7BRmINjI/gIUJQdLpRW3Xa8AM/y+NvCayzZJwawh/Q2NpT4ylJ0R -sHzbPhvY2I3xar6sUD8lu/J6hZYcyhrvCRWzUwIDAQABAoIBACwt56TW3MZxqZtN -8WYsUZheUispJ/ZQMcLo5JjOiSV1Jwk+gpJtyTse291z+bxagzP02/CQu4u32UVa -cmE0cp+LHO4zB8964dREwdm8P91fdS6Au/uwG5LNZniCFCQZAFvkv52Ef4XbzQen -uf4rKWerHBck6K0C5z/sZXxE6KtScE2ZLUmkhO0nkHM6MA6gFk2OMnB+oDTOWWPt -1mlreQlzuMYG/D4axviRYrOSYCE5Qu1SOw/DEOLQqqeBjQrKtAyOlFHZsIR6lBfe -KHMChPUcYIwaowt2DcqH/A+AFXRtaifa6DvH8Yul+2vAp47UEpaenVfM5bpN33XV -EzerjtECgYEA+xiXzblek67iQgRpc9eHSoqs4iRLhae8s8kpAG51Jz46Je+Dmium -XV769oiUGUxBeoUb7ryW+4MOzHJaA1BfGejQSvwLIB9e4cnikqnAArcqbcAcOCL1 -aYYDiSmSmN/AokNZlPKEBFXP9bzXrU9smQJWNTHlcRl7JXfnwF+jwNsCgYEAxhpE -SBr9vlUVHNh/S6C5i80NIYg6jCy2FgsmuzEqmcqV0pTyzegmq8bru+QmuvoUj2o4 -nVv4J9d1fLF6ECUVk9aK8UdJOOB6hAfurOdJCArgrsY/9t4uDzXfbPCdfSNQITE0 -XgeNGQX1EzvwwkBmyZKk0kLIr3syP8ZCWfXDROkCgYBR+dF1pJMv++R6UR5sZ20P -9P5ERj0xwXVl7MKqFWXCDhrFz9BTQPTrftrIKgbPy4mFCnf4FTHlov/t11dzxYWG -2+9Ey8yGDDfZ1yNVZn39ZPdBJXsRCLi+XrZAzYXCyyoEz6ArdJGNKMbgH2r6dfeq -bIzgiQ2zQvJlZSQQNiksCQKBgCgwzAmU8EXdHRttEOZXBU3HnBJhgP9PUuHGAWWY -4/uvjhXbAiekIbRX9xt3fiQQ+HrgIfxK3F246K0TlKAR5f7IWAf7Xm+bmz+OHG4X -vklTa6IJtpBvIwkS9PE1H75zm54gTW+GOKoK+12bm4zNZA0hIy9FPVHcvKUTpAJ8 -SdGBAoGAHLtJnB1NO4EgO6WtLQMXt7HrIbup8eZi8/82gC3422C+ooKIrYQ07qSw -nBOO/G0OB4yd6vCE2x5+TWSSCYGgG5A8aIv5qP76RP4hovGHxG/y2tfotw5UuOrh -nFWlTP4Urs8PeykvK9ao8r/T8BnPIC16U6ENYvAc0mRlFA2j1GA= ------END RSA PRIVATE KEY----- diff --git a/labs/kurento-screenshare/lib/ConnectionManager.js b/labs/kurento-screenshare/lib/ConnectionManager.js deleted file mode 100644 index 2dbef1e75d32f7759337ef0f56a3b11ced7d4ed6..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/ConnectionManager.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Lucas Fialho Zawacki - * Paulo Renato Lanzarin - * (C) Copyright 2017 Bigbluebutton - * - */ - -'use strict' - -const cookieParser = require('cookie-parser') -const express = require('express'); -const session = require('express-session') -const wsModule = require('./websocket'); -const http = require('http'); -const fs = require('fs'); -const BigBlueButtonGW = require('./bbb/pubsub/bbb-gw'); -var Screenshare = require('./screenshare'); -var C = require('./bbb/messages/Constants'); - -// Global variables - -module.exports = class ConnectionManager { - - constructor (settings, logger) { - this._logger = logger; - this._clientId = 0; - this._app = express(); - this._screenshareSessions = {}; - - this._setupExpressSession(); - this._setupHttpServer(); - } - - _setupExpressSession() { - this._app.use(cookieParser()); - - this._sessionHandler = session({ - secret : 'Shawarma', rolling : true, resave : true, saveUninitialized : true - }); - - this._app.use(this._sessionHandler); - } - - _setupHttpServer() { - let self = this; - /* - * Server startup - */ - this._httpServer = http.createServer(this._app).listen(3008, function() { - console.log(' [*] Running node-apps connection manager.'); - }); - - /* - * Management of sessions - */ - this._wss = new wsModule.Server({ - server : this._httpServer, - path : '/kurento-screenshare' - }); - - - // TODO isolate this - this._bbbGW = new BigBlueButtonGW(); - - this._bbbGW.addSubscribeChannel(C.FROM_BBB_TRANSCODE_SYSTEM_CHAN, function(error, redisWrapper) { - if(error) { - console.log(' Could not connect to transcoder redis channel, finishing app...'); - self._stopAll(); - } - console.log(' [server] Successfully subscribed to redis channel'); - }); - - - this._wss.on('connection', self._onNewConnection.bind(self)); - } - - _onNewConnection(webSocket) { - let self = this; - let connectionId; - let request = webSocket.upgradeReq; - let sessionId; - let response = { - writeHead : {} - }; - - this._sessionHandler(request, response, function(err) { - connectionId = request.session.id + "_" + self._clientId++; - console.log('Connection received with connectionId ' + connectionId); - }); - - webSocket.on('error', function(error) { - console.log('Connection ' + connectionId + ' error'); - self._stopSession(sessionId); - }); - - webSocket.on('close', function() { - console.log('Connection ' + connectionId + ' closed'); - self._stopSession(sessionId); - }); - - webSocket.on('message', function(_message) { - let message = JSON.parse(_message); - let session; - // The sessionId is voiceBridge for screensharing sessions - sessionId = message.voiceBridge; - - if(self._screenshareSessions[sessionId]) { - session = self._screenshareSessions[sessionId]; - } - - switch (message.id) { - - case 'presenter': - - // Checking if there's already a Screenshare session started - // because we shouldn't overwrite it - - if(session) { - break; - } - - session = new Screenshare(webSocket, connectionId, self._bbbGW, - sessionId, message.callerName, message.vh, message.vw, - message.internalMeetingId); - - self._screenshareSessions[sessionId] = {} - self._screenshareSessions[sessionId] = session; - - // starts presenter by sending sessionID, websocket and sdpoffer - session._startPresenter(connectionId, webSocket, message.sdpOffer, function(error, sdpAnswer) { - console.log(" Started presenter " + connectionId); - if (error) { - return webSocket.send(JSON.stringify({ - id : 'presenterResponse', - response : 'rejected', - message : error - })); - } - - webSocket.send(JSON.stringify({ - id : 'presenterResponse', - response : 'accepted', - sdpAnswer : sdpAnswer - })); - console.log(" [websocket] Sending presenterResponse \n" + sdpAnswer); - }); - break; - - case 'viewer': - console.log('Viewer message => [' + message.id + '] connection [' + connectionId + '][' + message.presenterId + '][' + message.sessionId + '][' + message.callerName + ']'); - - break; - case 'stop': - - console.log('[' + message.id + '] connection ' + connectionId); - - if (session) { - session._stop(sessionId); - } else { - console.log(" [stop] Why is there no session on STOP?"); - } - break; - - case 'onIceCandidate': - if (session) { - session._onIceCandidate(message.candidate); - } else { - console.log(" [iceCandidate] Why is there no session on ICE CANDIDATE?"); - } - break; - - case 'ping': - webSocket.send(JSON.stringify({ - id : 'pong', - response : 'accepted' - })); - break; - - default: - webSocket.sendMessage({ id : 'error', message : 'Invalid message ' + message }); - break; - } - }); - } - - _stopSession(sessionId) { - console.log(' [>] Stopping session ' + sessionId); - let session = this._screenshareSessions[sessionId]; - if(typeof session !== 'undefined' && typeof session._stop === 'function') { - session._stop(); - } - - delete this._screenshareSessions[sessionId]; - } - - _stopAll() { - console.log('\n [x] Stopping everything! '); - let sessionIds = Object.keys(this._screenshareSessions); - - for (let i = 0; i < sessionIds.length; i++) { - this._stopSession(sessionIds[i]); - } - - setTimeout(process.exit, 1000); - } -} diff --git a/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js b/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js deleted file mode 100644 index da92167e4169e8a03ee3d3cb3e03b78405c1049f..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/bbb/pubsub/RedisWrapper.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @classdesc - * Redis wrapper class for connecting to Redis channels - */ - -/* Modules */ - -var redis = require('redis'); -var config = require('config'); -var Constants = require('../messages/Constants.js'); -var util = require('util'); -const EventEmitter = require('events').EventEmitter; -const _retryThreshold = 1000 * 60 * 60; -const _maxRetries = 10; - - -/* Public members */ - -var RedisWrapper = function(subpattern) { - // Redis PubSub client holders - this.redisCli = null; - this.redisPub = null; - // Pub and Sub channels/patterns - this.subpattern = subpattern; - EventEmitter.call(this); -} - -util.inherits(RedisWrapper, EventEmitter); - -RedisWrapper.prototype.startRedis = function(callback) { - var self = this; - if (this.redisCli) { - console.log(" [RedisWrapper] Redis Client already exists"); - callback(false, this); - } - - var options = { - host : config.get('redisHost'), - port : config.get('redisPort'), - //password: config.get('redis.password') - retry_strategy: redisRetry - }; - - this.redisCli = redis.createClient(options); - this.redisPub = redis.createClient(options); - - console.log(" [RedisWrapper] Trying to subscribe to redis channel"); - - this.redisCli.on("psubscribe", function (channel, count) { - console.log(" [RedisWrapper] Successfully subscribed to pattern [" + channel + "]"); - }); - - this.redisCli.on("pmessage", self.onMessage.bind(self)); - this.redisCli.psubscribe(this.subpattern); - - console.log(" [RedisWrapper] Started Redis client at " + options.host + ":" + options.port + - " for subscription pattern: " + this.subpattern); - - callback(false, this); -}; - -RedisWrapper.prototype.stopRedis = function(callback) { - if (this.redisCli){ - this.redisCli.quit(); - } - callback(false); -}; - -RedisWrapper.prototype.publishToChannel = function(message, channel) { - if(this.redisPub) { - console.log(" [RedisWrapper] Sending message to channel [" + channel + "]: " + message); - this.redisPub.publish(channel, message); - } -}; - -RedisWrapper.prototype.onMessage = function(pattern, channel, message) { - console.log(" [RedisWrapper] Message received from channel [" + channel + "] : " + message); - // use event emitter to throw new message - this.emit(Constants.REDIS_MESSAGE, message); -} - -/* Private members */ - -function redisRetry(options) { - if (options.error && options.error.code === 'ECONNREFUSED') { - return new Error('The server refused the connection'); - } - if (options.total_retry_time > _retryThreshold) { - return new Error('Retry time exhausted'); - } - if (options.times_connected > _maxRetries) { - return undefined; - } - return Math.max(options.attempt * 100, 3000); -}; - -module.exports = RedisWrapper; diff --git a/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js b/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js deleted file mode 100644 index 1abbb25aa010d9dc2a18d09edb2064c8516d9463..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/bbb/pubsub/bbb-gw.js +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @classdesc - * BigBlueButton redis gateway for bbb-screenshare node app - */ - -/* Modules */ - -var C = require('../messages/Constants.js'); -var RedisWrapper = require('./RedisWrapper.js'); -var config = require('config'); -var util = require('util'); -var EventEmitter = require('events').EventEmitter; - -/* Public members */ - -var BigBlueButtonGW = function () { - this.redisClients = null - EventEmitter.call(this); -}; - -util.inherits(BigBlueButtonGW, EventEmitter); - -BigBlueButtonGW.prototype.addSubscribeChannel = function (channel, callback) { - var self = this; - - if (this.redisClients === null) { - this.redisClients = {}; - } - - if (this.redisClients[channel]) { - return callback(null, this.redisClients[channel]); - } - - var wrobj = new RedisWrapper(channel); - this.redisClients[channel] = {}; - this.redisClients[channel] = wrobj; - wrobj.startRedis(function(error, redisCli) { - if(error) { - console.log(" [BigBlueButtonGW] Could not start redis client for channel " + channel); - return callback(error); - } - - console.log(" [BigBlueButtonGW] Added redis client to this.redisClients[" + channel + "]"); - wrobj.on(C.REDIS_MESSAGE, self.incomingMessage.bind(self)); - - return callback(null, wrobj); - }); -}; - -/** - * Capture messages from subscribed channels and emit an event with it's - * identifier and payload. Check Constants.js for the identifiers. - * - * @param {Object} message Redis message - */ -BigBlueButtonGW.prototype.incomingMessage = function (message) { - var msg = JSON.parse(message); - - // Trying to parse both message types, 1x and 2x - if (msg.header) { - var header = msg.header; - var payload = msg.payload; - } - else if (msg.core) { - var header = msg.core.header; - var payload = msg.core.body; - } - - if (header){ - switch (header.name) { - // interoperability with 1.1 - case C.START_TRANSCODER_REPLY: - this.emit(C.START_TRANSCODER_REPLY, payload); - break; - case C.STOP_TRANSCODER_REPLY: - this.emit(C.STOP_TRANSCODER_REPLY, payload); - break; - // 2x messages - case C.START_TRANSCODER_RESP_2x: - payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x]; - - this.emit(C.START_TRANSCODER_RESP_2x, payload); - break; - case C.STOP_TRANSCODER_RESP_2x: - payload[C.MEETING_ID_2x] = header[C.MEETING_ID_2x]; - this.emit(C.STOP_TRANSCODER_RESP_2x, payload); - break; - - default: - console.log(" [BigBlueButtonGW] Unknown Redis message with ID =>" + header.name); - } - } -}; - -BigBlueButtonGW.prototype.publish = function (message, channel, callback) { - for(var client in this.redisClients) { - if(typeof this.redisClients[client].publishToChannel === 'function') { - this.redisClients[client].publishToChannel(message, channel); - return callback(null); - } - } - return callback("Client not found"); -}; - -module.exports = BigBlueButtonGW; diff --git a/labs/kurento-screenshare/lib/media-controller.js b/labs/kurento-screenshare/lib/media-controller.js deleted file mode 100644 index df9697ec6984deb5f425fbb7c210fba4b6ce62e6..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/media-controller.js +++ /dev/null @@ -1,141 +0,0 @@ -'use strict' - -const Constants = require('./bbb/messages/Constants.js'); -const config = require('config'); -const kurento = require('kurento-client'); -const mediaServerClient = null; - -var _mediaPipelines = {}; -var _mediaElements= {}; - -function createMediaPipeline(id, callback) { - console.log(' [media] Creating media pipeline for ' + id); - getMediaServerClient(function (error, mediaServerClient) { - mediaServerClient.create('MediaPipeline', function(err, pipeline) { - if (error) { - console.log("Could not find media server at address " + kurentoUrl); - return callback(error); - } - return callback(null , pipeline); - }); - }); -}; - -function getMediaServerClient (callback) { - let kurentoUrl = config.get('kurentoUrl'); - if (mediaServerClient) { - callback(null, mediaServerClient); - } - else { - kurento(kurentoUrl, function(error, _mediaServerClient) { - if (error) { - console.log("Could not find media server at address " + kurentoUrl); - return callback(error, null); - } - - console.log(" [server] Initiating kurento client. Connecting to: " + kurentoUrl); - return callback(null, _mediaServerClient); - }); - } -}; - -/* Public members */ -module.exports = { - - createMediaElement : function (conference, type, callback) { - let self = this; - self.getMediaPipeline(conference, function(error, pipeline) { - - pipeline.create(type, function(error, mediaElement) { - if (error) { - return callback(error, null); - } - console.log(" [MediaController] Created [" + type + "] media element: " + mediaElement.id); - _mediaElements[mediaElement.id] = mediaElement; - return callback(null, mediaElement); - }); - }); - }, - - connectMediaElements : function (sourceId, sinkId, type, callback) { - let source = _mediaElements[sourceId]; - let sink = _mediaElements[sinkId]; - - if (source && sink) { - if (type === 'ALL') { - source.connect(sink, function (error) { - return callback (error); - }); - } else { - console.log(typeof source.connect); - source.connect(sink, type, function (error) { - return callback (error); - }); - } - } else { - return callback ("Failed to connect " + type + ": " + sourceId + " to " + sinkId); - } - }, - - releaseMediaElement : function (elementId) { - let mediaElement = _mediaElements[elementId]; - - if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') { - mediaElement.release(); - } - }, - - releasePipeline: function (pipelineId) { - let MediaPipeline = _mediaPipelines[pipelineId]; - - if (typeof mediaElement !== 'undefined' && typeof mediaElement.release === 'function') { - mediaElement.release(); - } - }, - - processOffer : function (elementId, sdpOffer, callback) { - let mediaElement = _mediaElements[elementId]; - - if (typeof mediaElement !== 'undefined' && typeof mediaElement.processOffer === 'function') { - mediaElement.processOffer (sdpOffer, function (error, sdpAnswer) { - return callback (error, sdpAnswer); - }); - } else { - return callback (" [MediaController/processOffer] There is no element " + elementId, null); - } - }, - - getMediaPipeline : function(conference, callback) { - let self = this; - - if (_mediaPipelines[conference]) { - console.log(' [media] Pipeline already exists. ' + JSON.stringify(_mediaPipelines, null, 2)); - return callback(null, _mediaPipelines[conference]); - } else { - createMediaPipeline(conference, function(error, pipeline) { - _mediaPipelines[conference] = pipeline; - return callback(error, pipeline); - }); - } - }, - - addIceCandidate : function (elementId, candidate) { - let mediaElement = _mediaElements[elementId]; - - if (typeof mediaElement !== 'undefined' && typeof mediaElement.addIceCandidate === 'function') { - mediaElement.addIceCandidate(candidate); - } - }, - - gatherCandidates : function (elementId, callback) { - let mediaElement = _mediaElements[elementId]; - - if (typeof mediaElement !== 'undefined' && typeof mediaElement.gatherCandidates === 'function') { - mediaElement.gatherCandidates(function (error) { - return callback(error); - }); - } else { - return callback (" [MediaController/gatherCandidates] There is no element " + elementId, null); - } - }, -}; diff --git a/labs/kurento-screenshare/lib/screenshare.js b/labs/kurento-screenshare/lib/screenshare.js deleted file mode 100644 index 411fbd54a9ac3cf994f7e0f703ed7c3f32f8718e..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/lib/screenshare.js +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Lucas Fialho Zawacki - * Paulo Renato Lanzarin - * (C) Copyright 2017 Bigbluebutton - * - */ - -'use strict' - -// Imports -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 now = moment(); -const MediaController = require('./media-controller'); - -// Global stuff -var sharedScreens = {}; -var rtpEndpoints = {}; - -const kurento = require('kurento-client'); -const config = require('config'); -const kurentoUrl = config.get('kurentoUrl'); -const kurentoIp = config.get('kurentoIp'); -const localIpAddress = config.get('localIpAddress'); - -if (config.get('acceptSelfSignedCertificate')) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED=0; -} - -module.exports = class Screenshare { - constructor(ws, id, bbbgw, voiceBridge, caller, vh, vw, meetingId) { - this._ws = ws; - this._id = id; - this._BigBlueButtonGW = bbbgw; - this._presenterEndpoint = null; - this._ffmpegRtpEndpoint = null; - this._voiceBridge = voiceBridge; - this._meetingId = meetingId; - this._caller = caller; - this._streamUrl = ""; - this._vw = vw; - this._vh = vh; - this._candidatesQueue = []; - } - - // TODO isolate ICE - _onIceCandidate(_candidate) { - let candidate = kurento.getComplexType('IceCandidate')(_candidate); - - if (this._presenterEndpoint) { - this._presenterEndpoint.addIceCandidate(candidate); - } - else { - this._candidatesQueue.push(candidate); - } - }; - - _startPresenter(id, ws, sdpOffer, callback) { - let self = this; - let _callback = callback; - - // Force H264 on Firefox and Chrome - sdpOffer = h264_sdp.transform(sdpOffer); - console.log("Starting presenter for " + sdpOffer); - MediaController.createMediaElement(self._voiceBridge, C.WebRTC, function(error, webRtcEndpoint) { - if (error) { - console.log("Media elements error" + error); - return _callback(error); - } - MediaController.createMediaElement(self._voiceBridge, C.RTP, function(error, rtpEndpoint) { - if (error) { - console.log("Media elements error" + error); - return _callback(error); - } - - - while(self._candidatesQueue.length) { - let candidate = self._candidatesQueue.shift(); - MediaController.addIceCandidate(webRtcEndpoint.id, candidate); - } - - MediaController.connectMediaElements(webRtcEndpoint.id, rtpEndpoint.id, C.VIDEO, function(error) { - if (error) { - console.log("Media elements CONNECT error " + error); - //pipeline.release(); - return _callback(error); - } - - // It's a user sharing a Screen - sharedScreens[id] = webRtcEndpoint; - rtpEndpoints[id] = rtpEndpoint; - - // Store our endpoint - self._presenterEndpoint = webRtcEndpoint; - self._ffmpegRtpEndpoint = rtpEndpoint; - - self._presenterEndpoint.on('OnIceCandidate', function(event) { - let candidate = kurento.getComplexType('IceCandidate')(event.candidate); - ws.sendMessage({ id : 'iceCandidate', cameraId: id, candidate : candidate }); - }); - - MediaController.processOffer(webRtcEndpoint.id, sdpOffer, function(error, webRtcSdpAnswer) { - if (error) { - console.log(" [webrtc] processOffer error => " + error + " for SDP " + sdpOffer); - //pipeline.release(); - return _callback(error); - } - - let sendVideoPort = MediaHandler.getVideoPort(); - - let rtpSdpOffer = MediaHandler.generateVideoSdp(localIpAddress, sendVideoPort); - console.log(" [rtpendpoint] RtpEndpoint processing => " + rtpSdpOffer); - - MediaController.gatherCandidates(webRtcEndpoint.id, function(error) { - if (error) { - return _callback(error); - } - - MediaController.processOffer(rtpEndpoint.id, rtpSdpOffer, function(error, rtpSdpAnswer) { - if (error) { - console.log(" [rtpendpoint] processOffer error => " + error + " for SDP " + rtpSdpOffer); - //pipeline.release(); - return _callback(error); - } - - console.log(" [rtpendpoint] KMS answer SDP => " + rtpSdpAnswer); - let recvVideoPort = rtpSdpAnswer.match(/m=video\s(\d*)/)[1]; - let rtpParams = MediaHandler.generateTranscoderParams(kurentoIp, localIpAddress, - sendVideoPort, recvVideoPort, self._meetingId, "stream_type_video", C.RTP_TO_RTMP, "copy", "caller"); - - self._ffmpegRtpEndpoint.on('MediaFlowInStateChange', function(event) { - if (event.state === 'NOT_FLOWING') { - self._onRtpMediaNotFlowing(); - } - else if (event.state === 'FLOWING') { - self._onRtpMediaFlowing(self._meetingId, rtpParams); - } - }); - return _callback(null, webRtcSdpAnswer); - }); - }); - }); - }); - }); - }); - }; - - _stop() { - - console.log(' [stop] Releasing endpoints for ' + this._id); - - this._stopScreensharing(); - - if (this._presenterEndpoint) { - MediaController.releaseMediaElement(this._presenterEndpoint.id); - this._presenterEndpoint = null; - } else { - console.log(" [webRtcEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE"); - } - - if (this._ffmpegRtpEndpoint) { - MediaController.releaseMediaElement(this._ffmpegRtpEndpoint.id); - this._ffmpegRtpEndpoint = null; - } else { - console.log(" [rtpEndpoint] PLEASE DONT TRY STOPPING THINGS TWICE"); - } - - console.log(' [stop] Screen is shared, releasing ' + this._id); - - delete sharedScreens[this._id]; - - delete this._candidatesQueue; - }; - - _stopScreensharing() { - let self = this; - let strm = Messaging.generateStopTranscoderRequestMessage(this._meetingId, this._meetingId); - - self._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {}); - - // Interoperability: capturing 1.1 stop_transcoder_reply messages - self._BigBlueButtonGW.once(C.STOP_TRANSCODER_REPLY, function(payload) { - let meetingId = payload[C.MEETING_ID]; - self._stopRtmpBroadcast(meetingId); - }); - - // Capturing stop transcoder responses from the 2x model - self._BigBlueButtonGW.once(C.STOP_TRANSCODER_RESP_2x, function(payload) { - let meetingId = payload[C.MEETING_ID_2x]; - self._stopRtmpBroadcast(meetingId); - }); - - } - - _onRtpMediaFlowing(meetingId, rtpParams) { - let self = this; - let strm = Messaging.generateStartTranscoderRequestMessage(meetingId, meetingId, rtpParams); - - // Interoperability: capturing 1.1 start_transcoder_reply messages - self._BigBlueButtonGW.once(C.START_TRANSCODER_REPLY, function(payload) { - let meetingId = payload[C.MEETING_ID]; - let output = payload["params"].output; - self._startRtmpBroadcast(meetingId, output); - }); - - // Capturing stop transcoder responses from the 2x model - self._BigBlueButtonGW.once(C.START_TRANSCODER_RESP_2x, function(payload) { - let meetingId = payload[C.MEETING_ID_2x]; - let output = payload["params"].output; - self._startRtmpBroadcast(meetingId, output); - }); - - - self._BigBlueButtonGW.publish(strm, C.TO_BBB_TRANSCODE_SYSTEM_CHAN, function(error) {}); - }; - - _stopRtmpBroadcast (meetingId) { - var self = this; - if(self._meetingId === meetingId) { - // TODO correctly assemble this timestamp - let timestamp = now.format('hhmmss'); - let dsrstom = Messaging.generateScreenshareRTMPBroadcastStoppedEvent2x(self._voiceBridge, - self._voiceBridge, self._streamUrl, self._vw, self._vh, timestamp); - self._BigBlueButtonGW.publish(dsrstom, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {}); - } - } - - _startRtmpBroadcast (meetingId, output) { - var self = this; - if(self._meetingId === meetingId) { - // TODO correctly assemble this timestamp - let timestamp = now.format('hhmmss'); - self._streamUrl = MediaHandler.generateStreamUrl(localIpAddress, meetingId, output); - let dsrbstam = Messaging.generateScreenshareRTMPBroadcastStartedEvent2x(self._voiceBridge, - self._voiceBridge, self._streamUrl, self._vw, self._vh, timestamp); - - self._BigBlueButtonGW.publish(dsrbstam, C.FROM_VOICE_CONF_SYSTEM_CHAN, function(error) {}); - } - } - - _onRtpMediaNotFlowing() { - console.log(" [screenshare] TODO RTP NOT_FLOWING"); - }; - - -}; diff --git a/labs/kurento-screenshare/package.json b/labs/kurento-screenshare/package.json deleted file mode 100644 index 187a3ab8a3f2e70c689e740178834ba1fadb5146..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "bbb-screenshare-video-kurento-bridge", - "version": "1.0.0", - "private": true, - "scripts": { - "start": "nodejs server.js", - "postinstall": "npm start" - }, - "dependencies": { - "cookie-parser": "^1.3.5", - "express": "~4.12.4", - "express-session": "~1.10.3", - "ws": "~1.0.1", - "kurento-client": "6.6.0", - "redis": "^2.6.2", - "sdp-transform": "*", - "moment": "*" - }, - "devDependencies": { - "config": "^1.26.1", - "js-yaml": "^3.8.3" - } -} diff --git a/labs/kurento-screenshare/server.js b/labs/kurento-screenshare/server.js deleted file mode 100755 index f645ac88a52ffa328944f6f775b868bda3b0ae34..0000000000000000000000000000000000000000 --- a/labs/kurento-screenshare/server.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Paulo Renato Lanzarin - * (C) Copyright 2017 Bigbluebutton - * - */ - -const ConnectionManager = require('./lib/ConnectionManager'); -const CM = new ConnectionManager(); - -process.on('SIGTERM', CM._stopAll.bind(CM)); -process.on('SIGINT', CM._stopAll.bind(CM));