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 diff --git a/config.js b/config.js index 90bbd6410..f180124f2 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.'*/ + disableThirdPartyRequests: false }; 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 { 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/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/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/debian/control b/debian/control index 47d274ce9..3d4368f9a 100644 --- a/debian/control +++ b/debian/control @@ -35,6 +35,6 @@ Description: Prosody configuration for Jitsi Meet Package: jitsi-meet-tokens Architecture: all -Depends: ${misc:Depends}, prosody | prosody-trunk, jitsi-meet-prosody +Depends: ${misc:Depends}, prosody-trunk (>= 1nightly603), libssl-dev, luarocks, jitsi-meet-prosody Description: Prosody token authentication plugin for Jitsi Meet diff --git a/debian/jitsi-meet-tokens.postinst b/debian/jitsi-meet-tokens.postinst index 4343e219a..d298316ab 100644 --- a/debian/jitsi-meet-tokens.postinst +++ b/debian/jitsi-meet-tokens.postinst @@ -62,13 +62,23 @@ case "$1" in sed -i 's/--plugin_paths/plugin_paths/g' $PROSODY_HOST_CONFIG sed -i 's/authentication = "anonymous"/authentication = "token"/g' $PROSODY_HOST_CONFIG sed -i 's/ --allow_unencrypted_plain_auth/ allow_unencrypted_plain_auth/g' $PROSODY_HOST_CONFIG - sed -i "s/ --app_id=example_app_id/ app_id=$APP_ID/g" $PROSODY_HOST_CONFIG - sed -i "s/ --app_secret=example_app_secret/ app_secret=$APP_SECRET/g" $PROSODY_HOST_CONFIG + sed -i "s/ --app_id=\"example_app_id\"/ app_id=\"$APP_ID\"/g" $PROSODY_HOST_CONFIG + sed -i "s/ --app_secret=\"example_app_secret\"/ app_secret=\"$APP_SECRET\"/g" $PROSODY_HOST_CONFIG sed -i 's/ --modules_enabled = { "token_verification" }/ modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG - if [ -x "/etc/init.d/prosody" ]; then - invoke-rc.d prosody reload + # Install luajwt + if ! luarocks install luajwt; then + echo "Failed to install luajwt - try installing it manually" fi + + if [ -x "/etc/init.d/prosody" ]; then + invoke-rc.d prosody restart + fi + + echo "This package requires BOSH Prosody module to be patched !" + echo "Use the following command, after this package has been installed and" + echo "after every prosody-trunk upgrade:" + echo "sudo patch -N /usr/lib/prosody/modules/mod_bosh.lua /usr/share/jitsi-meet/prosody-plugins/mod_bosh.lua.patch" else echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet" fi diff --git a/debian/jitsi-meet-tokens.postrm b/debian/jitsi-meet-tokens.postrm index 53a69f530..b57fe1ee7 100644 --- a/debian/jitsi-meet-tokens.postrm +++ b/debian/jitsi-meet-tokens.postrm @@ -39,13 +39,12 @@ case "$1" in # Revert prosody config sed -i 's/plugin_paths/--plugin_paths/g' $PROSODY_HOST_CONFIG sed -i 's/authentication = "token"/authentication = "anonymous"/g' $PROSODY_HOST_CONFIG - sed -i 's/ allow_unencrypted_plain_auth/ --allow_unencrypted_plain_auth/g' $PROSODY_HOST_CONFIG - sed -i "s/ app_id=$APP_ID/ --app_id=example_app_id/g" $PROSODY_HOST_CONFIG - sed -i "s/ app_secret=$APP_SECRET/ --app_secret=example_app_secret/g" $PROSODY_HOST_CONFIG + sed -i "s/ app_id=\"$APP_ID\"/ --app_id=\"example_app_id\"/g" $PROSODY_HOST_CONFIG + sed -i "s/ app_secret=\"$APP_SECRET\"/ --app_secret=\"example_app_secret\"/g" $PROSODY_HOST_CONFIG sed -i 's/ modules_enabled = { "token_verification" }/ --modules_enabled = { "token_verification" }/g' $PROSODY_HOST_CONFIG if [ -x "/etc/init.d/prosody" ]; then - invoke-rc.d prosody reload + invoke-rc.d prosody restart fi fi diff --git a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example index dd4302c63..36efd73f5 100644 --- a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example +++ b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example @@ -4,11 +4,10 @@ VirtualHost "jitmeet.example.com" -- enabled = false -- Remove this line to enable this host authentication = "anonymous" - -- Three properties below get uncommented by jitsi-meet-tokens package config + -- Properties below are modified by jitsi-meet-tokens package config -- and authentication above is switched to "token" - --allow_unencrypted_plain_auth = true; - --app_id=example_app_id - --app_secret=example_app_secret + --app_id="example_app_id" + --app_secret="example_app_secret" -- Assign this host a certificate for TLS, otherwise it would use the one -- set in the global section (if any). -- Note that old-style SSL on port 5223 only supports one certificate, and will always diff --git a/images/ie.png b/images/ie.png new file mode 100644 index 000000000..9d8e2a95d Binary files /dev/null and b/images/ie.png differ diff --git a/images/safari.png b/images/safari.png new file mode 100644 index 000000000..4cc298372 Binary files /dev/null and b/images/safari.png differ diff --git a/index.html b/index.html index 590c2884b..042410a21 100644 --- a/index.html +++ b/index.html @@ -15,11 +15,9 @@ - - @@ -225,7 +223,7 @@
- +
@@ -246,5 +244,18 @@
+ diff --git a/interface_config.js b/interface_config.js index d6dec390e..413640dda 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", @@ -28,5 +27,7 @@ var interfaceConfig = { /** * Whether to only show the filmstrip (and hide the toolbar). */ - filmStripOnly: false + filmStripOnly: false, + RANDOM_AVATAR_URL_PREFIX: false, + RANDOM_AVATAR_URL_SUFFIX: false }; diff --git a/modules/RTC/RTC.js b/modules/RTC/RTC.js index 9ac19b204..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.inactive = 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,16 +319,16 @@ var RTC = { * @param handler the handler to remove. */ removeMediaStreamInactiveHandler: function (mediaStream, handler) { - if (mediaStream.removeEventListener) { - // chrome - if(typeof mediaStream.active !== "undefined") - mediaStream.inactive = null; - else - mediaStream.onended = null; - } else { + if(RTCBrowserType.isTemasysPluginUsed()) { // themasys mediaStream.detachEvent('ended', handler); } + else { + if(typeof mediaStream.active !== "undefined") + mediaStream.oninactive = null; + else + mediaStream.onended = null; + } } }; diff --git a/modules/RTC/RTCUtils.js b/modules/RTC/RTCUtils.js index 67872b76f..761c805b4 100644 --- a/modules/RTC/RTCUtils.js +++ b/modules/RTC/RTCUtils.js @@ -146,7 +146,9 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream) { // this later can be a problem with some of the tests if(RTCBrowserType.isFirefox() && config.firefox_fake_device) { - constraints.audio = true; + // seems to be fixed now, removing this experimental fix, as having + // multiple audio tracks brake the tests + //constraints.audio = true; constraints.fake = true; } diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js index 670bb38ea..c9bbea0d6 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,28 @@ 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 (!callStats.isEnabled()) + 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() { + return callStats.isEnabled(); + }, /** * Opens the feedback window. */ @@ -120,7 +142,7 @@ var Feedback = { var states = { overall_feedback: { html: constructOverallFeedbackHtml(), - persistent: true, + persistent: false, buttons: {}, closeText: '', focus: "div[id='stars']", @@ -161,7 +183,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 95e4870eb..1685f5dfb 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) { @@ -325,8 +325,16 @@ function registerListeners() { "dialog.connectError", pres); }); APP.xmpp.addListener(XMPPEvents.ROOM_CONNECT_ERROR, function (pres) { - UI.messageHandler.openReportDialog(null, - "dialog.connectError", pres); + if (config.token && + $(pres).find( + '>error[type="cancel"]' + + '>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]' + ).length) { + messageHandler.showError("dialog.error", "dialog.tokenAuthFailed"); + } else { + UI.messageHandler.openReportDialog(null, + "dialog.connectError", pres); + } }); APP.xmpp.addListener(XMPPEvents.READY_TO_JOIN, function () { @@ -343,6 +351,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); @@ -425,15 +437,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"); @@ -524,6 +533,8 @@ function onMucJoined(jid, info) { VideoLayout.mucJoined(); + + Toolbar.checkAutoEnableDesktopSharing(); } function initEtherpad(name) { @@ -697,6 +708,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; @@ -884,11 +904,11 @@ UI.setVideoMuteButtonsState = function (mute) { } }; -UI.userAvatarChanged = function (resourceJid, thumbUrl, contactListUrl) { - VideoLayout.userAvatarChanged(resourceJid, thumbUrl); - ContactList.userAvatarChanged(resourceJid, contactListUrl); +UI.userAvatarChanged = function (resourceJid, avatarUrl) { + VideoLayout.userAvatarChanged(resourceJid, avatarUrl); + ContactList.userAvatarChanged(resourceJid, avatarUrl); if(resourceJid === APP.xmpp.myResource()) - SettingsMenu.changeAvatar(thumbUrl); + SettingsMenu.changeAvatar(avatarUrl); }; UI.setVideoMute = setVideoMute; diff --git a/modules/UI/avatar/Avatar.js b/modules/UI/avatar/Avatar.js index 6ab1327e4..d785bffdd 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, interfaceConfig */ var Settings = require("../../settings/Settings"); var users = {}; @@ -18,51 +18,51 @@ var Avatar = { } users[jid] = id; } - var thumbUrl = this.getThumbUrl(jid); - var contactListUrl = this.getContactListUrl(jid); + var avatarUrl = this.getAvatarUrl(jid); var resourceJid = Strophe.getResourceFromJid(jid); - APP.UI.userAvatarChanged(resourceJid, thumbUrl, contactListUrl); + APP.UI.userAvatarChanged(resourceJid, avatarUrl); }, /** - * Returns image URL for the avatar to be displayed on large video area - * where current active speaker is presented. + * Returns the URL of the image for the avatar of a particular user, + * identified by its jid + * @param jid * @param jid full MUC jid of the user for whom we want to obtain avatar URL */ - getActiveSpeakerUrl: function (jid) { - return this.getGravatarUrl(jid, 100); - }, - /** - * Returns image URL for the avatar to be displayed on small video thumbnail - * @param jid full MUC jid of the user for whom we want to obtain avatar URL - */ - getThumbUrl: function (jid) { - return this.getGravatarUrl(jid, 100); - }, - /** - * Returns the URL for the avatar to be displayed as contactlist item - * @param jid full MUC jid of the user for whom we want to obtain avatar URL - */ - getContactListUrl: function (jid) { - return this.getGravatarUrl(jid, 30); - }, - getGravatarUrl: function (jid, size) { - if (!jid) { - console.error("Get gravatar - jid is undefined"); - return null; - } - var id = users[jid]; - if (!id) { - console.warn( - "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"); - } + getAvatarUrl: function (jid) { + if (config.disableThirdPartyRequests) { + return 'images/avatar2.png'; + } else { + if (!jid) { + console.error("Get avatar - jid is undefined"); + return null; + } + var id = users[jid]; + // If the ID looks like an email, we'll use gravatar. + // Otherwise, it's a random avatar, and we'll use the configured + // URL. + var random = !id || id.indexOf('@') < 0; + + if (!id) { + console.warn( + "No avatar stored yet for " + jid + " - using JID as ID"); + id = jid; + } + id = MD5.hexdigest(id.trim().toLowerCase()); + + // Default to using gravatar. + var urlPref = 'https://www.gravatar.com/avatar/'; + var urlSuf = "?d=wavatar&size=100"; + + if (random && interfaceConfig.RANDOM_AVATAR_URL_PREFIX) { + urlPref = interfaceConfig.RANDOM_AVATAR_URL_PREFIX; + urlSuf = interfaceConfig.RANDOM_AVATAR_URL_SUFFIX; + } + + return urlPref + id + urlSuf; + } + } }; - -module.exports = Avatar; \ No newline at end of file +module.exports = Avatar; diff --git a/modules/UI/side_pannels/contactlist/ContactList.js b/modules/UI/side_pannels/contactlist/ContactList.js index 4e3c29466..4fdbb2795 100644 --- a/modules/UI/side_pannels/contactlist/ContactList.js +++ b/modules/UI/side_pannels/contactlist/ContactList.js @@ -32,7 +32,7 @@ function updateNumberOfParticipants(delta) { function createAvatar(jid) { var avatar = document.createElement('img'); avatar.className = "icon-avatar avatar"; - avatar.src = Avatar.getContactListUrl(jid); + avatar.src = Avatar.getAvatarUrl(jid); return avatar; } @@ -181,11 +181,11 @@ var ContactList = { contactName.html(displayName); }, - userAvatarChanged: function (resourceJid, contactListUrl) { + userAvatarChanged: function (resourceJid, avatarUrl) { // set the avatar in the contact list var contact = $('#' + resourceJid + '>img'); if (contact && contact.length > 0) { - contact.get(0).src = contactListUrl; + contact.get(0).src = avatarUrl; } } 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/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 9f229ddfd..b60d87b74 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); } /** @@ -380,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"}); } }; @@ -632,13 +648,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) { diff --git a/modules/UI/videolayout/ConnectionIndicator.js b/modules/UI/videolayout/ConnectionIndicator.js index 7923d4135..4ee8ae5de 100644 --- a/modules/UI/videolayout/ConnectionIndicator.js +++ b/modules/UI/videolayout/ConnectionIndicator.js @@ -90,6 +90,11 @@ ConnectionIndicator.prototype.generateText = function () { if(this.resolution && this.jid) { var keys = Object.keys(this.resolution); for(var ssrc in this.resolution) { + // skip resolutions for ssrc that don't have this info + // like receive-only ssrc for FF + if(this.resolution[ssrc] + && this.resolution[ssrc].height != -1 + && this.resolution[ssrc].width != -1) resolutionValue = this.resolution[ssrc]; } } diff --git a/modules/UI/videolayout/LargeVideo.js b/modules/UI/videolayout/LargeVideo.js index 20485c81a..89243ee66 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) { @@ -239,7 +242,7 @@ function getCameraVideoSize(videoWidth, function updateActiveSpeakerAvatarSrc() { var avatar = $("#activeSpeakerAvatar")[0]; var jid = currentSmallVideo.peerJid; - var url = Avatar.getActiveSpeakerUrl(jid); + var url = Avatar.getAvatarUrl(jid); if (avatar.src === url) return; if (jid) { @@ -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/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/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 3d5e7acd2..5352b22ee 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 $([]); } }; @@ -352,7 +369,7 @@ SmallVideo.prototype.showAvatar = function (show) { if (!this.hasAvatar) { if (this.peerJid) { // Init avatar - this.avatarChanged(Avatar.getThumbUrl(this.peerJid)); + this.avatarChanged(Avatar.getAvatarUrl(this.peerJid)); } else { console.error("Unable to init avatar - no peerjid", this); return; @@ -406,4 +423,4 @@ SmallVideo.prototype.avatarChanged = function (thumbUrl) { } }; -module.exports = SmallVideo; \ No newline at end of file +module.exports = SmallVideo; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 10aefd39c..d98a18464 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 @@ -214,7 +223,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 +410,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 +436,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 +451,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 +470,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); } @@ -598,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 { @@ -683,6 +697,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 && @@ -882,6 +906,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 +949,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; @@ -964,14 +1005,14 @@ var VideoLayout = (function (my) { } }; - my.userAvatarChanged = function(resourceJid, thumbUrl) { + my.userAvatarChanged = function(resourceJid, avatarUrl) { var smallVideo = VideoLayout.getSmallVideo(resourceJid); if(smallVideo) - smallVideo.avatarChanged(thumbUrl); + smallVideo.avatarChanged(avatarUrl); else console.warn( "Missed avatar update - no small video yet for " + resourceJid); - LargeVideo.updateAvatar(resourceJid, thumbUrl); + LargeVideo.updateAvatar(resourceJid, avatarUrl); }; my.createEtherpadIframe = function(src, onloadHandler) @@ -998,7 +1039,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/modules/statistics/CallStats.js b/modules/statistics/CallStats.js index e91932832..73327f0aa 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,12 +59,24 @@ 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; } }, + /** + * 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; @@ -108,6 +135,26 @@ 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); + } else if (callStatsIntegrationEnabled) { + pendingErrors.push({ + type: type, + error: e, + pc: pc + }); + } + // else just ignore it + }, /** * Notifies CallStats that getUserMedia failed. @@ -115,78 +162,57 @@ var CallStats = { * @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..00bf4c0ca 100644 --- a/modules/statistics/statistics.js +++ b/modules/statistics/statistics.js @@ -113,25 +113,46 @@ 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); + } + ); + }, + /** + * 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/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 = [ diff --git a/modules/xmpp/JingleSessionPC.js b/modules/xmpp/JingleSessionPC.js index 9f167d9af..30c833086 100644 --- a/modules/xmpp/JingleSessionPC.js +++ b/modules/xmpp/JingleSessionPC.js @@ -40,7 +40,11 @@ function JingleSessionPC(me, sid, connection, service, eventEmitter) { this.switchstreams = false; this.wait = true; - this.localStreamsSSRC = null; + /** + * 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; @@ -256,8 +260,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 +487,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); @@ -555,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) { @@ -720,7 +731,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); } }); }); @@ -1427,13 +1438,22 @@ JingleSessionPC.prototype.setLocalDescription = function () { 'ssrc': ssrc.id, 'type': media.type }); - }); - } - else if(self.localStreamsSSRC && self.localStreamsSSRC[media.type]) - { - newssrcs.push({ - 'ssrc': self.localStreamsSSRC[media.type], - 'type': media.type + + // In FF we have multiple local SSRC per media type, 1 that is + // sending and some that are receive only. The + // localStramsSSRC['audio'] needs to be set to the one that is + // sending! We find it by checking for an msid. Note that + // self.localStreamsSSRC is primarily used by the tests atm. + var isSending = media.ssrcs.some(function (ssrc$1) { + return ssrc$1.id == ssrc.id && ssrc$1.attribute == 'msid'; + }); + + if (!isSending) { + return; + } + + // FIXME allows for only one SSRC per media type + self.localStreamsSSRC[media.type] = ssrc.id; }); } diff --git a/modules/xmpp/LocalSSRCReplacement.js b/modules/xmpp/LocalSSRCReplacement.js index 0b8e65cff..4bf109c1b 100644 --- a/modules/xmpp/LocalSSRCReplacement.js +++ b/modules/xmpp/LocalSSRCReplacement.js @@ -130,7 +130,9 @@ var storeLocalVideoSSRC = function (jingleIq) { function generateRecvonlySSRC() { // localRecvOnlySSRC = - Math.random().toString(10).substring(2, 11); + localVideoSSRC ? + localVideoSSRC : Math.random().toString(10).substring(2, 11); + localRecvOnlyCName = Math.random().toString(36).substring(2); console.info( @@ -175,44 +177,45 @@ var LocalSSRCReplacement = { // IF we have local video SSRC stored make sure it is replaced // with old SSRC - if (localVideoSSRC) { - var newSdp = new SDP(localDescription.sdp); - if (newSdp.media[1].indexOf("a=ssrc:") !== -1 && - !newSdp.containsSSRC(localVideoSSRC)) { - // Get new video SSRC - var map = newSdp.getMediaSsrcMap(); - var videoPart = map[1]; - var videoSSRCs = videoPart.ssrcs; - var newSSRC = Object.keys(videoSSRCs)[0]; + var sdp = new SDP(localDescription.sdp); + if (sdp.media.length < 2) + return; - console.info( - "Replacing new video SSRC: " + newSSRC + - " with " + localVideoSSRC); + if (localVideoSSRC && sdp.media[1].indexOf("a=ssrc:") !== -1 && + !sdp.containsSSRC(localVideoSSRC)) { - localDescription.sdp = - newSdp.raw.replace( - new RegExp('a=ssrc:' + newSSRC, 'g'), - 'a=ssrc:' + localVideoSSRC); - } - } else { + console.info("Does not contain: " + + localVideoSSRC + "---" + sdp.media[1]); + + // Get new video SSRC + var map = sdp.getMediaSsrcMap(); + var videoPart = map[1]; + var videoSSRCs = videoPart.ssrcs; + var newSSRC = Object.keys(videoSSRCs)[0]; + + console.info( + "Replacing new video SSRC: " + newSSRC + + " with " + localVideoSSRC); + + localDescription.sdp = + sdp.raw.replace( + new RegExp('a=ssrc:' + newSSRC, 'g'), + 'a=ssrc:' + localVideoSSRC); + } + else if (sdp.media[1].indexOf('a=ssrc:') === -1 && + sdp.media[1].indexOf('a=recvonly') !== -1) { // Make sure we have any SSRC for recvonly video stream - var sdp = new SDP(localDescription.sdp); - - if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 && - sdp.media[1].indexOf('a=recvonly') !== -1) { - - if (!localRecvOnlySSRC) { - generateRecvonlySSRC(); - } - - console.info('No SSRC in video recvonly stream' + - ' - adding SSRC: ' + localRecvOnlySSRC); - - sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC + - ' cname:' + localRecvOnlyCName + '\r\n'; - - localDescription.sdp = sdp.session + sdp.media.join(''); + if (!localRecvOnlySSRC) { + generateRecvonlySSRC(); } + + console.info('No SSRC in video recvonly stream' + + ' - adding SSRC: ' + localRecvOnlySSRC); + + sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC + + ' cname:' + localRecvOnlyCName + '\r\n'; + + localDescription.sdp = sdp.session + sdp.media.join(''); } return localDescription; }, diff --git a/modules/xmpp/SDP.js b/modules/xmpp/SDP.js index e799e5520..1fcf59d7a 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) { @@ -74,16 +75,18 @@ SDP.prototype.getMediaSsrcMap = function() { * @param ssrc the ssrc to check. * @returns {boolean} true if this SDP contains given SSRC. */ -SDP.prototype.containsSSRC = function(ssrc) { +SDP.prototype.containsSSRC = function (ssrc) { + // FIXME this code is really strange - improve it if you can var medias = this.getMediaSsrcMap(); - Object.keys(medias).forEach(function(mediaindex){ - var media = medias[mediaindex]; - //console.log("Check", channel, ssrc); - if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){ - return true; + var result = false; + Object.keys(medias).forEach(function (mediaindex) { + if (result) + return; + if (medias[mediaindex].ssrcs[ssrc]) { + result = true; } }); - return false; + return result; }; // remove iSAC and CN from SDP @@ -138,7 +141,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 @@ -165,11 +168,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}); 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/modules/xmpp/TraceablePeerConnection.js b/modules/xmpp/TraceablePeerConnection.js index 4e05a2811..51dd22e89 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)); @@ -293,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); } ); @@ -329,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); } ); @@ -366,6 +368,7 @@ TraceablePeerConnection.prototype.createOffer } offer = SSRCReplacement.mungeLocalVideoSSRC(offer); + self.trace('createOfferOnSuccess::mungeLocalVideoSSRC', dumpSDP(offer)); if (config.enableSimulcast && self.simulcast.isSupported()) { offer = self.simulcast.mungeLocalDescription(offer); @@ -375,7 +378,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 @@ -397,6 +400,7 @@ TraceablePeerConnection.prototype.createAnswer // munge local video SSRC answer = SSRCReplacement.mungeLocalVideoSSRC(answer); + self.trace('createAnswerOnSuccess::mungeLocalVideoSSRC', dumpSDP(answer)); if (config.enableSimulcast && self.simulcast.isSupported()) { answer = self.simulcast.mungeLocalDescription(answer); @@ -406,7 +410,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 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( diff --git a/modules/xmpp/strophe.emuc.js b/modules/xmpp/strophe.emuc.js index ef6be3585..2b83c049b 100644 --- a/modules/xmpp/strophe.emuc.js +++ b/modules/xmpp/strophe.emuc.js @@ -562,10 +562,6 @@ module.exports = function(XMPP, eventEmitter) { delete this.presMap["startMuted"]; } - if (config.token) { - pres.c('token', { xmlns: 'http://jitsi.org/jitmeet/auth-token'}).t(config.token).up(); - } - pres.up(); this.connection.send(pres); }, diff --git a/modules/xmpp/xmpp.js b/modules/xmpp/xmpp.js index 9b28cb49f..0edc1c41b 100644 --- a/modules/xmpp/xmpp.js +++ b/modules/xmpp/xmpp.js @@ -308,21 +308,16 @@ var XMPP = { configDomain = config.hosts.domain; } var jid = configDomain || window.location.hostname; - var password = null; - if (config.token) { - password = config.token; - if (config.id) { - jid = config.id + "@" + jid; - } else { - jid = generateUserName() + "@" + jid; - } - } - connect(jid, password); + connect(jid); }, 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()); + bosh += '?room=' + APP.UI.getRoomNode(); + if (config.token) { + bosh += "&token=" + config.token; + } + return new Strophe.Connection(bosh); }, getStatusString: function (status) { return Strophe.getStatusString(status); @@ -596,6 +591,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; diff --git a/package.json b/package.json index ed4b2eab3..c133f3c07 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-simulcast": "0.1.0", - "sdp-transform": "1.4.1", + "sdp-interop": "0.1.11", + "sdp-simulcast": "0.1.3", + "sdp-transform": "1.5.*", "socket.io-client": "1.3.6", "strophe": "^1.2.2", "strophejs-plugins": "^0.0.6", diff --git a/prosody-plugins/mod_auth_token.lua b/prosody-plugins/mod_auth_token.lua index bf2df1920..1b24e94c2 100644 --- a/prosody-plugins/mod_auth_token.lua +++ b/prosody-plugins/mod_auth_token.lua @@ -1,42 +1,46 @@ -- Token authentication -- Copyright (C) 2015 Atlassian -local usermanager = require "core.usermanager"; +local generate_uuid = require "util.uuid".generate; local new_sasl = require "util.sasl".new; - -local log = module._log; -local host = module.host; - +local sasl = require "util.sasl"; +local formdecode = require "util.http".formdecode; local token_util = module:require "token/util"; -- define auth provider local provider = {}; ---do --- local list; --- for mechanism in pairs(new_sasl(module.host):mechanisms()) do --- list = (not(list) and mechanism) or (list..", "..mechanism); --- end --- if not list then --- module:log("error", "No mechanisms"); --- else --- module:log("error", "Mechanisms: %s", list); --- end ---end - +local host = module.host; local appId = module:get_option_string("app_id"); local appSecret = module:get_option_string("app_secret"); -local tokenLifetime = module:get_option_number("token_lifetime"); +local allowEmptyToken = module:get_option_boolean("allow_empty_token"); + +if allowEmptyToken == true then + module:log("warn", "WARNING - empty tokens allowed"); +end + +if appId == nil then + module:log("error", "'app_id' must not be empty"); + return; +end + +if appSecret == nil then + module:log("error", "'app_secret' must not be empty"); + return; +end + +-- Extract 'token' param from BOSH URL when session is created +module:hook("bosh-session", function(event) + local session, request = event.session, event.request; + local query = request.url.query; + if query ~= nil then + session.auth_token = query and formdecode(query).token or nil; + end +end) function provider.test_password(username, password) - local result, msg = token_util.verify_password(password, appId, appSecret, tokenLifetime); - if result == true then - return true; - else - log("error", "Token auth failed for user %s, reason: %s",username, msg); - return nil, msg; - end + return nil, "Password based auth not supported"; end function provider.get_password(username) @@ -51,10 +55,6 @@ function provider.user_exists(username) return nil; end -function provider.users() - return next, hosts[module.host].sessions, nil; -end - function provider.create_user(username, password) return nil; end @@ -63,13 +63,59 @@ function provider.delete_user(username) return nil; end -function provider.get_sasl_handler() - local testpass_authentication_profile = { - plain_test = function(sasl, username, password, realm) - return usermanager.test_password(username, realm, password), true; +function provider.get_sasl_handler(session) + -- JWT token extracted from BOSH URL + local token = session.auth_token; + + local function get_username_from_token(self, message) + + if token == nil then + if allowEmptyToken == true then + return true; + else + return false, "not-allowed", "token required"; + end end - }; - return new_sasl(host, testpass_authentication_profile); + + -- here we check if 'room' claim exists + local room, roomErr = token_util.get_room_name(token, appSecret); + if room == nil then + return false, "not-allowed", roomErr; + end + + -- now verify the whole token + local result, msg + = token_util.verify_token(token, appId, appSecret, room); + if result == true then + -- Binds room name to the session which is later checked on MUC join + session.jitsi_meet_room = room; + return true + else + return false, "not-allowed", msg + end + end + + return new_sasl(host, { anonymous = get_username_from_token }); end module:provides("auth", provider); + +local function anonymous(self, message) + + local username = generate_uuid(); + + -- This calls the handler created in 'provider.get_sasl_handler(session)' + local result, err, msg = self.profile.anonymous(self, username, self.realm); + + self.username = username; + + if result == true then + return "success" + else + + return "failure", err, msg + end +end + +sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous); + diff --git a/prosody-plugins/mod_bosh.lua.patch b/prosody-plugins/mod_bosh.lua.patch new file mode 100644 index 000000000..928261661 --- /dev/null +++ b/prosody-plugins/mod_bosh.lua.patch @@ -0,0 +1,12 @@ +--- /usr/lib/prosody/modules/mod_bosh.lua 2015-12-16 14:28:34.000000000 -0600 ++++ /usr/lib/prosody/modules/mod_bosh.lua 2015-12-22 10:45:59.818197967 -0600 +@@ -294,6 +294,9 @@ + + session.log("debug", "BOSH session created for request from %s", session.ip); + log("info", "New BOSH session, assigned it sid '%s'", sid); ++ ++ hosts[session.host].events.fire_event( ++ "bosh-session", { session = session, request = request }); + + -- Send creation response + local creating_session = true; diff --git a/prosody-plugins/mod_token_verification.lua b/prosody-plugins/mod_token_verification.lua index 08e1e53ec..597664ebd 100644 --- a/prosody-plugins/mod_token_verification.lua +++ b/prosody-plugins/mod_token_verification.lua @@ -4,7 +4,6 @@ local log = module._log; local host = module.host; local st = require "util.stanza"; -local token_util = module:require("token/util"); local is_admin = require "core.usermanager".is_admin; @@ -16,37 +15,70 @@ end local parentCtx = module:context(parentHostName); if parentCtx == nil then - log("error", "Failed to start - unable to get parent context for host: %s", tostring(parentHostName)); + log("error", + "Failed to start - unable to get parent context for host: %s", + tostring(parentHostName)); return; 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"); +local allowEmptyToken = parentCtx:get_option_boolean("allow_empty_token"); -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 allow empty: %s", + tostring(host), tostring(appId), tostring(appSecret), + tostring(allowEmptyToken)); + +local function verify_user(session, stanza) + log("debug", "Session token: %s, session room: %s", + tostring(session.auth_token), + tostring(session.jitsi_meet_room)); + + if allowEmptyToken and session.auth_token == nil then + module:log( + "debug", + "Skipped room token verification - empty tokens are allowed"); + return nil; + end -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; + 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); - if token ~= nil then - token = token[1]; + + 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 - local result, msg = token_util.verify_password(token, appId, appSecret, tokenLifetime); - if result ~= true then - log("debug", "Token verification failed: %s", msg); - origin.send(st.error_reply(stanza, "cancel", "not-allowed", msg)); + + local token = session.auth_token; + local auth_room = session.jitsi_meet_room; + if room ~= auth_room then + log("error", "Token %s not allowed to join: %s", + tostring(token), tostring(auth_room)); + session.send( + st.error_reply( + stanza, "cancel", "not-allowed", "Room and token mismatched")); return true; end + log("debug", "allowed: %s to enter/create room: %s", user_jid, room); end -module:hook("muc-room-pre-create", handle_pre_create); +module:hook("muc-room-pre-create", function(event) + local origin, stanza = event.origin, event.stanza; + log("debug", "pre create: %s %s", tostring(origin), tostring(stanza)); + return verify_user(origin, stanza); +end); + +module:hook("muc-occupant-pre-join", function(event) + local origin, room, stanza = event.origin, event.room, event.stanza; + log("debug", "pre join: %s %s", tostring(room), tostring(stanza)); + return verify_user(origin, stanza); +end); diff --git a/prosody-plugins/token/util.lib.lua b/prosody-plugins/token/util.lib.lua index 64747e03a..c281c3d95 100644 --- a/prosody-plugins/token/util.lib.lua +++ b/prosody-plugins/token/util.lib.lua @@ -1,76 +1,51 @@ -- 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; +local function _get_room_name(token, appSecret) + local claims, err = jwt.decode(token, appSecret); + if claims ~= nil then + return claims["room"]; else - log("error", "Invalid password format: '%s'", password); - return nil; + return nil, err; end end -local function extract_hash(password) - local hash, room, ts = string.match(password, "(%w+)_(%w+)_(%d+)"); - return hash; -end +local function _verify_token(token, appId, appSecret, roomName) -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(token, 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_token(token, appId, appSecret, roomName) + return _verify_token(token, appId, appSecret, roomName); end -return _M; +function _M.get_room_name(token, appSecret) + return _get_room_name(token, appSecret); +end + +return _M; \ No newline at end of file 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 diff --git a/unsupported_browser.html b/unsupported_browser.html index 68981fbab..94728c2ca 100644 --- a/unsupported_browser.html +++ b/unsupported_browser.html @@ -12,33 +12,50 @@
- 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
+
+