From 26e2fd6ef02126f12d680a1f5fc9450bd72fbb2c Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 13 Nov 2015 11:04:49 -0600 Subject: [PATCH 01/44] Fixes desktop streaming layout. --- modules/UI/UI.js | 6 ++- modules/UI/toolbars/BottomToolbar.js | 9 +++- modules/UI/videolayout/LargeVideo.js | 70 ++++++++++++++++++--------- modules/UI/videolayout/VideoLayout.js | 57 ++++++++++++++++------ service/UI/UIEvents.js | 7 ++- 5 files changed, 108 insertions(+), 41 deletions(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 95e4870eb..3bf7c3dda 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -107,7 +107,7 @@ function setupChat() { function setupToolbars() { Toolbar.init(UI); Toolbar.setupButtonsFromConfig(); - BottomToolbar.init(); + BottomToolbar.init(eventEmitter); } function streamHandler(stream, isMuted) { @@ -343,6 +343,10 @@ function registerListeners() { AudioLevels.init(); }); + UI.addListener(UIEvents.FILM_STRIP_TOGGLED, function (isToggled) { + VideoLayout.onFilmStripToggled(isToggled); + }); + if (!interfaceConfig.filmStripOnly) { APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED, updateChatConversation); APP.xmpp.addListener(XMPPEvents.CHAT_ERROR_RECEIVED, chatAddError); diff --git a/modules/UI/toolbars/BottomToolbar.js b/modules/UI/toolbars/BottomToolbar.js index 0cf0f9a7a..747688f83 100644 --- a/modules/UI/toolbars/BottomToolbar.js +++ b/modules/UI/toolbars/BottomToolbar.js @@ -2,6 +2,9 @@ var PanelToggler = require("../side_pannels/SidePanelToggler"); var UIUtil = require("../util/UIUtil"); var AnalyticsAdapter = require("../../statistics/AnalyticsAdapter"); +var UIEvents = require("../../../service/UI/UIEvents"); + +var eventEmitter = null; var buttonHandlers = { "bottom_toolbar_contact_list": function () { @@ -27,7 +30,8 @@ var defaultBottomToolbarButtons = { var BottomToolbar = (function (my) { - my.init = function () { + my.init = function (emitter) { + eventEmitter = emitter; UIUtil.hideDisabledButtons(defaultBottomToolbarButtons); for(var k in buttonHandlers) @@ -45,6 +49,9 @@ var BottomToolbar = (function (my) { my.toggleFilmStrip = function() { var filmstrip = $("#remoteVideos"); filmstrip.toggleClass("hidden"); + + eventEmitter.emit( UIEvents.FILM_STRIP_TOGGLED, + filmstrip.hasClass("hidden")); }; $(document).bind("remotevideo.resized", function (event, width, height) { diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 20485c81a..b32569e48 100644 --- a/modules/UI/videolayout/LargeVideo.js +++ b/modules/UI/videolayout/LargeVideo.js @@ -115,7 +115,10 @@ function getDesktopVideoSize(videoWidth, var availableWidth = Math.max(videoWidth, videoSpaceWidth); var availableHeight = Math.max(videoHeight, videoSpaceHeight); - videoSpaceHeight -= $('#remoteVideos').outerHeight(); + var filmstrip = $("#remoteVideos"); + + if (!filmstrip.hasClass("hidden")) + videoSpaceHeight -= filmstrip.outerHeight(); if (availableWidth / aspectRatio >= videoSpaceHeight) { @@ -268,13 +271,7 @@ function changeVideo(isVisible) { largeVideoElement.style.transform = flipX ? "scaleX(-1)" : "none"; - var isDesktop = currentSmallVideo.getVideoType() === 'screen'; - // Change the way we'll be measuring and positioning large video - - getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize; - getVideoPosition = isDesktop ? getDesktopVideoPosition : - getCameraVideoPosition; - + LargeVideo.updateVideoSizeAndPosition(currentSmallVideo.getVideoType()); // Only if the large video is currently visible. if (isVisible) { @@ -451,10 +448,8 @@ var LargeVideo = { return; if (LargeVideo.isCurrentlyOnLarge(resourceJid)) { - var isDesktop = newVideoType === 'screen'; - getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize; - getVideoPosition = isDesktop ? getDesktopVideoPosition - : getCameraVideoPosition; + LargeVideo.updateVideoSizeAndPosition(newVideoType); + this.position(null, null, null, null, true); } }, @@ -496,17 +491,23 @@ var LargeVideo = { }, /** * Resizes the large html elements. - * @param animate boolean property that indicates whether the resize should be animated or not. - * @param isChatVisible boolean property that indicates whether the chat area is displayed or not. - * If that parameter is null the method will check the chat pannel visibility. - * @param completeFunction a function to be called when the video space is resized - * @returns {*[]} array with the current width and height values of the largeVideo html element. + * + * @param animate boolean property that indicates whether the resize should + * be animated or not. + * @param isSideBarVisible boolean property that indicates whether the chat + * area is displayed or not. + * If that parameter is null the method will check the chat panel + * visibility. + * @param completeFunction a function to be called when the video space is + * resized + * @returns {*[]} array with the current width and height values of the + * largeVideo html element. */ - resize: function (animate, isVisible, completeFunction) { + resize: function (animate, isSideBarVisible, completeFunction) { if(!isEnabled) return; var availableHeight = window.innerHeight; - var availableWidth = UIUtil.getAvailableVideoWidth(isVisible); + var availableWidth = UIUtil.getAvailableVideoWidth(isSideBarVisible); if (availableWidth < 0 || availableHeight < 0) return; @@ -514,7 +515,8 @@ var LargeVideo = { var top = availableHeight / 2 - avatarSize / 4 * 3; $('#activeSpeaker').css('top', top); - this.VideoLayout.resizeVideoSpace(animate, isVisible, completeFunction); + this.VideoLayout + .resizeVideoSpace(animate, isSideBarVisible, completeFunction); if(animate) { $('#largeVideoContainer').animate({ width: availableWidth, @@ -530,12 +532,36 @@ var LargeVideo = { } return [availableWidth, availableHeight]; }, - resizeVideoAreaAnimated: function (isVisible, completeFunction) { + /** + * Resizes the large video. + * + * @param isSideBarVisible indicating if the side bar is visible + * @param completeFunction the callback function to be executed after the + * resize + */ + resizeVideoAreaAnimated: function (isSideBarVisible, completeFunction) { if(!isEnabled) return; - var size = this.resize(true, isVisible, completeFunction); + var size = this.resize(true, isSideBarVisible, completeFunction); this.position(null, null, size[0], size[1], true); }, + /** + * Updates the video size and position. + * + * @param videoType the video type indicating if the stream is of type + * desktop or web cam + */ + updateVideoSizeAndPosition: function (videoType) { + if (!videoType) + videoType = currentSmallVideo.getVideoType(); + + var isDesktop = videoType === 'screen'; + + // Change the way we'll be measuring and positioning large video + getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize; + getVideoPosition = isDesktop ? getDesktopVideoPosition : + getCameraVideoPosition; + }, getResourceJid: function () { return currentSmallVideo ? currentSmallVideo.getResourceJid() : null; }, diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 10aefd39c..6513280b9 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -214,7 +214,8 @@ var VideoLayout = (function (my) { my.handleVideoThumbClicked = function(noPinnedEndpointChangedEvent, resourceJid) { if(focusedVideoResourceJid) { - var oldSmallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid); + var oldSmallVideo + = VideoLayout.getSmallVideo(focusedVideoResourceJid); if (oldSmallVideo && !interfaceConfig.filmStripOnly) oldSmallVideo.focus(false); } @@ -400,7 +401,8 @@ var VideoLayout = (function (my) { if(animate) { $('#remoteVideos').animate({ - height: height + 2 // adds 2 px because of small video 1px border + // adds 2 px because of small video 1px border + height: height + 2 }, { queue: false, @@ -425,7 +427,8 @@ var VideoLayout = (function (my) { } else { // size videos so that while keeping AR and max height, we have a // nice fit - $('#remoteVideos').height(height + 2);// adds 2 px because of small video 1px border + // adds 2 px because of small video 1px border + $('#remoteVideos').height(height + 2); $('#remoteVideos>span').width(width); $('#remoteVideos>span').height(height); @@ -439,10 +442,10 @@ var VideoLayout = (function (my) { * @param videoSpaceWidth the width of the video space */ my.calculateThumbnailSize = function (videoSpaceWidth) { - // Calculate the available height, which is the inner window height minus - // 39px for the header minus 2px for the delimiter lines on the top and - // bottom of the large video, minus the 36px space inside the remoteVideos - // container used for highlighting shadow. + // Calculate the available height, which is the inner window height + // minus 39px for the header minus 2px for the delimiter lines on the + // top and bottom of the large video, minus the 36px space inside the + // remoteVideos container used for highlighting shadow. var availableHeight = 100; var numvids = $('#remoteVideos>span:visible').length; @@ -458,7 +461,11 @@ var VideoLayout = (function (my) { var availableWidth = availableWinWidth / numvids; var aspectRatio = 16.0 / 9.0; var maxHeight = Math.min(160, availableHeight); - availableHeight = Math.min(maxHeight, availableWidth / aspectRatio, window.innerHeight - 18); + availableHeight + = Math.min( maxHeight, + availableWidth / aspectRatio, + window.innerHeight - 18); + if (availableHeight < availableWidth / aspectRatio) { availableWidth = Math.floor(availableHeight * aspectRatio); } @@ -882,6 +889,17 @@ var VideoLayout = (function (my) { }; + /** + * Updates the video size and position when the film strip is toggled. + * + * @param isToggled indicates if the film strip is toggled or not. True + * would mean that the film strip is hidden, false would mean it's shown + */ + my.onFilmStripToggled = function(isToggled) { + LargeVideo.updateVideoSizeAndPosition(); + LargeVideo.position(null, null, null, null, true); + }; + my.showMore = function (jid) { if (jid === 'local') { localVideoThumbnail.connectionIndicator.showMore(); @@ -914,21 +932,27 @@ var VideoLayout = (function (my) { }; /** - * Resizes the video area + * Resizes the video area. + * + * @param isSideBarVisible indicates if the side bar is currently visible * @param callback a function to be called when the video space is * resized. */ - my.resizeVideoArea = function(isVisible, callback) { - LargeVideo.resizeVideoAreaAnimated(isVisible, callback); + my.resizeVideoArea = function(isSideBarVisible, callback) { + LargeVideo.resizeVideoAreaAnimated(isSideBarVisible, callback); VideoLayout.resizeThumbnails(true); }; /** * Resizes the #videospace html element - * @param animate boolean property that indicates whether the resize should be animated or not. - * @param isChatVisible boolean property that indicates whether the chat area is displayed or not. - * If that parameter is null the method will check the chat pannel visibility. - * @param completeFunction a function to be called when the video space is resized + * @param animate boolean property that indicates whether the resize should + * be animated or not. + * @param isChatVisible boolean property that indicates whether the chat + * area is displayed or not. + * If that parameter is null the method will check the chat panel + * visibility. + * @param completeFunction a function to be called when the video space + * is resized. */ my.resizeVideoSpace = function (animate, isChatVisible, completeFunction) { var availableHeight = window.innerHeight; @@ -998,7 +1022,8 @@ var VideoLayout = (function (my) { LargeVideo.enableVideoProblemFilter(true); var reconnectingKey = "connection.RECONNECTING"; $('#videoConnectionMessage').attr("data-i18n", reconnectingKey); - $('#videoConnectionMessage').text(APP.translation.translateString(reconnectingKey)); + $('#videoConnectionMessage') + .text(APP.translation.translateString(reconnectingKey)); $('#videoConnectionMessage').css({display: "block"}); }; diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 4c3c36e49..e0b31eae5 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -2,6 +2,11 @@ var UIEvents = { NICKNAME_CHANGED: "UI.nickname_changed", SELECTED_ENDPOINT: "UI.selected_endpoint", PINNED_ENDPOINT: "UI.pinned_endpoint", - LARGEVIDEO_INIT: "UI.largevideo_init" + LARGEVIDEO_INIT: "UI.largevideo_init", + /** + * Notifies interested parties when the film strip (remote video's panel) + * is hidden (toggled) or shown (un-toggled). + */ + FILM_STRIP_TOGGLED: "UI.filmstrip_toggled" }; module.exports = UIEvents; \ No newline at end of file From 74c420a609a7a7a88db53150247d69cd7f8bd94a Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 13 Nov 2015 16:18:22 -0600 Subject: [PATCH 02/44] Adds config option for auto enable desktop sharing when opening an url. --- modules/UI/UI.js | 2 ++ modules/UI/toolbars/Toolbar.js | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 3bf7c3dda..637256394 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -528,6 +528,8 @@ function onMucJoined(jid, info) { VideoLayout.mucJoined(); + + Toolbar.checkAutoEnableDesktopSharing(); } function initEtherpad(name) { diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 9f229ddfd..03949b09f 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -632,13 +632,23 @@ var Toolbar = (function (my) { } }; - // checks whether recording is enabled and whether we have params to start automatically recording + // checks whether recording is enabled and whether we have params + // to start automatically recording my.checkAutoRecord = function () { if (UIUtil.isButtonEnabled('recording') && config.autoRecord) { toggleRecording(config.autoRecordToken); } }; + // checks whether desktop sharing is enabled and whether + // we have params to start automatically sharing + my.checkAutoEnableDesktopSharing = function () { + if (UIUtil.isButtonEnabled('desktop') + && config.autoEnableDesktopSharing) { + APP.desktopsharing.toggleScreenSharing(); + } + }; + // Shows or hides SIP calls button my.showSipCallButton = function (show) { if (APP.xmpp.isSipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) { From f9d1fd13dfb57652a7307b14cd0998d2489032b4 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 16 Nov 2015 13:33:29 -0600 Subject: [PATCH 03/44] Fixes an issue where lastN event, includes in the logic local resource and detects it as removed from lastN and schedules update of large video. If we receive this event for newly joined participant and we have pinned the local video, the event triggers update of large video which displays the wrong participant, not the pinned local video. --- modules/UI/videolayout/VideoLayout.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 6513280b9..5c4449e3f 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -690,6 +690,16 @@ var VideoLayout = (function (my) { $('#remoteVideos>span').each(function( index, element ) { var resourceJid = VideoLayout.getPeerContainerResourceJid(element); + // We do not want to process any logic for our own(local) video + // because the local participant is never in the lastN set. + // The code of this function might detect that the local participant + // has been dropped out of the lastN set and will update the large + // video + // Detected from avatar tests, where lastN event override + // local video pinning + if(resourceJid == APP.xmpp.myResource()) + return; + var isReceived = true; if (resourceJid && lastNEndpoints.indexOf(resourceJid) < 0 && From 0ae702922c76e5bf660361f4a78d05035a3375a9 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 16 Nov 2015 16:50:15 -0600 Subject: [PATCH 04/44] Makes the room parameter lower case. --- modules/xmpp/xmpp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/xmpp/xmpp.js b/modules/xmpp/xmpp.js index 9b28cb49f..3a980a5a4 100644 --- a/modules/xmpp/xmpp.js +++ b/modules/xmpp/xmpp.js @@ -322,7 +322,7 @@ var XMPP = { createConnection: function () { var bosh = config.bosh || '/http-bind'; // adds the room name used to the bosh connection - return new Strophe.Connection(bosh + '?ROOM=' + APP.UI.getRoomNode()); + return new Strophe.Connection(bosh + '?room=' + APP.UI.getRoomNode()); }, getStatusString: function (status) { return Strophe.getStatusString(status); From 7ea675159e0fa2e1c3af7a913cd2480e0a039062 Mon Sep 17 00:00:00 2001 From: yanas Date: Mon, 16 Nov 2015 18:06:28 -0600 Subject: [PATCH 05/44] Disables feedback functionality if callstats isn't available. --- css/main.css | 1 + modules/UI/Feedback.js | 30 +++++++++++++++++++++++++++--- modules/UI/UI.js | 5 +---- modules/UI/toolbars/Toolbar.js | 26 +++++++++++++++++++++----- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/css/main.css b/css/main.css index e4855d8c9..5a937025f 100644 --- a/css/main.css +++ b/css/main.css @@ -248,6 +248,7 @@ form { } div.feedbackButton { + display: none; position: absolute; background-color: rgba(0,0,0,.50); border-radius: 50%; diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js index 670bb38ea..baf53447c 100644 --- a/modules/UI/Feedback.js +++ b/modules/UI/Feedback.js @@ -1,4 +1,4 @@ -/* global $, interfaceConfig */ +/* global $, config, interfaceConfig */ /* * Created by Yana Stamcheva on 2/10/15. @@ -73,6 +73,30 @@ var Feedback = { * The feedback score. -1 indicates no score has been given for now. */ feedbackScore: -1, + /** + * Initialise the Feedback functionality. + */ + init: function () { + // CallStats is the way we send feedback, so we don't have to initialise + // if callstats isn't enabled. + if (!config.callStatsID || !config.callStatsSecret) + return; + + $("div.feedbackButton").css("display", "block"); + $("#feedbackButton").click(function (event) { + Feedback.openFeedbackWindow(); + }); + }, + /** + * Indicates if the feedback functionality is enabled. + * + * @return true if the feedback functionality is enabled, false otherwise. + */ + isEnabled: function() { + var isCallStatsEnabled = (config.callStatsID && config.callStatsSecret); + + return isCallStatsEnabled; + }, /** * Opens the feedback window. */ @@ -120,7 +144,7 @@ var Feedback = { var states = { overall_feedback: { html: constructOverallFeedbackHtml(), - persistent: true, + persistent: false, buttons: {}, closeText: '', focus: "div[id='stars']", @@ -161,7 +185,7 @@ var Feedback = { var feedbackDialog = APP.UI.messageHandler.openDialogWithStates( states, - { persistent: true, + { persistent: false, buttons: {}, closeText: '', loaded: onLoadFunction, diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 637256394..8cbb07709 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -429,15 +429,12 @@ UI.start = function (init) { $("#downloadlog").click(function (event) { dump(event.target); }); - $("#feedbackButton").click(function (event) { - Feedback.openFeedbackWindow(); - }); + Feedback.init(); } else { $("#header").css("display", "none"); $("#bottomToolbar").css("display", "none"); - $("#feedbackButton").css("display", "none"); $("#downloadlog").css("display", "none"); $("#remoteVideos").css("padding", "0px 0px 18px 0px"); $("#remoteVideos").css("right", "0px"); diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 03949b09f..092858101 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -151,12 +151,28 @@ function hangup() { } }; - if (Feedback.feedbackScore > 0) { - Feedback.openFeedbackWindow(); - conferenceDispose(); + if (Feedback.isEnabled()) + { + // If the user has already entered feedback, we'll show the window and + // immidiately start the conference dispose timeout. + if (Feedback.feedbackScore > 0) { + Feedback.openFeedbackWindow(); + conferenceDispose(); + + } + // Otherwise we'll wait for user's feedback. + else + Feedback.openFeedbackWindow(conferenceDispose); + } + else { + conferenceDispose(); + + // If the feedback functionality isn't enabled we show a thank you + // dialog. + APP.UI.messageHandler.openMessageDialog(null, null, null, + APP.translation.translateString("dialog.thankYou", + {appName:interfaceConfig.APP_NAME})); } - else - Feedback.openFeedbackWindow(conferenceDispose); } /** From b64f3a591327430c11af8998bbeb004ca89c2810 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 16 Nov 2015 17:42:21 -0600 Subject: [PATCH 06/44] Adds method to obtain remote video type. --- modules/UI/UI.js | 9 +++++++++ modules/UI/videolayout/VideoLayout.js | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 8cbb07709..59070a3db 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -700,6 +700,15 @@ UI.getLargeVideoResource = function () { return VideoLayout.getLargeVideoResource(); }; +/** + * Return the type of the remote video. + * @param jid the jid for the remote video + * @returns the video type video or screen. + */ +UI.getRemoteVideoType = function (jid) { + return VideoLayout.getRemoteVideoType(jid); +}; + UI.getRoomNode = function () { if (roomNode) return roomNode; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 5c4449e3f..a17b5f506 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -191,6 +191,15 @@ var VideoLayout = (function (my) { return LargeVideo.getResourceJid(); }; + /** + * Return the type of the remote video. + * @param jid the jid for the remote video + * @returns the video type video or screen. + */ + my.getRemoteVideoType = function (jid) { + return remoteVideoTypes[jid]; + }; + /** * Called when large video update is finished * @param currentSmallVideo small video currently displayed on large video From ce397d9e747dcdf4332fdc2cd0b946f420fd923f Mon Sep 17 00:00:00 2001 From: George Politis Date: Thu, 1 Oct 2015 17:23:40 -0500 Subject: [PATCH 07/44] Fixes issue in ssrc-group SDP parsing. How did this even work before? --- modules/xmpp/SDP.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/xmpp/SDP.js b/modules/xmpp/SDP.js index e799e5520..bacbe77ed 100644 --- a/modules/xmpp/SDP.js +++ b/modules/xmpp/SDP.js @@ -57,6 +57,7 @@ SDP.prototype.getMediaSsrcMap = function() { }); tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:'); tmp.forEach(function(line){ + var idx = line.indexOf(' '); var semantics = line.substr(0, idx).substr(13); var ssrcs = line.substr(14 + semantics.length).split(' '); if (ssrcs.length) { From 5d571e696f9612964f6573783c57950c51a585fa Mon Sep 17 00:00:00 2001 From: George Politis Date: Fri, 23 Oct 2015 09:32:29 -0500 Subject: [PATCH 08/44] Sets up simulcast for 2 layers. --- modules/xmpp/TraceablePeerConnection.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index 4e05a2811..56bba8944 100644 --- a/modules/xmpp/TraceablePeerConnection.js +++ b/modules/xmpp/TraceablePeerConnection.js @@ -25,7 +25,7 @@ function TraceablePeerConnection(ice_config, constraints, session) { var Interop = require('sdp-interop').Interop; this.interop = new Interop(); var Simulcast = require('sdp-simulcast'); - this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false}); + this.simulcast = new Simulcast({numOfLayers: 2, explodeRemoteSimulcast: false}); // override as desired this.trace = function (what, info) { @@ -218,6 +218,8 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { function() { var desc = this.peerconnection.localDescription; + // TODO this should be after the Unified Plan -> Plan B + // transformation. desc = SSRCReplacement.mungeLocalVideoSSRC(desc); this.trace('getLocalDescription::preTransform', dumpSDP(desc)); From 9f1e953e8afd3b0e1159066a8ce021e0ce86207d Mon Sep 17 00:00:00 2001 From: George Politis Date: Tue, 17 Nov 2015 18:10:41 +0000 Subject: [PATCH 09/44] Bumps sdp-simulcast@0.1.2. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed4b2eab3..0d3606b2b 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "pako": "*", "retry": "0.6.1", "sdp-interop": "0.1.10", - "sdp-simulcast": "0.1.0", + "sdp-simulcast": "0.1.2", "sdp-transform": "1.4.1", "socket.io-client": "1.3.6", "strophe": "^1.2.2", From 94b54279f253c92cf683cf32ea81427b260ee096 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 17 Nov 2015 14:27:26 -0600 Subject: [PATCH 10/44] Fixes wrong handler name, which causes adding multiple local video tags in the local video. --- modules/RTC/RTC.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/RTC/RTC.js b/modules/RTC/RTC.js index 9ac19b204..790e81708 100644 --- a/modules/RTC/RTC.js +++ b/modules/RTC/RTC.js @@ -303,7 +303,7 @@ var RTC = { if (mediaStream.addEventListener) { // chrome if(typeof mediaStream.active !== "undefined") - mediaStream.inactive = handler; + mediaStream.oninactive = handler; else mediaStream.onended = handler; } else { @@ -322,7 +322,7 @@ var RTC = { if (mediaStream.removeEventListener) { // chrome if(typeof mediaStream.active !== "undefined") - mediaStream.inactive = null; + mediaStream.oninactive = null; else mediaStream.onended = null; } else { From 72c39a0162f52d31ce92195e0726b13edb7b1318 Mon Sep 17 00:00:00 2001 From: isymchych Date: Fri, 13 Nov 2015 17:58:59 +0200 Subject: [PATCH 11/44] accumulate erorrs unitl connected to callstats --- modules/statistics/CallStats.js | 103 +++++++++++++----------- modules/statistics/statistics.js | 31 ++++--- modules/xmpp/JingleSessionPC.js | 2 +- modules/xmpp/TraceablePeerConnection.js | 8 +- 4 files changed, 79 insertions(+), 65 deletions(-) diff --git a/modules/statistics/CallStats.js b/modules/statistics/CallStats.js index e91932832..3ba54286f 100644 --- a/modules/statistics/CallStats.js +++ b/modules/statistics/CallStats.js @@ -5,20 +5,35 @@ var jsSHA = require('jssha'); var io = require('socket.io-client'); var callStats = null; -// getUserMedia calls happen before CallStats init -// so if there are any getUserMedia errors, we store them in this array +/** + * @const + * @see http://www.callstats.io/api/#enumeration-of-wrtcfuncnames + */ +var wrtcFuncNames = { + createOffer: "createOffer", + createAnswer: "createAnswer", + setLocalDescription: "setLocalDescription", + setRemoteDescription: "setRemoteDescription", + addIceCandidate: "addIceCandidate", + getUserMedia: "getUserMedia" +}; + +// some errors may happen before CallStats init +// in this case we accumulate them in this array // and send them to callstats on init -var pendingUserMediaErrors = []; +var pendingErrors = []; function initCallback (err, msg) { - console.log("Initializing Status: err="+err+" msg="+msg); + console.log("CallStats Status: err=" + err + " msg=" + msg); } +var callStatsIntegrationEnabled = config.callStatsID && config.callStatsSecret; + var CallStats = { init: function (jingleSession) { - - if(!config.callStatsID || !config.callStatsSecret || callStats !== null) + if(!callStatsIntegrationEnabled || callStats !== null) { return; + } callStats = new callstats($, io, jsSHA); @@ -44,10 +59,12 @@ var CallStats = { this.confID, this.pcCallback.bind(this)); - // notify callstats about getUserMedia failures if there were any - if (pendingUserMediaErrors.length) { - pendingUserMediaErrors.forEach(this.sendGetUserMediaFailed, this); - pendingUserMediaErrors.length = 0; + // notify callstats about failures if there were any + if (pendingErrors.length) { + pendingErrors.forEach(function (error) { + this._reportError(error.type, error.error, error.pc); + }, this); + pendingErrors.length = 0; } }, pcCallback: function (err, msg) { @@ -109,84 +126,76 @@ var CallStats = { this.confID, feedbackJSON); }, + _reportError: function (type, e, pc) { + if (callStats) { + callStats.reportError(pc, this.confID, type, e); + } else if (callStatsIntegrationEnabled) { + pendingErrors.push({ + type: type, + error: e, + pc: pc + }); + } + // else just ignore it + }, + /** * Notifies CallStats that getUserMedia failed. * * @param {Error} e error to send */ sendGetUserMediaFailed: function (e) { - if(!callStats) { - pendingUserMediaErrors.push(e); - return; - } - callStats.reportError(this.peerconnection, this.confID, - callStats.webRTCFunctions.getUserMedia, e); + this._reportError(wrtcFuncNames.getUserMedia, e, null); }, /** * Notifies CallStats that peer connection failed to create offer. * * @param {Error} e error to send + * @param {RTCPeerConnection} pc connection on which failure occured. */ - sendCreateOfferFailed: function (e) { - if(!callStats) { - return; - } - callStats.reportError(this.peerconnection, this.confID, - callStats.webRTCFunctions.createOffer, e); + sendCreateOfferFailed: function (e, pc) { + this._reportError(wrtcFuncNames.createOffer, e, pc); }, /** * Notifies CallStats that peer connection failed to create answer. * * @param {Error} e error to send + * @param {RTCPeerConnection} pc connection on which failure occured. */ - sendCreateAnswerFailed: function (e) { - if(!callStats) { - return; - } - callStats.reportError(this.peerconnection, this.confID, - callStats.webRTCFunctions.createAnswer, e); + sendCreateAnswerFailed: function (e, pc) { + this._reportError(wrtcFuncNames.createAnswer, e, pc); }, /** * Notifies CallStats that peer connection failed to set local description. * * @param {Error} e error to send + * @param {RTCPeerConnection} pc connection on which failure occured. */ - sendSetLocalDescFailed: function (e) { - if(!callStats) { - return; - } - callStats.reportError(this.peerconnection, this.confID, - callStats.webRTCFunctions.setLocalDescription, e); + sendSetLocalDescFailed: function (e, pc) { + this._reportError(wrtcFuncNames.setLocalDescription, e, pc); }, /** * Notifies CallStats that peer connection failed to set remote description. * * @param {Error} e error to send + * @param {RTCPeerConnection} pc connection on which failure occured. */ - sendSetRemoteDescFailed: function (e) { - if(!callStats) { - return; - } - callStats.reportError( - this.peerconnection, this.confID, - callStats.webRTCFunctions.setRemoteDescription, e); + sendSetRemoteDescFailed: function (e, pc) { + this._reportError(wrtcFuncNames.setRemoteDescription, e, pc); }, /** * Notifies CallStats that peer connection failed to add ICE candidate. * * @param {Error} e error to send + * @param {RTCPeerConnection} pc connection on which failure occured. */ - sendAddIceCandidateFailed: function (e) { - if(!callStats) { - return; - } - callStats.reportError(this.peerconnection, this.confID, - callStats.webRTCFunctions.addIceCandidate, e); + sendAddIceCandidateFailed: function (e, pc) { + this._reportError(wrtcFuncNames.addIceCandidate, e, pc); } }; module.exports = CallStats; diff --git a/modules/statistics/statistics.js b/modules/statistics/statistics.js index 91908f0de..9f63b0786 100644 --- a/modules/statistics/statistics.js +++ b/modules/statistics/statistics.js @@ -113,25 +113,30 @@ var statistics = { APP.RTC.addListener(RTCEvents.GET_USER_MEDIA_FAILED, function (e) { CallStats.sendGetUserMediaFailed(e); }); - APP.xmpp.addListener(RTCEvents.CREATE_OFFER_FAILED, function (e) { - CallStats.sendCreateOfferFailed(e); + APP.xmpp.addListener(RTCEvents.CREATE_OFFER_FAILED, function (e, pc) { + CallStats.sendCreateOfferFailed(e, pc); }); - APP.xmpp.addListener(RTCEvents.CREATE_ANSWER_FAILED, function (e) { - CallStats.sendCreateAnswerFailed(e); + APP.xmpp.addListener(RTCEvents.CREATE_ANSWER_FAILED, function (e, pc) { + CallStats.sendCreateAnswerFailed(e, pc); }); APP.xmpp.addListener( RTCEvents.SET_LOCAL_DESCRIPTION_FAILED, - function (e) { - CallStats.sendSetLocalDescFailed(e); - }); + function (e, pc) { + CallStats.sendSetLocalDescFailed(e, pc); + } + ); APP.xmpp.addListener( RTCEvents.SET_REMOTE_DESCRIPTION_FAILED, - function (e) { - CallStats.sendSetRemoteDescFailed(e); - }); - APP.xmpp.addListener(RTCEvents.ADD_ICE_CANDIDATE_FAILED, function (e) { - CallStats.sendAddIceCandidateFailed(e); - }); + function (e, pc) { + CallStats.sendSetRemoteDescFailed(e, pc); + } + ); + APP.xmpp.addListener( + RTCEvents.ADD_ICE_CANDIDATE_FAILED, + function (e, pc) { + CallStats.sendAddIceCandidateFailed(e, pc); + } + ); } }; diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js index 9f167d9af..6500e6186 100644 --- a/modules/xmpp/JingleSessionPC.js +++ b/modules/xmpp/JingleSessionPC.js @@ -720,7 +720,7 @@ JingleSessionPC.prototype.addIceCandidate = function (elem) { self.peerconnection.addIceCandidate(candidate); } catch (e) { console.error('addIceCandidate failed', e.toString(), line); - self.eventEmitter.emit(RTCEvents.ADD_ICE_CANDIDATE_FAILED, err); + self.eventEmitter.emit(RTCEvents.ADD_ICE_CANDIDATE_FAILED, err, self.peerconnection); } }); }); diff --git a/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index 56bba8944..5b006feb5 100644 --- a/modules/xmpp/TraceablePeerConnection.js +++ b/modules/xmpp/TraceablePeerConnection.js @@ -295,7 +295,7 @@ TraceablePeerConnection.prototype.setLocalDescription }, function (err) { self.trace('setLocalDescriptionOnFailure', err); - self.eventEmitter.emit(RTCEvents.SET_LOCAL_DESCRIPTION_FAILED, err); + self.eventEmitter.emit(RTCEvents.SET_LOCAL_DESCRIPTION_FAILED, err, self.peerconnection); failureCallback(err); } ); @@ -331,7 +331,7 @@ TraceablePeerConnection.prototype.setRemoteDescription }, function (err) { self.trace('setRemoteDescriptionOnFailure', err); - self.eventEmitter.emit(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED, err); + self.eventEmitter.emit(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED, err, self.peerconnection); failureCallback(err); } ); @@ -377,7 +377,7 @@ TraceablePeerConnection.prototype.createOffer }, function(err) { self.trace('createOfferOnFailure', err); - self.eventEmitter.emit(RTCEvents.CREATE_OFFER_FAILED, err); + self.eventEmitter.emit(RTCEvents.CREATE_OFFER_FAILED, err, self.peerconnection); failureCallback(err); }, constraints @@ -408,7 +408,7 @@ TraceablePeerConnection.prototype.createAnswer }, function(err) { self.trace('createAnswerOnFailure', err); - self.eventEmitter.emit(RTCEvents.CREATE_ANSWER_FAILED, err); + self.eventEmitter.emit(RTCEvents.CREATE_ANSWER_FAILED, err, self.peerconnection); failureCallback(err); }, constraints From 236c4bb37c3497a93565ad12e2b9b0da78055937 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 17 Nov 2015 16:39:05 -0600 Subject: [PATCH 12/44] Adds a method in callstats in order to check if it's enabled. --- modules/UI/Feedback.js | 6 ++---- modules/statistics/CallStats.js | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js index baf53447c..c9bbea0d6 100644 --- a/modules/UI/Feedback.js +++ b/modules/UI/Feedback.js @@ -79,7 +79,7 @@ var Feedback = { init: function () { // CallStats is the way we send feedback, so we don't have to initialise // if callstats isn't enabled. - if (!config.callStatsID || !config.callStatsSecret) + if (!callStats.isEnabled()) return; $("div.feedbackButton").css("display", "block"); @@ -93,9 +93,7 @@ var Feedback = { * @return true if the feedback functionality is enabled, false otherwise. */ isEnabled: function() { - var isCallStatsEnabled = (config.callStatsID && config.callStatsSecret); - - return isCallStatsEnabled; + return callStats.isEnabled(); }, /** * Opens the feedback window. diff --git a/modules/statistics/CallStats.js b/modules/statistics/CallStats.js index 3ba54286f..73327f0aa 100644 --- a/modules/statistics/CallStats.js +++ b/modules/statistics/CallStats.js @@ -67,6 +67,16 @@ var CallStats = { pendingErrors.length = 0; } }, + /** + * Returns true if the callstats integration is enabled, otherwise returns + * false. + * + * @returns true if the callstats integration is enabled, otherwise returns + * false. + */ + isEnabled: function() { + return callStatsIntegrationEnabled; + }, pcCallback: function (err, msg) { if (!callStats) { return; @@ -125,7 +135,14 @@ var CallStats = { callStats.sendUserFeedback( this.confID, feedbackJSON); }, - + /** + * Reports an error to callstats. + * + * @param type the type of the error, which will be one of the wrtcFuncNames + * @param e the error + * @param pc the peerconnection + * @private + */ _reportError: function (type, e, pc) { if (callStats) { callStats.reportError(pc, this.confID, type, e); From 19d9c0be50f75fabd91696c8560b1246f2b58e86 Mon Sep 17 00:00:00 2001 From: isymchych Date: Thu, 19 Nov 2015 14:32:07 +0200 Subject: [PATCH 13/44] fixed switching to large video from FF on safari --- modules/UI/videolayout/SmallVideo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 3d5e7acd2..8186fdffc 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -322,7 +322,7 @@ SmallVideo.prototype.selectVideoElement = function () { } else { return $('#' + this.videoSpanId + (this.isLocal ? '>>' : '>') + - videoElem + '>param[value="video"]').parent(); + videoElem + '>param[value="video"]').first().parent(); } }; From de311b137291cc2ad91ae3689d456fa549d3f20c Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Thu, 19 Nov 2015 16:21:10 -0600 Subject: [PATCH 14/44] Updates gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 501a87349..d04786e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules deploy-local.sh libs/app.bundle.* all.css +.remote-sync.json From 1d59283518c831a06237f85d663fc15edbfa52ff Mon Sep 17 00:00:00 2001 From: isymchych Date: Thu, 19 Nov 2015 14:32:07 +0200 Subject: [PATCH 15/44] fixed switching to large video from FF on safari --- modules/UI/videolayout/SmallVideo.js | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 3d5e7acd2..67ed2e986 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -320,9 +320,26 @@ SmallVideo.prototype.selectVideoElement = function () { if (!RTCBrowserType.isTemasysPluginUsed()) { return $('#' + this.videoSpanId).find(videoElem); } else { - return $('#' + this.videoSpanId + - (this.isLocal ? '>>' : '>') + - videoElem + '>param[value="video"]').parent(); + var matching = $('#' + this.videoSpanId + + (this.isLocal ? '>>' : '>') + + videoElem + '>param[value="video"]'); + if (matching.length < 2) { + return matching.parent(); + } + + // there are 2 video objects from FF + // object with id which ends with '_default' (like 'remoteVideo_default') + // doesn't contain video, so we ignore it + for (var i = 0; i < matching.length; i += 1) { + var el = matching[i].parentNode; + + // check id suffix + if (el.id.substr(-8) !== '_default') { + return $(el); + } + } + + return $([]); } }; From c3f9226ec8b90c6161f343e1fa040215d6afb8db Mon Sep 17 00:00:00 2001 From: George Politis Date: Mon, 23 Nov 2015 15:54:10 -0600 Subject: [PATCH 16/44] Updates the supported browser list and closes #372. - Adds Safari and IE in the supported browser list. - Adds version numbers for the supported browsers. --- css/unsupported_browser.css | 40 ++++++++++++++++++++++++++---------- images/ie.png | Bin 0 -> 4167 bytes images/safari.png | Bin 0 -> 12912 bytes unsupported_browser.html | 25 ++++++++++++++++++---- 4 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 images/ie.png create mode 100644 images/safari.png diff --git a/css/unsupported_browser.css b/css/unsupported_browser.css index 847103b91..65744b12d 100644 --- a/css/unsupported_browser.css +++ b/css/unsupported_browser.css @@ -11,8 +11,8 @@ body { #wrap{ display: block; position: absolute; - width:900px; - height: 365px; + width:500px; + height: 565px; overflow:hidden; text-align: center; margin: auto; @@ -29,7 +29,7 @@ body { #text{ display:inline-block; font-size: 28px; - width: 568px; + /* width: 568px; */ vertical-align:middle; padding-top: 25px; } @@ -51,18 +51,23 @@ a { .browser_wrapper { width: 138px; - height: 188px; + /* height: 188px; */ vertical-align: middle; color: #929391; font-size: 20px; float: left; margin-left: 15px; + margin-top: 5px; } +.browser_text +{ + height: 2em; +} .supported_browsers { margin: 0px auto 0px auto; - width: 660px; + /* width: 660px; */ } .clear @@ -97,14 +102,14 @@ a { } #chromium_logo { - width: 85px; - height: 79px; + width: 77px; + height: 78px; background-image: url('/images/chromium.png'); } #firefox_logo { - width: 73px; - height: 79px; + width: 86px; + height: 80px; background-image: url('/images/firefox.png'); } @@ -114,5 +119,18 @@ a { height: 78px; background-image: url('/images/opera.png'); } - - + +#safari_logo +{ + width: 78px; + height: 79px; + background-image: url('/images/safari.png'); +} + +#ie_logo +{ + width: 80px; + height: 78px; + background-image: url('/images/ie.png'); +} + diff --git a/images/ie.png b/images/ie.png new file mode 100644 index 0000000000000000000000000000000000000000..9d8e2a95dc5003c91660510683ff34a1bdaf2604 GIT binary patch literal 4167 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 M#J~byrSBH^KVUqCbN~PV literal 0 HcmV?d00001 diff --git a/images/safari.png b/images/safari.png new file mode 100644 index 0000000000000000000000000000000000000000..4cc2983723dfb12f61cb0be4d22a142ffc7ca218 GIT binary patch literal 12912 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 X{{I0w0DDB(0JyI*D$=!*rlJ1>F$h
- Chrome + Chrome 44+
- Chromium + Chromium 44+
- Opera + Opera 32+
- Firefox +
+ Firefox and Iceweasel 40+
+
+
+ IE
(Temasys 0.8.854+)
+
+ +
DOWNLOAD
+
+
+
+
+ Safari
(Temasys 0.8.854+)
+
+ +
DOWNLOAD
+
+
From 6b621654abb0c33c9abcf2be20e0a3cb419f57a4 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 23 Nov 2015 17:30:09 -0600 Subject: [PATCH 17/44] Adds speaker indicator and no longer use the display name for that purpose. --- css/videolayout_default.css | 21 ++++++++++++++++ interface_config.js | 1 - modules/UI/videolayout/RemoteVideo.js | 35 +++++++++++++++++++++++++++ modules/UI/videolayout/VideoLayout.js | 18 ++++++-------- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 02649ca18..02a71eafa 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -319,6 +319,27 @@ z-index: 3; } +.videocontainer>span.dominantspeakerindicator { + bottom: 0px; + left: 0px; + width: 25px; + height: 25px; + z-index: 3; + text-align: center; + border-radius: 50%; + background: #0cf; + margin: 5px; + display: inline-block; + position: absolute; + color: #FFFFFF; + font-size: 11pt; + border: 0px; +} + +#speakerindicatoricon { + padding-top: 5px; +} + #reloadPresentation { display: none; position: absolute; diff --git a/interface_config.js b/interface_config.js index d6dec390e..3078707e0 100644 --- a/interface_config.js +++ b/interface_config.js @@ -5,7 +5,6 @@ var interfaceConfig = { INITIAL_TOOLBAR_TIMEOUT: 20000, TOOLBAR_TIMEOUT: 4000, DEFAULT_REMOTE_DISPLAY_NAME: "Fellow Jitster", - DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME: "speaker", DEFAULT_LOCAL_DISPLAY_NAME: "me", SHOW_JITSI_WATERMARK: true, JITSI_WATERMARK_LINK: "https://jitsi.org", diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 4812bc720..7fafb0337 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -334,6 +334,41 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted) { } }; +/** + * Updates the Indicator for dominant speaker. + * + * @param isSpeaker indicates the current indicator state + */ +RemoteVideo.prototype.updateDominantSpeakerIndicator = function (isSpeaker) { + + if (!this.container) { + console.warn( "Unable to set dominant speaker indicator - " + + this.videoSpanId + " does not exist"); + return; + } + + var indicatorSpan + = $('#' + this.videoSpanId + '>span.dominantspeakerindicator'); + + // If we do not have an indicator for this video. + if (indicatorSpan.length <= 0) { + indicatorSpan = document.createElement('span'); + + indicatorSpan.innerHTML + = ""; + indicatorSpan.className = 'dominantspeakerindicator'; + + $('#' + this.videoSpanId)[0].appendChild(indicatorSpan); + + // adds a tooltip + UIUtils.setTooltip(indicatorSpan, "speaker", "left"); + APP.translation.translateElement($(indicatorSpan)); + } + + $(indicatorSpan).css("visibility", isSpeaker ? "visible" : "hidden"); +}; + + /** * Sets the display name for the given video span id. */ diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index a17b5f506..a22ac90e8 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -614,16 +614,14 @@ var VideoLayout = (function (my) { var members = APP.xmpp.getMembers(); // Update the current dominant speaker. if (resourceJid !== currentDominantSpeaker) { - var currentJID = APP.xmpp.findJidFromResource(currentDominantSpeaker); - var newJID = APP.xmpp.findJidFromResource(resourceJid); - if (currentDominantSpeaker && (!members || !members[currentJID] || - !members[currentJID].displayName) && remoteVideo) { - remoteVideo.setDisplayName(null); - } - if (resourceJid && (!members || !members[newJID] || - !members[newJID].displayName) && remoteVideo) { - remoteVideo.setDisplayName(null, - interfaceConfig.DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME); + if (remoteVideo) { + remoteVideo.updateDominantSpeakerIndicator(true); + // let's remove the indications from the remote video if any + var oldSpeakerRemoteVideo + = remoteVideos[currentDominantSpeaker]; + if (oldSpeakerRemoteVideo) { + oldSpeakerRemoteVideo.updateDominantSpeakerIndicator(false); + } } currentDominantSpeaker = resourceJid; } else { From c5b3677e71aa287376f075df25e85bb379a85f49 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 24 Nov 2015 13:10:29 -0600 Subject: [PATCH 18/44] Updates sdp-interop and sdp-transform versions, thanks to virtuacoplenny. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0d3606b2b..9e74f9791 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,9 @@ "jssha": "1.5.0", "pako": "*", "retry": "0.6.1", - "sdp-interop": "0.1.10", + "sdp-interop": "0.1.11", "sdp-simulcast": "0.1.2", - "sdp-transform": "1.4.1", + "sdp-transform": "1.5.2", "socket.io-client": "1.3.6", "strophe": "^1.2.2", "strophejs-plugins": "^0.0.6", From b2a3866fe42c0a99599203cb23f6745b7d61e8f8 Mon Sep 17 00:00:00 2001 From: bgrozev Date: Wed, 25 Nov 2015 15:46:41 -0600 Subject: [PATCH 19/44] Fixes a typo (reported by Emil Ivov) --- modules/util/RoomnameGenerator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/util/RoomnameGenerator.js b/modules/util/RoomnameGenerator.js index 198838091..4ed55d16e 100644 --- a/modules/util/RoomnameGenerator.js +++ b/modules/util/RoomnameGenerator.js @@ -16,7 +16,7 @@ var pluralNouns = [ "Priests", "Rats", "Reptiles", "Reptilians", "Rhinos", "Seagulls", "Sheep", "Siblings", "Snakes", "Spaghetti", "Spiders", "Squid", "Squirrels", "Stars", "Students", "Teachers", "Tigers", "Tomatoes", "Trees", "Vampires", - "Vegetables", "Viruses", "Vulcans", "Warewolves", "Weasels", "Whales", + "Vegetables", "Viruses", "Vulcans", "Weasels", "Werewolves", "Whales", "Witches", "Wizards", "Wolves", "Workers", "Worms", "Zebras" ]; //var places = [ From 286225e81e0a9eeefa346ec90291584088dc033d Mon Sep 17 00:00:00 2001 From: paweldomas Date: Wed, 25 Nov 2015 11:56:58 -0600 Subject: [PATCH 20/44] Removes unused code used to inject local SSRCs. --- modules/xmpp/JingleSessionPC.js | 14 ++------------ modules/xmpp/SDP.js | 8 ++------ 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js index 6500e6186..e7772a9d4 100644 --- a/modules/xmpp/JingleSessionPC.js +++ b/modules/xmpp/JingleSessionPC.js @@ -40,7 +40,6 @@ function JingleSessionPC(me, sid, connection, service, eventEmitter) { this.switchstreams = false; this.wait = true; - this.localStreamsSSRC = null; this.ssrcOwners = {}; this.ssrcVideoTypes = {}; this.eventEmitter = eventEmitter; @@ -256,8 +255,7 @@ JingleSessionPC.prototype.accept = function () { // FIXME why do we generate session-accept in 3 different places ? prsdp.toJingle( accept, - this.initiator == this.me ? 'initiator' : 'responder', - this.localStreamsSSRC); + this.initiator == this.me ? 'initiator' : 'responder'); var sdp = this.peerconnection.localDescription.sdp; while (SDPUtil.find_line(sdp, 'a=inactive')) { // FIXME: change any inactive to sendrecv or whatever they were originally @@ -484,8 +482,7 @@ JingleSessionPC.prototype.createdOffer = function (sdp) { sid: this.sid}); self.localSDP.toJingle( init, - this.initiator == this.me ? 'initiator' : 'responder', - this.localStreamsSSRC); + this.initiator == this.me ? 'initiator' : 'responder'); SSRCReplacement.processSessionInit(init); @@ -1429,13 +1426,6 @@ JingleSessionPC.prototype.setLocalDescription = function () { }); }); } - else if(self.localStreamsSSRC && self.localStreamsSSRC[media.type]) - { - newssrcs.push({ - 'ssrc': self.localStreamsSSRC[media.type], - 'type': media.type - }); - } }); diff --git a/modules/xmpp/SDP.js b/modules/xmpp/SDP.js index bacbe77ed..fc24b4d34 100644 --- a/modules/xmpp/SDP.js +++ b/modules/xmpp/SDP.js @@ -139,7 +139,7 @@ SDP.prototype.removeMediaLines = function(mediaindex, prefix) { }; // add content's to a jingle element -SDP.prototype.toJingle = function (elem, thecreator, ssrcs) { +SDP.prototype.toJingle = function (elem, thecreator) { // console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]); var i, j, k, mline, ssrc, rtpmap, tmp, lines; // new bundle plan @@ -166,11 +166,7 @@ SDP.prototype.toJingle = function (elem, thecreator, ssrcs) { if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) { ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first } else { - if(ssrcs && ssrcs[mline.media]) { - ssrc = ssrcs[mline.media]; - } else { - ssrc = false; - } + ssrc = false; } elem.c('content', {creator: thecreator, name: mline.media}); From 0a7cea26b3be3ce58f7fd42ae5d7341453b6bee3 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Wed, 25 Nov 2015 16:05:58 -0600 Subject: [PATCH 21/44] Exposes methods for obtaining stream SSRCs and audio levels. --- modules/statistics/statistics.js | 16 ++++++++++++++++ modules/xmpp/JingleSessionPC.js | 16 ++++++++++++++++ modules/xmpp/xmpp.js | 13 +++++++++++++ 3 files changed, 45 insertions(+) diff --git a/modules/statistics/statistics.js b/modules/statistics/statistics.js index 9f63b0786..00bf4c0ca 100644 --- a/modules/statistics/statistics.js +++ b/modules/statistics/statistics.js @@ -137,6 +137,22 @@ var statistics = { CallStats.sendAddIceCandidateFailed(e, pc); } ); + }, + /** + * Obtains audio level reported in the stats for specified peer. + * @param peerJid full MUC jid of the user for whom we want to obtain last + * audio level. + * @param ssrc the SSRC of audio stream for which we want to obtain audio + * level. + * @returns {*} a float form 0 to 1 that represents current audio level or + * null if for any reason the value is not available + * at this time. + */ + getPeerSSRCAudioLevel: function (peerJid, ssrc) { + + var peerStats = rtpStats.jid2stats[peerJid]; + + return peerStats ? peerStats.ssrc2AudioLevel[ssrc] : null; } }; diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js index e7772a9d4..dc53b6e01 100644 --- a/modules/xmpp/JingleSessionPC.js +++ b/modules/xmpp/JingleSessionPC.js @@ -40,6 +40,11 @@ function JingleSessionPC(me, sid, connection, service, eventEmitter) { this.switchstreams = false; this.wait = true; + /** + * A map that stores SSRCs of local streams + * @type {{}} maps media type('audio' or 'video') to SSRC number + */ + this.localStreamsSSRC = {}; this.ssrcOwners = {}; this.ssrcVideoTypes = {}; this.eventEmitter = eventEmitter; @@ -552,6 +557,15 @@ JingleSessionPC.prototype.getSsrcOwner = function (ssrc) { return this.ssrcOwners[ssrc]; }; +/** + * Returns the SSRC of local audio stream. + * @param mediaType 'audio' or 'video' media type + * @returns {*} the SSRC number of local audio or video stream. + */ +JingleSessionPC.prototype.getLocalSSRC = function (mediaType) { + return this.localStreamsSSRC[mediaType]; +}; + JingleSessionPC.prototype.setRemoteDescription = function (elem, desctype) { this.remoteSDP = new SDP(''); if (config.webrtcIceTcpDisable) { @@ -1424,6 +1438,8 @@ JingleSessionPC.prototype.setLocalDescription = function () { 'ssrc': ssrc.id, 'type': media.type }); + // FIXME allows for only one SSRC per media type + self.localStreamsSSRC[media.type] = ssrc.id; }); } diff --git a/modules/xmpp/xmpp.js b/modules/xmpp/xmpp.js index 3a980a5a4..0e90713d2 100644 --- a/modules/xmpp/xmpp.js +++ b/modules/xmpp/xmpp.js @@ -596,6 +596,19 @@ var XMPP = { return null; return connection.jingle.activecall.getSsrcOwner(ssrc); }, + /** + * Gets the SSRC of local media stream. + * @param mediaType the media type that tells whether we want to get + * the SSRC of local audio or video stream. + * @returns {*} the SSRC number for local media stream or null if + * not available. + */ + getLocalSSRC: function (mediaType) { + if (!this.isConferenceInProgress()) { + return null; + } + return connection.jingle.activecall.getLocalSSRC(mediaType); + }, // Returns true iff we have joined the MUC. isMUCJoined: function () { return connection === null ? false : connection.emuc.joined; From b6786716074b8d5835de3a30b2b376f2a20707ae Mon Sep 17 00:00:00 2001 From: George Politis Date: Tue, 1 Dec 2015 13:24:15 -0600 Subject: [PATCH 22/44] Revert "Sets up simulcast for 2 layers." This reverts commit b2993d8cf3f647c3194257eef0b21b77026b7bef. --- modules/xmpp/TraceablePeerConnection.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index 5b006feb5..eb8771387 100644 --- a/modules/xmpp/TraceablePeerConnection.js +++ b/modules/xmpp/TraceablePeerConnection.js @@ -25,7 +25,7 @@ function TraceablePeerConnection(ice_config, constraints, session) { var Interop = require('sdp-interop').Interop; this.interop = new Interop(); var Simulcast = require('sdp-simulcast'); - this.simulcast = new Simulcast({numOfLayers: 2, explodeRemoteSimulcast: false}); + this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false}); // override as desired this.trace = function (what, info) { @@ -218,8 +218,6 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { function() { var desc = this.peerconnection.localDescription; - // TODO this should be after the Unified Plan -> Plan B - // transformation. desc = SSRCReplacement.mungeLocalVideoSSRC(desc); this.trace('getLocalDescription::preTransform', dumpSDP(desc)); From d42415959facddca687197c5a9b509ca91bce149 Mon Sep 17 00:00:00 2001 From: George Politis Date: Tue, 1 Dec 2015 13:25:20 -0600 Subject: [PATCH 23/44] Adds comment. --- modules/xmpp/TraceablePeerConnection.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index eb8771387..7df5bd8a5 100644 --- a/modules/xmpp/TraceablePeerConnection.js +++ b/modules/xmpp/TraceablePeerConnection.js @@ -218,6 +218,8 @@ if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { function() { var desc = this.peerconnection.localDescription; + // FIXME this should probably be after the Unified Plan -> Plan B + // transformation. desc = SSRCReplacement.mungeLocalVideoSSRC(desc); this.trace('getLocalDescription::preTransform', dumpSDP(desc)); From d7317a94bb74e049ac47f253db1fe84d8cc63fb9 Mon Sep 17 00:00:00 2001 From: damencho Date: Wed, 2 Dec 2015 16:35:00 -0600 Subject: [PATCH 24/44] Converts ssltcp candidate to tcp one on FF. --- modules/xmpp/SDPUtil.js | 13 +++++++++++-- package.json | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/xmpp/SDPUtil.js b/modules/xmpp/SDPUtil.js index ad581fc06..ed1057050 100644 --- a/modules/xmpp/SDPUtil.js +++ b/modules/xmpp/SDPUtil.js @@ -1,4 +1,6 @@ /* jshint -W101 */ +var RTCBrowserType = require("../RTC/RTCBrowserType"); + var SDPUtil = { filter_special_chars: function (text) { return text.replace(/[\\\/\{,\}\+]/g, ""); @@ -311,7 +313,14 @@ var SDPUtil = { line += ' '; line += cand.getAttribute('component'); line += ' '; - line += cand.getAttribute('protocol'); //.toUpperCase(); // chrome M23 doesn't like this + + var protocol = cand.getAttribute('protocol'); + // use tcp candidates for FF + if (RTCBrowserType.isFirefox() && protocol.toLowerCase() == 'ssltcp') { + protocol = 'tcp'; + } + + line += protocol; //.toUpperCase(); // chrome M23 doesn't like this line += ' '; line += cand.getAttribute('priority'); line += ' '; @@ -338,7 +347,7 @@ var SDPUtil = { } break; } - if (cand.getAttribute('protocol').toLowerCase() == 'tcp') { + if (protocol.toLowerCase() == 'tcp') { line += 'tcptype'; line += ' '; line += cand.getAttribute('tcptype'); diff --git a/package.json b/package.json index 9e74f9791..c133f3c07 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,8 @@ "pako": "*", "retry": "0.6.1", "sdp-interop": "0.1.11", - "sdp-simulcast": "0.1.2", - "sdp-transform": "1.5.2", + "sdp-simulcast": "0.1.3", + "sdp-transform": "1.5.*", "socket.io-client": "1.3.6", "strophe": "^1.2.2", "strophejs-plugins": "^0.0.6", From 09a509400fc822e4e288787283cf7157711be37e Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 4 Dec 2015 10:38:42 -0600 Subject: [PATCH 25/44] Fixes showing prezi button. --- modules/UI/toolbars/Toolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 092858101..b60d87b74 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -396,7 +396,7 @@ var Toolbar = (function (my) { * Disables and enables some of the buttons. */ my.setupButtonsFromConfig = function () { - if (UIUtil.isButtonEnabled('prezi')) { + if (!UIUtil.isButtonEnabled('prezi')) { $("#toolbar_button_prezi").css({display: "none"}); } }; From 5f6bba435c2ba0ff566bca5ba575ab7e2fb0432e Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 4 Dec 2015 11:30:11 -0600 Subject: [PATCH 26/44] Fixes Uncaught TypeError: mediaStream.detachEvent on stopping desktop sharing. --- modules/RTC/RTC.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/RTC/RTC.js b/modules/RTC/RTC.js index 790e81708..79d212f21 100644 --- a/modules/RTC/RTC.js +++ b/modules/RTC/RTC.js @@ -300,18 +300,18 @@ var RTC = { * @param handler the handler */ addMediaStreamInactiveHandler: function (mediaStream, handler) { - if (mediaStream.addEventListener) { - // chrome - if(typeof mediaStream.active !== "undefined") - mediaStream.oninactive = handler; - else - mediaStream.onended = handler; - } else { + if(RTCBrowserType.isTemasysPluginUsed()) { // themasys mediaStream.attachEvent('ended', function () { handler(mediaStream); }); } + else { + if(typeof mediaStream.active !== "undefined") + mediaStream.oninactive = handler; + else + mediaStream.onended = handler; + } }, /** * Removes onended/inactive handler. @@ -319,15 +319,15 @@ var RTC = { * @param handler the handler to remove. */ removeMediaStreamInactiveHandler: function (mediaStream, handler) { - if (mediaStream.removeEventListener) { - // chrome + if(RTCBrowserType.isTemasysPluginUsed()) { + // themasys + mediaStream.detachEvent('ended', handler); + } + else { if(typeof mediaStream.active !== "undefined") mediaStream.oninactive = null; else mediaStream.onended = null; - } else { - // themasys - mediaStream.detachEvent('ended', handler); } } }; From 3f42f8bf671a44cb6662dda98a16454526dd43ba Mon Sep 17 00:00:00 2001 From: Jesse Bickel Date: Fri, 4 Dec 2015 13:03:10 -0600 Subject: [PATCH 27/44] Only load 3rd party JavaScript when config.enableThirdPartyRequests is true. --- config.js | 1 + index.html | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/config.js b/config.js index 90bbd6410..1bb3d1563 100644 --- a/config.js +++ b/config.js @@ -69,4 +69,5 @@ var config = { /*noticeMessage: 'Service update is scheduled for 16th March 2015. ' + 'During that time service will not be available. ' + 'Apologise for inconvenience.'*/ + enableThirdPartyRequests: true }; diff --git a/index.html b/index.html index 93681fc6d..facee3d0e 100644 --- a/index.html +++ b/index.html @@ -11,11 +11,9 @@ - - @@ -224,5 +222,20 @@ + From 895bb3fd608a7a13848909538fc957e24aa61383 Mon Sep 17 00:00:00 2001 From: Jesse Bickel Date: Mon, 7 Dec 2015 14:41:35 -0600 Subject: [PATCH 28/44] Use gravatar when enabled. --- index.html | 2 +- modules/UI/avatar/Avatar.js | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/index.html b/index.html index facee3d0e..8b134f2cc 100644 --- a/index.html +++ b/index.html @@ -201,7 +201,7 @@
- +
diff --git a/modules/UI/avatar/Avatar.js b/modules/UI/avatar/Avatar.js index 6ab1327e4..d59f494c7 100644 --- a/modules/UI/avatar/Avatar.js +++ b/modules/UI/avatar/Avatar.js @@ -1,4 +1,4 @@ -/* global Strophe, APP, MD5 */ +/* global Strophe, APP, MD5, config */ var Settings = require("../../settings/Settings"); var users = {}; @@ -57,12 +57,16 @@ var Avatar = { "No avatar stored yet for " + jid + " - using JID as ID"); id = jid; } - return 'https://www.gravatar.com/avatar/' + - MD5.hexdigest(id.trim().toLowerCase()) + - "?d=wavatar&size=" + (size || "30"); + if (config.enableThirdPartyRequests === true) { + return 'https://www.gravatar.com/avatar/' + + MD5.hexdigest(id.trim().toLowerCase()) + + "?d=wavatar&size=" + (size || "30"); + } else { + return 'images/avatar2.png'; + } } }; -module.exports = Avatar; \ No newline at end of file +module.exports = Avatar; From c2c3d0fd8734c507c75ddd173ca905ddceafa160 Mon Sep 17 00:00:00 2001 From: Jesse Bickel Date: Mon, 7 Dec 2015 15:52:11 -0600 Subject: [PATCH 29/44] Shrink the avatar on contact list to <=30px. --- css/contact_list.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/css/contact_list.css b/css/contact_list.css index 4929cc90b..7dab8a523 100644 --- a/css/contact_list.css +++ b/css/contact_list.css @@ -44,6 +44,8 @@ vertical-align: middle; font-size: 22pt; border-radius: 20px; + max-height: 30px; + max-width: 30px; } #contactlist .clickable { From 3406802aa8d66abdcd8f85222c5a6b8b1fd212ab Mon Sep 17 00:00:00 2001 From: paweldomas Date: Wed, 18 Nov 2015 12:49:36 -0600 Subject: [PATCH 30/44] Uses JWT for token generation in prosody plugin. --- prosody-plugins/mod_auth_token.lua | 3 +- prosody-plugins/mod_token_verification.lua | 19 ++++-- prosody-plugins/token/util.lib.lua | 78 ++++++---------------- 3 files changed, 35 insertions(+), 65 deletions(-) diff --git a/prosody-plugins/mod_auth_token.lua b/prosody-plugins/mod_auth_token.lua index bf2df1920..a8a899dac 100644 --- a/prosody-plugins/mod_auth_token.lua +++ b/prosody-plugins/mod_auth_token.lua @@ -27,10 +27,9 @@ local provider = {}; local appId = module:get_option_string("app_id"); local appSecret = module:get_option_string("app_secret"); -local tokenLifetime = module:get_option_number("token_lifetime"); function provider.test_password(username, password) - local result, msg = token_util.verify_password(password, appId, appSecret, tokenLifetime); + local result, msg = token_util.verify_password(password, appId, appSecret, nil); if result == true then return true; else diff --git a/prosody-plugins/mod_token_verification.lua b/prosody-plugins/mod_token_verification.lua index 08e1e53ec..79933040f 100644 --- a/prosody-plugins/mod_token_verification.lua +++ b/prosody-plugins/mod_token_verification.lua @@ -22,25 +22,34 @@ end local appId = parentCtx:get_option_string("app_id"); local appSecret = parentCtx:get_option_string("app_secret"); -local tokenLifetime = parentCtx:get_option_string("token_lifetime"); -log("debug", "%s - starting MUC token verifier app_id: %s app_secret: %s token-lifetime: %s", - tostring(host), tostring(appId), tostring(appSecret), tostring(tokenLifetime)); +log("debug", "%s - starting MUC token verifier app_id: %s app_secret: %s", + tostring(host), tostring(appId), tostring(appSecret)); local function handle_pre_create(event) + local origin, stanza = event.origin, event.stanza; local token = stanza:get_child("token", "http://jitsi.org/jitmeet/auth-token"); + -- token not required for admin users local user_jid = stanza.attr.from; if is_admin(user_jid) then log("debug", "Token not required from admin user: %s", user_jid); return nil; end - log("debug", "Will verify token for user: %s ", user_jid); + + local room = string.match(stanza.attr.to, "^(%w+)@"); + log("debug", "Will verify token for user: %s, room: %s ", user_jid, room); + if room == nil then + log("error", "Unable to get name of the MUC room ? to: %s", stanza.attr.to); + return nil; + end + if token ~= nil then token = token[1]; end - local result, msg = token_util.verify_password(token, appId, appSecret, tokenLifetime); + + local result, msg = token_util.verify_password(token, appId, appSecret, room); if result ~= true then log("debug", "Token verification failed: %s", msg); origin.send(st.error_reply(stanza, "cancel", "not-allowed", msg)); diff --git a/prosody-plugins/token/util.lib.lua b/prosody-plugins/token/util.lib.lua index 64747e03a..6781f07b9 100644 --- a/prosody-plugins/token/util.lib.lua +++ b/prosody-plugins/token/util.lib.lua @@ -1,76 +1,38 @@ -- Token authentication -- Copyright (C) 2015 Atlassian -local hashes = require "util.hashes"; +local jwt = require "luajwt"; local _M = {}; -local function calc_hash(password, appId, appSecret) - local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); - if hash ~= nil and room ~= nil and ts ~= nil then - log("debug", "Hash: '%s' room: '%s', ts: '%s'", hash, room, ts); - local toHash = room .. ts .. appId .. appSecret; - log("debug", "to be hashed: '%s'", toHash); - local hash = hashes.sha256(toHash, true); - log("debug", "hash: '%s'", hash); - return hash; - else - log("error", "Invalid password format: '%s'", password); - return nil; - end -end +local function verify_password_impl(password, appId, appSecret, roomName) -local function extract_hash(password) - local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); - return hash; -end - -local function extract_ts(password) - local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); - return ts; -end - -local function get_utc_timestamp() - return os.time(os.date("!*t")) * 1000; -end - -local function verify_timestamp(ts, tokenLifetime) - return get_utc_timestamp() - ts <= tokenLifetime; -end - -local function verify_password_impl(password, appId, appSecret, tokenLifetime) - - if password == nil then - return nil, "password is missing"; - end - - if tokenLifetime == nil then - tokenLifetime = 24 * 60 * 60 * 1000; + local claims, err = jwt.decode(password, appSecret, true); + if claims == nil then + return nil, err; end - local ts = extract_ts(password); - if ts == nil then - return nil, "timestamp not found in the password"; + local issClaim = claims["iss"]; + if issClaim == nil then + return nil, "Issuer field is missing"; end - local os_ts = get_utc_timestamp(); - log("debug", "System TS: '%s' user TS: %s", tostring(os_ts), tostring(ts)); - local isValid = verify_timestamp(ts, tokenLifetime); - if not isValid then - return nil, "token expired"; + if issClaim ~= appId then + return nil, "Invalid application ID('iss' claim)"; end - local realHash = calc_hash(password, appId, appSecret); - local givenhash = extract_hash(password); - log("debug", "Compare '%s' to '%s'", tostring(realHash), tostring(givenhash)); - if realHash == givenhash then - return true; - else - return nil, "invalid hash"; + local roomClaim = claims["room"]; + if roomClaim == nil then + return nil, "Room field is missing"; end + if roomName ~= nil and roomName ~= roomClaim then + return nil, "Invalid room name('room' claim)"; + end + + return true; end -function _M.verify_password(password, appId, appSecret, tokenLifetime) - return verify_password_impl(password, appId, appSecret, tokenLifetime); +function _M.verify_password(password, appId, appSecret, roomName) + return verify_password_impl(password, appId, appSecret, roomName); end return _M; From 1e3ef532aa426176ba78c96b412744e96bc0a865 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 20 Nov 2015 13:28:42 -0600 Subject: [PATCH 31/44] Adds 'enforcedBridge' property used to make Jicofo use specific bridge and ignore what BridgeSelector module says. --- modules/xmpp/moderator.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/xmpp/moderator.js b/modules/xmpp/moderator.js index 0c32057dc..8d355d343 100644 --- a/modules/xmpp/moderator.js +++ b/modules/xmpp/moderator.js @@ -149,6 +149,14 @@ var Moderator = { { name: 'bridge', value: config.hosts.bridge}) .up(); } + + if (config.enforcedBridge) { + elem.c( + 'property', + { name: 'enforcedBridge', value: config.enforcedBridge}) + .up(); + } + // Tell the focus we have Jigasi configured if (config.hosts.call_control !== undefined) { elem.c( From f42684d7896061580ee10614bb05f8246e676bbc Mon Sep 17 00:00:00 2001 From: Boris Grozev Date: Tue, 8 Dec 2015 22:51:29 +0000 Subject: [PATCH 32/44] Changes enableThirdParty requests to disableThirdParty requests, in order to not change existing behaviour (without changes to config.js). --- config.js | 2 +- index.html | 6 ++---- modules/UI/avatar/Avatar.js | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config.js b/config.js index 1bb3d1563..f180124f2 100644 --- a/config.js +++ b/config.js @@ -69,5 +69,5 @@ var config = { /*noticeMessage: 'Service update is scheduled for 16th March 2015. ' + 'During that time service will not be available. ' + 'Apologise for inconvenience.'*/ - enableThirdPartyRequests: true + disableThirdPartyRequests: false }; diff --git a/index.html b/index.html index 8b134f2cc..d66835e1d 100644 --- a/index.html +++ b/index.html @@ -223,13 +223,11 @@