From bf9c4ea444fb8c6deca51121bb3a48a26762dfb5 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Mon, 28 Mar 2016 16:19:32 -0500 Subject: [PATCH 01/31] Implements server side connection establishment --- Makefile | 5 +- app.js | 48 +++++------ connection.js | 38 ++++++++- .../connection_optimization.html | 0 .../do_external_connect.js | 79 +++++++++++++++++++ index.html | 14 ++-- modules/config/URLProcessor.js | 16 +--- utils.js | 52 ++++++++++++ 8 files changed, 201 insertions(+), 51 deletions(-) create mode 100644 connection_optimization/connection_optimization.html create mode 100644 connection_optimization/do_external_connect.js create mode 100644 utils.js diff --git a/Makefile b/Makefile index 12624e6e0..afd026632 100644 --- a/Makefile +++ b/Makefile @@ -31,8 +31,9 @@ deploy-appbundle: deploy-lib-jitsi-meet: cp $(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.js \ - $(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map $(DEPLOY_DIR) - + $(LIBJITSIMEET_DIR)/lib-jitsi-meet.min.map \ + $(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \ + $(DEPLOY_DIR) deploy-css: (cd css; cat $(CSS_FILES)) | $(CLEANCSS) > css/all.css diff --git a/app.js b/app.js index c64cfeff9..cee8118e8 100644 --- a/app.js +++ b/app.js @@ -1,4 +1,4 @@ -/* global $, JitsiMeetJS, config */ +/* global $, JitsiMeetJS, config, getRoomName */ /* application specific logic */ import "babel-polyfill"; @@ -23,40 +23,34 @@ import API from './modules/API/API'; import UIEvents from './service/UI/UIEvents'; - +/** + * Builds and returns the room name. + */ function buildRoomName () { - let path = window.location.pathname; - let roomName; + let roomName = getRoomName(); - // determinde the room node from the url - // TODO: just the roomnode or the whole bare jid? - if (config.getroomnode && typeof config.getroomnode === 'function') { - // custom function might be responsible for doing the pushstate - roomName = config.getroomnode(path); - } else { - /* fall back to default strategy - * this is making assumptions about how the URL->room mapping happens. - * It currently assumes deployment at root, with a rewrite like the - * following one (for nginx): - location ~ ^/([a-zA-Z0-9]+)$ { - rewrite ^/(.*)$ / break; - } - */ - if (path.length > 1) { - roomName = path.substr(1).toLowerCase(); - } else { - let word = RoomnameGenerator.generateRoomWithoutSeparator(); - roomName = word.toLowerCase(); - window.history.pushState( - 'VideoChat', `Room: ${word}`, window.location.pathname + word - ); - } + if(!roomName) { + let word = RoomnameGenerator.generateRoomWithoutSeparator(); + roomName = word.toLowerCase(); + window.history.pushState( + 'VideoChat', `Room: ${word}`, window.location.pathname + word + ); } return roomName; } const APP = { + // Used by do_external_connect.js if we receive the attach data after + // connect was already executed. status property can be "initialized", + // "ready" or "connecting". We are interested in "ready" status only which + // means that connect was executed but we have to wait for the attach data. + // In status "ready" handler property will be set to a function that will + // finish the connect process when the attach data or error is received. + connect: { + status: "initialized", + handler: null + }, UI, settings, conference, diff --git a/connection.js b/connection.js index b6348a774..6e7dec393 100644 --- a/connection.js +++ b/connection.js @@ -5,6 +5,39 @@ import LoginDialog from './modules/UI/authentication/LoginDialog'; const ConnectionEvents = JitsiMeetJS.events.connection; const ConnectionErrors = JitsiMeetJS.errors.connection; +/** + * Checks if we have data to use attach instead of connect. If we have the data + * executes attach otherwise check if we have to wait for the data. If we have + * to wait for the attach data we are setting handler to APP.connect.handler + * which is going to be called when the attach data is received otherwise + * executes connect. + * + * @param {string} [id] user id + * @param {string} [password] password + * @param {string} [roomName] the name of the conference. + */ +function checkForAttachParametersAndConnect(id, password, connection) { + console.log("call checkForAttachParametersAndConnect"); + if(window.XMPPAttachInfo){ + APP.connect.status = "connecting"; + if(window.XMPPAttachInfo.status === "error") { + connection.connect({id, password}); + return; + } + + var attachOptions = window.XMPPAttachInfo.data; + if(attachOptions) { + connection.attach(attachOptions); + } else { + connection.connect({id, password}); + } + } else { + APP.connect.status = "ready"; + APP.connect.handler = checkForAttachParametersAndConnect.bind(null, + id, password, connection); + } +} + /** * Try to open connection using provided credentials. * @param {string} [id] @@ -18,7 +51,8 @@ function connect(id, password, roomName) { let connectionConfig = config; connectionConfig.bosh += '?room=' + roomName; - let connection = new JitsiMeetJS.JitsiConnection(null, null, config); + let connection + = new JitsiMeetJS.JitsiConnection(null, config.token, config); return new Promise(function (resolve, reject) { connection.addEventListener( @@ -50,7 +84,7 @@ function connect(id, password, roomName) { reject(err); } - connection.connect({id, password}); + checkForAttachParametersAndConnect(id, password, connection); }); } diff --git a/connection_optimization/connection_optimization.html b/connection_optimization/connection_optimization.html new file mode 100644 index 000000000..e69de29bb diff --git a/connection_optimization/do_external_connect.js b/connection_optimization/do_external_connect.js new file mode 100644 index 000000000..99c277f4f --- /dev/null +++ b/connection_optimization/do_external_connect.js @@ -0,0 +1,79 @@ +/* global config, getRoomName, getConfigParamsFromUrl */ +/* global createConnectionExternally */ +/** + * Implements extrnal connect using createConnectionExtenally function defined + * in external_connect.js for Jitsi Meet. Parses the room name and token from + * the url and executes createConnectionExtenally. + * + * NOTE: If you are using lib-jitsi-meet without Jitsi Meet you should use this + * file as reference only because the implementation is Jitsi Meet specific. + * + * NOTE: For optimal results this file should be included right after + * exrnal_connect.js. + */ + + + + /** + * Gets the token from the URL. + */ +function buildToken(){ + var params = getConfigParamsFromUrl(); + return params["config.token"] || config.token; +} + +/** + * Executes createConnectionExternally function. + */ +(function () { + // FIXME: Add implementation for changing that config from the url for + // consistency + var url = config.externalConnectUrl; + + /** + * Check if connect from connection.js was executed and executes the handler + * that is going to finish the connect work. + */ + function checkForConnectHandlerAndConnect() { + + if(window.APP && window.APP.connect.status === "ready") { + window.APP.connect.handler(); + } + } + + function error_callback(error){ + console.warn(error); + // Sets that global variable to be used later by connect method in + // connection.js + window.XMPPAttachInfo = { + status: "error" + }; + checkForConnectHandlerAndConnect(); + } + + if(!url || !window.createConnectionExternally) { + error_callback(); + return; + } + var room_name = getRoomName(); + if(!room_name) { + error_callback(); + return; + } + + url += "?room=" + room_name; + + var token = buildToken(); + if(token) + url += "&token=" + token; + + createConnectionExternally(url, function(connectionInfo) { + // Sets that global variable to be used later by connect method in + // connection.js + window.XMPPAttachInfo = { + status: "success", + data: connectionInfo + }; + checkForConnectHandlerAndConnect(); + }, error_callback); +})(); diff --git a/index.html b/index.html index b77017611..e52d422c1 100644 --- a/index.html +++ b/index.html @@ -1,6 +1,13 @@ + + + + + + + @@ -9,12 +16,7 @@ - - - - - - + diff --git a/modules/config/URLProcessor.js b/modules/config/URLProcessor.js index ceb40b8a0..8f1021dfe 100644 --- a/modules/config/URLProcessor.js +++ b/modules/config/URLProcessor.js @@ -1,18 +1,6 @@ -/* global $, $iq, config, interfaceConfig */ +/* global $, $iq, config, interfaceConfig, getConfigParamsFromUrl */ var configUtils = require('./Util'); var params = {}; -function getConfigParamsFromUrl() { - if (!location.hash) - return {}; - var hash = location.hash.substr(1); - var result = {}; - hash.split("&").forEach(function (part) { - var item = part.split("="); - result[item[0]] = JSON.parse( - decodeURIComponent(item[1]).replace(/\\&/, "&")); - }); - return result; -} params = getConfigParamsFromUrl(); @@ -62,4 +50,4 @@ var URLProcessor = { } }; -module.exports = URLProcessor; \ No newline at end of file +module.exports = URLProcessor; diff --git a/utils.js b/utils.js new file mode 100644 index 000000000..ebc11984d --- /dev/null +++ b/utils.js @@ -0,0 +1,52 @@ +/* global config */ + +/** + * Defines some utility methods that are used before the other JS files are + * loaded. + */ + + +/** + * Builds and returns the room name. + */ +function getRoomName () { + var path = window.location.pathname; + var roomName; + + // determinde the room node from the url + // TODO: just the roomnode or the whole bare jid? + if (config.getroomnode && typeof config.getroomnode === 'function') { + // custom function might be responsible for doing the pushstate + roomName = config.getroomnode(path); + } else { + /* fall back to default strategy + * this is making assumptions about how the URL->room mapping happens. + * It currently assumes deployment at root, with a rewrite like the + * following one (for nginx): + location ~ ^/([a-zA-Z0-9]+)$ { + rewrite ^/(.*)$ / break; + } + */ + if (path.length > 1) { + roomName = path.substr(1).toLowerCase(); + } + } + + return roomName; +} + +/** + * Parses the hash parameters from the URL and returns them as a JS object. + */ +function getConfigParamsFromUrl() { + if (!location.hash) + return {}; + var hash = location.hash.substr(1); + var result = {}; + hash.split("&").forEach(function (part) { + var item = part.split("="); + result[item[0]] = JSON.parse( + decodeURIComponent(item[1]).replace(/\\&/, "&")); + }); + return result; +} From 4a1175d44fc0e3f2820fb67b44dec0338184446c Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Mon, 28 Mar 2016 17:42:20 -0500 Subject: [PATCH 02/31] Fixes comments after review --- connection.js | 5 ++++- debian/jitsi-meet.install | 1 + index.html | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/connection.js b/connection.js index 6e7dec393..aa0ca0f7c 100644 --- a/connection.js +++ b/connection.js @@ -17,9 +17,12 @@ const ConnectionErrors = JitsiMeetJS.errors.connection; * @param {string} [roomName] the name of the conference. */ function checkForAttachParametersAndConnect(id, password, connection) { - console.log("call checkForAttachParametersAndConnect"); if(window.XMPPAttachInfo){ APP.connect.status = "connecting"; + // When connection optimization is not deployed or enabled the default + // value will be window.XMPPAttachInfo.status = "error" + // If the connection optimization is deployed and enabled and there is + // a failure the value will be window.XMPPAttachInfo.status = "error" if(window.XMPPAttachInfo.status === "error") { connection.connect({id, password}); return; diff --git a/debian/jitsi-meet.install b/debian/jitsi-meet.install index c5ac146e5..e48f4d7a8 100644 --- a/debian/jitsi-meet.install +++ b/debian/jitsi-meet.install @@ -9,3 +9,4 @@ sounds /usr/share/jitsi-meet/ fonts /usr/share/jitsi-meet/ images /usr/share/jitsi-meet/ lang /usr/share/jitsi-meet/ +connection_optimization /usr/share/jitsi-meet/ diff --git a/index.html b/index.html index e52d422c1..e3c9d4a38 100644 --- a/index.html +++ b/index.html @@ -5,6 +5,7 @@ + From 68994fbe7470bab3bdb3e3ec431f06cadd264f56 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 12:13:54 -0500 Subject: [PATCH 03/31] Recording related UI modifications. --- conference.js | 16 +++---- css/main.css | 17 +------- css/videolayout_default.css | 30 +++++++++++++ index.html | 3 +- lang/main.json | 27 +++++++++--- modules/UI/UI.js | 43 ++++--------------- modules/UI/toolbars/Toolbar.js | 77 ++-------------------------------- modules/UI/util/UIUtil.js | 6 +-- service/UI/UIEvents.js | 2 +- 9 files changed, 75 insertions(+), 146 deletions(-) diff --git a/conference.js b/conference.js index a59cfd429..450d0e950 100644 --- a/conference.js +++ b/conference.js @@ -558,7 +558,7 @@ export default { _getConferenceOptions() { let options = config; - if(config.enableRecording) { + if(config.enableRecording && !config.recordingType) { options.recordingType = (config.hosts && (typeof config.hosts.jirecon != "undefined"))? "jirecon" : "colibri"; @@ -839,7 +839,8 @@ export default { APP.UI.changeDisplayName(id, displayName); }); - room.on(ConferenceEvents.RECORDING_STATE_CHANGED, (status, error) => { + room.on(ConferenceEvents.RECORDER_STATE_CHANGED, (status, error) => { + console.log("Received recorder status change: ", status, error); if(status == "error") { console.error(error); return; @@ -999,15 +1000,8 @@ export default { // Starts or stops the recording for the conference. - APP.UI.addListener(UIEvents.RECORDING_TOGGLE, (predefinedToken) => { - if (predefinedToken) { - room.toggleRecording({token: predefinedToken}); - return; - } - APP.UI.requestRecordingToken().then((token) => { - room.toggleRecording({token: token}); - }); - + APP.UI.addListener(UIEvents.RECORDING_TOGGLED, (options) => { + room.toggleRecording(options); }); APP.UI.addListener(UIEvents.SUBJECT_CHANGED, (topic) => { diff --git a/css/main.css b/css/main.css index e3a442e74..a164f8516 100644 --- a/css/main.css +++ b/css/main.css @@ -129,21 +129,6 @@ html, body{ -moz-transition: all .5s ease-in-out; transition: all .5s ease-in-out; } -/*#ffde00*/ -#toolbar_button_record.active { - -webkit-text-shadow: -1px 0 10px #00ccff, - 0 1px 10px #00ccff, - 1px 0 10px #00ccff, - 0 -1px 10px #00ccff; - -moz-text-shadow: 1px 0 10px #00ccff, - 0 1px 10px #00ccff, - 1px 0 10px #00ccff, - 0 -1px 10px #00ccff; - text-shadow: -1px 0 10px #00ccff, - 0 1px 10px #00ccff, - 1px 0 10px #00ccff, - 0 -1px 10px #00ccff; -} a.button:hover, a.bottomToolbarButton:hover { @@ -298,7 +283,7 @@ div.feedbackButton:hover { } .active { - color: #00ccff; + background-color: #00ccff; } .bottomToolbar_span>span { diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 5b585738f..ed00174e4 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -488,4 +488,34 @@ padding: 10px; color: rgba(255,255,255,.5); z-index: 10000; +} + +.centeredVideoLabel { + display: none; + position: absolute; + bottom: 45%; + top: auto; + right: auto; + left: auto; + line-height: 28px; + height: 28px; + width: auto; + padding: 5px; + margin-right: auto; + margin-left: auto; + background: rgba(0,0,0,.5); + color: #FFF; + z-index: 10000; + border-radius: 4px; + -webkit-transition: all 2s 2s linear; + transition: all 2s 2s linear; +} + +.moveToCorner { + top: 5px; + right: 5px; + margin-right: 0px; + margin-left: auto; + background: rgba(0,0,0,.3); + color: rgba(255,255,255,.5); } \ No newline at end of file diff --git a/index.html b/index.html index b77017611..1e2608db7 100644 --- a/index.html +++ b/index.html @@ -116,7 +116,7 @@ - + @@ -154,6 +154,7 @@ HD +
diff --git a/lang/main.json b/lang/main.json index f03c1bbc3..99de636ae 100644 --- a/lang/main.json +++ b/lang/main.json @@ -51,7 +51,7 @@ "mute": "Mute / Unmute", "videomute": "Start / stop camera", "authenticate": "Authenticate", - "record": "Record", + "record": "Toggle recording", "lock": "Lock / unlock room", "invite": "Invite others", "chat": "Open / close chat", @@ -178,6 +178,7 @@ "joinAgain": "Join again", "Share": "Share", "Save": "Save", + "recording": "Recording", "recordingToken": "Enter recording token", "Dial": "Dial", "sipMsg": "Enter SIP number", @@ -205,7 +206,14 @@ "firefoxExtensionPrompt": "You need to install a Firefox extension in order to use screen sharing. Please try again after you get it from here!", "feedbackQuestion": "How was your call?", "thankYou": "Thank you for using __appName__!", - "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?" + "sorryFeedback": "We're sorry to hear that. Would you like to tell us more?", + "liveStreaming": "Live Streaming", + "streamKey": "Stream name/key", + "startLiveStreaming": "Start live streaming", + "stopStreamingWarning": "Are you sure you would like to stop the live streaming?", + "stopRecordingWarning": "Are you sure you would like to stop the recording?", + "stopLiveStreaming": "Stop live streaming", + "stopRecording": "Stop recording" }, "email": { @@ -253,8 +261,17 @@ }, "recording": { - "toaster": "Currently recording!", - "pending": "Your recording will start as soon as another participant joins", - "on": "Recording has been started" + "pending": "Recording waiting for a participant to join...", + "on": "Recording", + "off": "Recording stopped", + "failedToStart": "Recording failed to start" + }, + "liveStreaming": + { + "pending": "Starting Live Stream...", + "on": "Live Streaming", + "off": "Live Streaming Stopped", + "unavailable": "The live streaming service is currently unavailable. Please try again later.", + "failedToStart": "Live streaming failed to start" } } diff --git a/modules/UI/UI.js b/modules/UI/UI.js index a914e1d24..2542a41af 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -14,6 +14,7 @@ import UIEvents from "../../service/UI/UIEvents"; import CQEvents from '../../service/connectionquality/CQEvents'; import EtherpadManager from './etherpad/Etherpad'; import SharedVideoManager from './shared_video/SharedVideo'; +import Recording from "./recording/Recording"; import VideoLayout from "./videolayout/VideoLayout"; import FilmStrip from "./videolayout/FilmStrip"; @@ -359,12 +360,16 @@ UI.start = function () { bindEvents(); sharedVideoManager = new SharedVideoManager(eventEmitter); if (!interfaceConfig.filmStripOnly) { - $("#videospace").mousemove(function () { return ToolbarToggler.showToolbar(); }); setupToolbars(); setupChat(); + + // Initialise the recording module. + if (config.enableRecording) + Recording.init(eventEmitter, config.recordingType); + // Display notice message at the top of the toolbar if (config.noticeMessage) { $('#noticeText').text(config.noticeMessage); @@ -562,15 +567,14 @@ UI.updateLocalRole = function (isModerator) { VideoLayout.showModeratorIndicator(); Toolbar.showSipCallButton(isModerator); - Toolbar.showRecordingButton(isModerator); - Toolbar.showSharedVideoButton(isModerator); + Recording.showRecordingButton(isModerator); SettingsMenu.showStartMutedOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator); if (isModerator) { messageHandler.notify(null, "notify.me", 'connected', "notify.moderator"); - Toolbar.checkAutoRecord(); + Recording.checkAutoRecord(); } }; @@ -973,37 +977,8 @@ UI.requestFeedback = function () { }); }; -/** - * Request recording token from the user. - * @returns {Promise} - */ -UI.requestRecordingToken = function () { - let msg = APP.translation.generateTranslationHTML("dialog.recordingToken"); - let token = APP.translation.translateString("dialog.token"); - return new Promise(function (resolve, reject) { - messageHandler.openTwoButtonDialog( - null, null, null, - `

${msg}

- `, - false, "dialog.Save", - function (e, v, m, f) { - if (v && f.recordingToken) { - resolve(UIUtil.escapeHtml(f.recordingToken)); - } else { - reject(); - } - }, - null, - function () { }, - ':input:first' - ); - }); -}; - UI.updateRecordingState = function (state) { - Toolbar.updateRecordingState(state); + Recording.updateRecordingState(state); }; UI.notifyTokenAuthFailed = function () { diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 890b13bb2..a72cfef0b 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -6,7 +6,6 @@ import AnalyticsAdapter from '../../statistics/AnalyticsAdapter'; import UIEvents from '../../../service/UI/UIEvents'; let roomUrl = null; -let recordingToaster = null; let emitter = null; @@ -43,51 +42,6 @@ function openLinkDialog () { ); } -// Sets the state of the recording button -function setRecordingButtonState (recordingState) { - let selector = $('#toolbar_button_record'); - - if (recordingState === 'on') { - selector.removeClass("icon-recEnable"); - selector.addClass("icon-recEnable active"); - - $("#largeVideo").toggleClass("videoMessageFilter", true); - let recordOnKey = "recording.on"; - $('#videoConnectionMessage').attr("data-i18n", recordOnKey); - $('#videoConnectionMessage').text(APP.translation.translateString(recordOnKey)); - - setTimeout(function(){ - $("#largeVideo").toggleClass("videoMessageFilter", false); - $('#videoConnectionMessage').css({display: "none"}); - }, 1500); - - recordingToaster = messageHandler.notify( - null, "recording.toaster", null, - null, null, - {timeOut: 0, closeButton: null, tapToDismiss: false} - ); - } else if (recordingState === 'off') { - selector.removeClass("icon-recEnable active"); - selector.addClass("icon-recEnable"); - - $("#largeVideo").toggleClass("videoMessageFilter", false); - $('#videoConnectionMessage').css({display: "none"}); - - if (recordingToaster) { - messageHandler.remove(recordingToaster); - } - } else if (recordingState === 'pending') { - selector.removeClass("icon-recEnable active"); - selector.addClass("icon-recEnable"); - - $("#largeVideo").toggleClass("videoMessageFilter", true); - let recordPendingKey = "recording.pending"; - $('#videoConnectionMessage').attr("data-i18n", recordPendingKey); - $('#videoConnectionMessage').text(APP.translation.translateString(recordPendingKey)); - $('#videoConnectionMessage').css({display: "block"}); - } -} - const buttonHandlers = { "toolbar_button_mute": function () { if (APP.conference.audioMuted) { @@ -107,10 +61,6 @@ const buttonHandlers = { emitter.emit(UIEvents.VIDEO_MUTED, true); } }, - "toolbar_button_record": function () { - AnalyticsAdapter.sendEvent('toolbar.recording.toggled'); - emitter.emit(UIEvents.RECORDING_TOGGLE); - }, "toolbar_button_security": function () { emitter.emit(UIEvents.ROOM_LOCK_CLICKED); }, @@ -250,7 +200,8 @@ const Toolbar = { */ unlockLockButton () { if ($("#toolbar_button_security").hasClass("icon-security-locked")) - UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked"); + UIUtil.buttonClick("#toolbar_button_security", + "icon-security icon-security-locked"); }, /** @@ -258,7 +209,8 @@ const Toolbar = { */ lockLockButton () { if ($("#toolbar_button_security").hasClass("icon-security")) - UIUtil.buttonClick("#toolbar_button_security", "icon-security icon-security-locked"); + UIUtil.buttonClick("#toolbar_button_security", + "icon-security icon-security-locked"); }, /** @@ -279,15 +231,6 @@ const Toolbar = { } }, - // Shows or hides the 'recording' button. - showRecordingButton (show) { - if (UIUtil.isButtonEnabled('recording') && show) { - $('#toolbar_button_record').css({display: "inline-block"}); - } else { - $('#toolbar_button_record').css({display: "none"}); - } - }, - // Shows or hides the 'shared video' button. showSharedVideoButton (show) { if (UIUtil.isButtonEnabled('sharedvideo') && show) { @@ -297,14 +240,6 @@ const Toolbar = { } }, - // checks whether recording is enabled and whether we have params - // to start automatically recording - checkAutoRecord () { - if (UIUtil.isButtonEnabled('recording') && config.autoRecord) { - emitter.emit(UIEvents.RECORDING_TOGGLE, UIUtil.escapeHtml(config.autoRecordToken)); - } - }, - // checks whether desktop sharing is enabled and whether // we have params to start automatically sharing checkAutoEnableDesktopSharing () { @@ -382,10 +317,6 @@ const Toolbar = { } }, - updateRecordingState (state) { - setRecordingButtonState(state); - }, - /** * Marks video icon as muted or not. * @param {boolean} muted if icon should look like muted or not diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index f34c2544b..ab284805f 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -123,11 +123,7 @@ }, isButtonEnabled: function (name) { - var isEnabled = interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1; - if (name === 'recording') { - return isEnabled && config.enableRecording; - } - return isEnabled; + return interfaceConfig.TOOLBAR_BUTTONS.indexOf(name) !== -1; }, hideDisabledButtons: function (mappings) { diff --git a/service/UI/UIEvents.js b/service/UI/UIEvents.js index 5c091481c..5da292339 100644 --- a/service/UI/UIEvents.js +++ b/service/UI/UIEvents.js @@ -62,7 +62,7 @@ export default { CONTACT_CLICKED: "UI.contact_clicked", HANGUP: "UI.hangup", LOGOUT: "UI.logout", - RECORDING_TOGGLE: "UI.recording_toggle", + RECORDING_TOGGLED: "UI.recording_toggled", SIP_DIAL: "UI.sip_dial", SUBJECT_CHANGED: "UI.subject_changed", VIDEO_DEVICE_CHANGED: "UI.video_device_changed", From 7df5e92bc96c47e68185f95154343ea7caf79226 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 13:10:31 -0500 Subject: [PATCH 04/31] Adds new recording module. --- modules/UI/recording/Recording.js | 277 ++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 modules/UI/recording/Recording.js diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js new file mode 100644 index 000000000..aae5ec85b --- /dev/null +++ b/modules/UI/recording/Recording.js @@ -0,0 +1,277 @@ +/* global APP, $, config, interfaceConfig */ +/* + * Copyright @ 2015 Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import UIEvents from "../../../service/UI/UIEvents"; +import UIUtil from '../util/UIUtil'; + +/** + * Recording. + */ +let recordingToaster = null; + +function _isRecordingButtonEnabled() { + return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1 + && config.enableRecording; +} + +/** + * Request live stream token from the user. + * @returns {Promise} + */ +function _requestLiveStreamId() { + let msg = APP.translation.generateTranslationHTML("dialog.liveStreaming"); + let token = APP.translation.translateString("dialog.streamKey"); + return new Promise(function (resolve, reject) { + APP.UI.messageHandler.openTwoButtonDialog( + null, null, null, + `

${msg}

+ `, + false, "dialog.startLiveStreaming", + function (e, v, m, f) { + if (v && f.streamId) { + resolve(UIUtil.escapeHtml(f.streamId)); + } else { + reject(); + } + }, + null, + function () { }, + ':input:first' + ); + }); +} + +/** + * Request recording token from the user. + * @returns {Promise} + */ +function _requestRecordingToken () { + let msg = APP.translation.generateTranslationHTML("dialog.recordingToken"); + let token = APP.translation.translateString("dialog.token"); + + return new Promise(function (resolve, reject) { + APP.UI.messageHandler.openTwoButtonDialog( + null, null, null, + `

${msg}

+ `, + false, "dialog.Save", + function (e, v, m, f) { + if (v && f.recordingToken) { + resolve(UIUtil.escapeHtml(f.recordingToken)); + } else { + reject(); + } + }, + null, + function () { }, + ':input:first' + ); + }); +} + +function _showStopRecordingPrompt (recordingType) { + var title; + var message; + var buttonKey; + if (recordingType === "jibri") { + title = "dialog.liveStreaming"; + message = "dialog.stopStreamingWarning"; + buttonKey = "dialog.stopLiveStreaming"; + } + else { + title = "dialog.recording"; + message = "dialog.stopRecordingWarning"; + buttonKey = "dialog.stopRecording"; + } + + return new Promise(function (resolve, reject) { + APP.UI.messageHandler.openTwoButtonDialog( + title, + null, + message, + null, + false, + buttonKey, + function(e,v,m,f) { + if (v) { + resolve(); + } else { + reject(); + } + } + ); + }); +} + +function moveToCorner(selector, move) { + let moveToCornerClass = "moveToCorner"; + + if (move && !selector.hasClass(moveToCornerClass)) + selector.addClass(moveToCornerClass); + else + selector.removeClass(moveToCornerClass); +} + +var Recording = { + /** + * Initializes the recording UI. + */ + init (emitter, recordingType) { + this.eventEmitter = emitter; + + this.initRecordingButton(recordingType); + }, + + initRecordingButton(recordingType) { + let selector = $('#toolbar_button_record'); + + if (recordingType === 'jibri') { + this.baseClass = "fa fa-play-circle"; + this.recordingOnKey = "liveStreaming.on"; + this.recordingOffKey = "liveStreaming.off"; + this.recordingPendingKey = "liveStreaming.pending"; + this.failedToStartKey = "liveStreaming.failedToStart"; + } + else { + this.baseClass = "icon-recEnable"; + this.recordingOnKey = "recording.on"; + this.recordingOffKey = "recording.off"; + this.recordingPendingKey = "recording.pending"; + this.failedToStartKey = "recording.failedToStart"; + } + + selector.addClass(this.baseClass); + + var self = this; + selector.click(function () { + console.log("BUTTON CLICKED", self.currentState); + switch (self.currentState) { + case "on": { + _showStopRecordingPrompt(recordingType).then(() => + self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED)); + } + break; + case "available": + case "off": { + if (recordingType === 'jibri') + _requestLiveStreamId().then((streamId) => { + self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, + {streamId: streamId}); + }); + else { + if (self.predefinedToken) { + self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, + {token: self.predefinedToken}); + return; + } + + _requestRecordingToken().then((token) => { + self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, + {token: token}); + }); + } + } + break; + default: { + APP.UI.messageHandler.openMessageDialog( + "dialog.liveStreaming", + "liveStreaming.unavailable" + ); + } + } + }); + }, + + // Shows or hides the 'recording' button. + showRecordingButton (show) { + if (_isRecordingButtonEnabled() && show) { + $('#toolbar_button_record').css({display: "inline-block"}); + } else { + $('#toolbar_button_record').css({display: "none"}); + } + }, + + updateRecordingState(recordingState) { + this.setRecordingButtonState(recordingState); + }, + + // Sets the state of the recording button + setRecordingButtonState (recordingState) { + let buttonSelector = $('#toolbar_button_record'); + let labelSelector = $('#recordingLabel'); + + // TODO: handle recording state=available + if (recordingState === 'on') { + + buttonSelector.removeClass(this.baseClass); + buttonSelector.addClass(this.baseClass + " active"); + + labelSelector.attr("data-i18n", this.recordingOnKey); + moveToCorner(labelSelector, true, 3000); + labelSelector + .text(APP.translation.translateString(this.recordingOnKey)); + } else if (recordingState === 'off') { + + buttonSelector.removeClass(this.baseClass + " active"); + buttonSelector.addClass(this.baseClass); + + moveToCorner(labelSelector, false); + let messageKey; + if (this.currentState === "pending") + messageKey = this.failedToStartKey; + else + messageKey = this.recordingOffKey; + + labelSelector.attr("data-i18n", messageKey); + labelSelector.text(APP.translation.translateString(messageKey)); + + setTimeout(function(){ + $('#recordingLabel').css({display: "none"}); + }, 5000); + } else if (recordingState === 'pending') { + + buttonSelector.removeClass(this.baseClass + " active"); + buttonSelector.addClass(this.baseClass); + + moveToCorner(labelSelector, false); + labelSelector + .attr("data-i18n", this.recordingPendingKey); + labelSelector + .text(APP.translation.translateString( + this.recordingPendingKey)); + } + + this.currentState = recordingState; + + if (!labelSelector.is(":visible")) + labelSelector.css({display: "inline-block"}); + }, + // checks whether recording is enabled and whether we have params + // to start automatically recording + checkAutoRecord () { + if (_isRecordingButtonEnabled && config.autoRecord) { + this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken); + this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED, + this.predefinedToken); + } + } +}; + +export default Recording; From 51037c2dca9adac9cfa511be6f3f712715699076 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 13:46:10 -0500 Subject: [PATCH 05/31] Fixes status change from pending to unavailable. --- modules/UI/recording/Recording.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index aae5ec85b..3c55d749d 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -209,6 +209,10 @@ var Recording = { }, updateRecordingState(recordingState) { + // If there's no state change, we ignore the update. + if (this.currentState === recordingState) + return; + this.setRecordingButtonState(recordingState); }, @@ -227,7 +231,14 @@ var Recording = { moveToCorner(labelSelector, true, 3000); labelSelector .text(APP.translation.translateString(this.recordingOnKey)); - } else if (recordingState === 'off') { + } else if (recordingState === 'off' + || recordingState === 'unavailable') { + + // We don't want to do any changes if this is + // an availability change. + if (this.currentState === "available" + || this.currentState === "unavailable") + return; buttonSelector.removeClass(this.baseClass + " active"); buttonSelector.addClass(this.baseClass); @@ -245,7 +256,8 @@ var Recording = { setTimeout(function(){ $('#recordingLabel').css({display: "none"}); }, 5000); - } else if (recordingState === 'pending') { + } + else if (recordingState === 'pending') { buttonSelector.removeClass(this.baseClass + " active"); buttonSelector.addClass(this.baseClass); From 6d51cb5f40bb04c79461cd1de89716174ccf3fbe Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 13:49:05 -0500 Subject: [PATCH 06/31] Fixes mistakenly removed button. --- modules/UI/UI.js | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 2542a41af..946578faf 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -567,6 +567,7 @@ UI.updateLocalRole = function (isModerator) { VideoLayout.showModeratorIndicator(); Toolbar.showSipCallButton(isModerator); + Toolbar.showRecordingButton(isModerator); Recording.showRecordingButton(isModerator); SettingsMenu.showStartMutedOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator); From 8b060e9cc0357d93063fbc24eb736e7ab4c0e551 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 13:51:21 -0500 Subject: [PATCH 07/31] Fixes mistakenly removed button. --- modules/UI/UI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 946578faf..1f85edbf7 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -567,7 +567,7 @@ UI.updateLocalRole = function (isModerator) { VideoLayout.showModeratorIndicator(); Toolbar.showSipCallButton(isModerator); - Toolbar.showRecordingButton(isModerator); + Toolbar.showSharedVideoButton(isModerator); Recording.showRecordingButton(isModerator); SettingsMenu.showStartMutedOptions(isModerator); SettingsMenu.showFollowMeOptions(isModerator); From a38b628d763c77aea19d9ceb47f8d1c10e5ffba8 Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 29 Mar 2016 14:36:20 -0500 Subject: [PATCH 08/31] Docks toolbar when shared video is shown. --- modules/UI/shared_video/SharedVideo.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index f2b578a63..7b32f7990 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -333,6 +333,7 @@ class SharedVideoContainer extends LargeContainer { self.bodyBackground = document.body.style.background; document.body.style.background = 'black'; this.$iframe.css({opacity: 1}); + ToolbarToggler.dockToolbar(true); resolve(); }); }); @@ -340,6 +341,7 @@ class SharedVideoContainer extends LargeContainer { hide () { let self = this; + ToolbarToggler.dockToolbar(false); return new Promise(resolve => { this.$iframe.fadeOut(300, () => { document.body.style.background = self.bodyBackground; From 39c350cdba0a404e7f4042cb96b4cc8b30f36949 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 16:07:01 -0500 Subject: [PATCH 09/31] Fixes streaming tooltip. Checks if the stream id is not provided and requests the user before starting the recorder. --- css/videolayout_default.css | 2 +- index.html | 2 +- lang/main.json | 8 +- modules/UI/recording/Recording.js | 119 ++++++++++++++++++++++-------- 4 files changed, 97 insertions(+), 34 deletions(-) diff --git a/css/videolayout_default.css b/css/videolayout_default.css index ed00174e4..1c86854ee 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -513,7 +513,7 @@ .moveToCorner { top: 5px; - right: 5px; + right: 50px; /*leave free space for the HD label*/ margin-right: 0px; margin-left: auto; background: rgba(0,0,0,.3); diff --git a/index.html b/index.html index 1e2608db7..aec04ade9 100644 --- a/index.html +++ b/index.html @@ -116,7 +116,7 @@ - + diff --git a/lang/main.json b/lang/main.json index 99de636ae..6269e2f26 100644 --- a/lang/main.json +++ b/lang/main.json @@ -51,7 +51,6 @@ "mute": "Mute / Unmute", "videomute": "Start / stop camera", "authenticate": "Authenticate", - "record": "Toggle recording", "lock": "Lock / unlock room", "invite": "Invite others", "chat": "Open / close chat", @@ -264,7 +263,8 @@ "pending": "Recording waiting for a participant to join...", "on": "Recording", "off": "Recording stopped", - "failedToStart": "Recording failed to start" + "failedToStart": "Recording failed to start", + "buttonTooltip": "Start / stop recording" }, "liveStreaming": { @@ -272,6 +272,8 @@ "on": "Live Streaming", "off": "Live Streaming Stopped", "unavailable": "The live streaming service is currently unavailable. Please try again later.", - "failedToStart": "Live streaming failed to start" + "failedToStart": "Live streaming failed to start", + "buttonTooltip": "Start / stop live stream", + "streamIdRequired": "Please fill in the stream id in order to launch the live streaming." } } diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 3c55d749d..77603e326 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -32,27 +32,73 @@ function _isRecordingButtonEnabled() { * @returns {Promise} */ function _requestLiveStreamId() { - let msg = APP.translation.generateTranslationHTML("dialog.liveStreaming"); - let token = APP.translation.translateString("dialog.streamKey"); + const msg = APP.translation.generateTranslationHTML("dialog.liveStreaming"); + const token = APP.translation.translateString("dialog.streamKey"); + const cancelButton + = APP.translation.generateTranslationHTML("dialog.Cancel"); + const backButton = APP.translation.generateTranslationHTML("dialog.Back"); + const startStreamingButton + = APP.translation.generateTranslationHTML("dialog.startLiveStreaming"); + const streamIdRequired + = APP.translation.generateTranslationHTML( + "liveStreaming.streamIdRequired"); + return new Promise(function (resolve, reject) { - APP.UI.messageHandler.openTwoButtonDialog( - null, null, null, - `

${msg}

- ${msg} + `, - false, "dialog.startLiveStreaming", - function (e, v, m, f) { - if (v && f.streamId) { - resolve(UIUtil.escapeHtml(f.streamId)); - } else { - reject(); + persistent: false, + buttons: [ + {title: cancelButton, value: false}, + {title: startStreamingButton, value: true} + ], + focus: ':input:first', + defaultButton: 1, + submit: function (e, v, m, f) { + e.preventDefault(); + + if (v) { + if (f.streamId && f.streamId.length > 0) { + resolve(UIUtil.escapeHtml(f.streamId)); + dialog.close(); + return; + } + else { + dialog.goToState('state1'); + return false; + } + } else { + reject(); + dialog.close(); + return false; + } } }, - null, - function () { }, - ':input:first' - ); + + state1: { + html: `

${msg}

${streamIdRequired}`, + persistent: false, + buttons: [ + {title: cancelButton, value: false}, + {title: backButton, value: true} + ], + focus: ':input:first', + defaultButton: 1, + submit: function (e, v, m, f) { + e.preventDefault(); + if (v === 0) { + reject(); + dialog.close(); + } else { + dialog.goToState('state0'); + } + } + } + }); }); } @@ -129,12 +175,22 @@ function moveToCorner(selector, move) { selector.removeClass(moveToCornerClass); } +var Status = { + ON: "on", + OFF: "off", + AVAILABLE: "available", + UNAVAILABLE: "unavailable", + PENDING: "pending" +} + var Recording = { /** * Initializes the recording UI. */ init (emitter, recordingType) { this.eventEmitter = emitter; + // Use recorder states directly from the library. + this.currentState = Status.UNAVAILABLE; this.initRecordingButton(recordingType); }, @@ -148,6 +204,7 @@ var Recording = { this.recordingOffKey = "liveStreaming.off"; this.recordingPendingKey = "liveStreaming.pending"; this.failedToStartKey = "liveStreaming.failedToStart"; + this.recordingButtonTooltip = "liveStreaming.buttonTooltip"; } else { this.baseClass = "icon-recEnable"; @@ -155,21 +212,25 @@ var Recording = { this.recordingOffKey = "recording.off"; this.recordingPendingKey = "recording.pending"; this.failedToStartKey = "recording.failedToStart"; + this.recordingButtonTooltip = "recording.buttonTooltip"; } selector.addClass(this.baseClass); + selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip); + selector.attr("content", + APP.translation.translateString(this.recordingButtonTooltip)); var self = this; selector.click(function () { - console.log("BUTTON CLICKED", self.currentState); switch (self.currentState) { - case "on": { + case Status.ON: + case Status.PENDING: { _showStopRecordingPrompt(recordingType).then(() => self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED)); - } break; - case "available": - case "off": { + } + case Status.AVAILABLE: + case Status.OFF: { if (recordingType === 'jibri') _requestLiveStreamId().then((streamId) => { self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED, @@ -187,8 +248,8 @@ var Recording = { {token: token}); }); } - } break; + } default: { APP.UI.messageHandler.openMessageDialog( "dialog.liveStreaming", @@ -222,7 +283,7 @@ var Recording = { let labelSelector = $('#recordingLabel'); // TODO: handle recording state=available - if (recordingState === 'on') { + if (recordingState === Status.ON) { buttonSelector.removeClass(this.baseClass); buttonSelector.addClass(this.baseClass + " active"); @@ -231,13 +292,13 @@ var Recording = { moveToCorner(labelSelector, true, 3000); labelSelector .text(APP.translation.translateString(this.recordingOnKey)); - } else if (recordingState === 'off' - || recordingState === 'unavailable') { + } else if (recordingState === Status.OFF + || recordingState === Status.UNAVAILABLE) { // We don't want to do any changes if this is // an availability change. - if (this.currentState === "available" - || this.currentState === "unavailable") + if (this.currentState === Status.AVAILABLE + || this.currentState === Status.UNAVAILABLE) return; buttonSelector.removeClass(this.baseClass + " active"); @@ -245,7 +306,7 @@ var Recording = { moveToCorner(labelSelector, false); let messageKey; - if (this.currentState === "pending") + if (this.currentState === Status.PENDING) messageKey = this.failedToStartKey; else messageKey = this.recordingOffKey; @@ -257,7 +318,7 @@ var Recording = { $('#recordingLabel').css({display: "none"}); }, 5000); } - else if (recordingState === 'pending') { + else if (recordingState === Status.PENDING) { buttonSelector.removeClass(this.baseClass + " active"); buttonSelector.addClass(this.baseClass); From 76820bed8d21d83e10d4125af5406f94b2fe30f9 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 16:30:08 -0500 Subject: [PATCH 10/31] Fixes recording state handling. --- modules/UI/recording/Recording.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 77603e326..386b9d617 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -181,7 +181,7 @@ var Status = { AVAILABLE: "available", UNAVAILABLE: "unavailable", PENDING: "pending" -} +}; var Recording = { /** @@ -297,8 +297,8 @@ var Recording = { // We don't want to do any changes if this is // an availability change. - if (this.currentState === Status.AVAILABLE - || this.currentState === Status.UNAVAILABLE) + if (this.currentState !== Status.ON + && this.currentState !== Status.PENDING) return; buttonSelector.removeClass(this.baseClass + " active"); @@ -333,7 +333,9 @@ var Recording = { this.currentState = recordingState; - if (!labelSelector.is(":visible")) + // We don't show the label for available state. + if (recordingState !== Status.AVAILABLE + && !labelSelector.is(":visible")) labelSelector.css({display: "inline-block"}); }, // checks whether recording is enabled and whether we have params From 7f7d9d5594a2b539f27b93b91b909e354bc7caa3 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 29 Mar 2016 17:26:39 -0500 Subject: [PATCH 11/31] Adds an I am a recorder parameter that allows for the UI to be cleaner and simpler for the recorders. --- modules/UI/recording/Recording.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 386b9d617..38e65504e 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -18,10 +18,11 @@ import UIEvents from "../../../service/UI/UIEvents"; import UIUtil from '../util/UIUtil'; /** - * Recording. + * Indicates if the recording button should be enabled. + * + * @returns {boolean} {true} if the + * @private */ -let recordingToaster = null; - function _isRecordingButtonEnabled() { return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1 && config.enableRecording; @@ -270,6 +271,10 @@ var Recording = { }, updateRecordingState(recordingState) { + // I'm the recorder, so I don't want to see any UI related to states. + if (config.iAmRecorder) + return; + // If there's no state change, we ignore the update. if (this.currentState === recordingState) return; From 06f3ddc822832e5ef7efa453cf9d194804efc01f Mon Sep 17 00:00:00 2001 From: damencho Date: Tue, 29 Mar 2016 17:28:20 -0500 Subject: [PATCH 12/31] Hides the toolbar if the user is a recorder. --- modules/UI/toolbars/ToolbarToggler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/UI/toolbars/ToolbarToggler.js b/modules/UI/toolbars/ToolbarToggler.js index 698aef269..ac13c6910 100644 --- a/modules/UI/toolbars/ToolbarToggler.js +++ b/modules/UI/toolbars/ToolbarToggler.js @@ -59,7 +59,8 @@ const ToolbarToggler = { * Shows the main toolbar. */ showToolbar () { - if (interfaceConfig.filmStripOnly) { + // if we are a recorder we do not want to show the toolbar + if (interfaceConfig.filmStripOnly || config.iAmRecorder) { return; } let header = $("#header"); From 166a609b94f8a36ada30c103559ad5c07d4b1dd6 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Wed, 30 Mar 2016 13:32:17 -0500 Subject: [PATCH 13/31] Removes console.warn message from do_external_connect.js if there is no error passed. --- connection_optimization/do_external_connect.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/connection_optimization/do_external_connect.js b/connection_optimization/do_external_connect.js index 99c277f4f..b4b9fbc58 100644 --- a/connection_optimization/do_external_connect.js +++ b/connection_optimization/do_external_connect.js @@ -42,7 +42,8 @@ function buildToken(){ } function error_callback(error){ - console.warn(error); + if(error) //error=undefined if external connect is disabled. + console.warn(error); // Sets that global variable to be used later by connect method in // connection.js window.XMPPAttachInfo = { From d9e08032cee44662ef5fdf02ddfd5b87e5d931e8 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Wed, 30 Mar 2016 21:01:21 -0500 Subject: [PATCH 14/31] Changes translation module to load async --- modules/translation/translation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/translation/translation.js b/modules/translation/translation.js index 99331d5f3..0f3a6761d 100644 --- a/modules/translation/translation.js +++ b/modules/translation/translation.js @@ -24,7 +24,7 @@ var defaultOptions = { fallbackOnEmpty: true, useDataAttrOptions: true, app: interfaceConfig.APP_NAME, - getAsync: false, + getAsync: true, defaultValueFromContent: false, customLoad: function(lng, ns, options, done) { var resPath = "lang/__ns__-__lng__.json"; From 952eaf5a0c8257fb52cf3e4427a79e7fe2165375 Mon Sep 17 00:00:00 2001 From: damencho Date: Wed, 30 Mar 2016 21:36:05 -0500 Subject: [PATCH 15/31] Avoids double loading of the player. --- modules/UI/shared_video/SharedVideo.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 7b32f7990..6adab009b 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -64,6 +64,8 @@ export default class SharedVideoManager { if (this.isSharedVideoShown) return; + this.isSharedVideoShown = true; + // the video url this.url = url; @@ -148,8 +150,6 @@ export default class SharedVideoManager { SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo); VideoLayout.handleVideoThumbClicked(self.url); - self.isSharedVideoShown = true; - // If we are sending the command and we are starting the player // we need to continuously send the player current time position if(APP.conference.isLocalId(self.from)) { @@ -167,7 +167,7 @@ export default class SharedVideoManager { }; window.onPlayerError = function(event) { - console.error("Error in the player:" + event.data); + console.error("Error in the player:", event.data); }; } From 6951089130baea54b3f5c2e43dca80863eadac90 Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 31 Mar 2016 00:07:12 -0500 Subject: [PATCH 16/31] Force seek when transition from and to pause state. --- modules/UI/shared_video/SharedVideo.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 6adab009b..179ec6c31 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -184,7 +184,7 @@ export default class SharedVideoManager { if (attributes.state == 'playing') { - this.processTime(player, attributes); + this.processTime(player, attributes, playerPaused); // lets check the volume if (attributes.volume !== undefined && @@ -200,7 +200,7 @@ export default class SharedVideoManager { // if its not paused, pause it player.pauseVideo(); - this.processTime(player, attributes); + this.processTime(player, attributes, !playerPaused); } } @@ -208,9 +208,15 @@ export default class SharedVideoManager { * Check for time in attributes and if needed seek in current player * @param player the player to operate over * @param attributes the attributes with the player state we want + * @param forceSeek whether seek should be forced */ - processTime (player, attributes) + processTime (player, attributes, forceSeek) { + if(forceSeek) { + player.seekTo(attributes.time); + return; + } + // check received time and current time let currentPosition = player.getCurrentTime(); let diff = Math.abs(attributes.time - currentPosition); From 2e3dcb142d3cdf9bc4b2b5b30c03052efdef7166 Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 31 Mar 2016 11:43:37 -0500 Subject: [PATCH 17/31] Updates translation. --- index.html | 2 +- lang/main.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 12bff64b7..7dfe6931f 100644 --- a/index.html +++ b/index.html @@ -126,7 +126,7 @@
- + diff --git a/lang/main.json b/lang/main.json index 59b812737..7ac013ca4 100644 --- a/lang/main.json +++ b/lang/main.json @@ -55,7 +55,7 @@ "invite": "Invite others", "chat": "Open / close chat", "etherpad": "Shared document", - "sharedvideo": "Shared video", + "sharedvideo": "Share a YouTube video", "sharescreen": "Share screen", "fullscreen": "Enter / Exit Full Screen", "sip": "Call SIP number", From 104503ee13737e311e43f2bb3f26f047dbad18ef Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 31 Mar 2016 12:14:45 -0500 Subject: [PATCH 18/31] Make sure we store initial attributes in order, so it will hold the last state we want to be in. Respects quick initial stop received. --- conference.js | 2 +- modules/UI/UI.js | 2 +- modules/UI/shared_video/SharedVideo.js | 27 ++++++++++++++------------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/conference.js b/conference.js index dc2ba0249..bc8775e81 100644 --- a/conference.js +++ b/conference.js @@ -1110,7 +1110,7 @@ export default { Commands.SHARED_VIDEO, ({value, attributes}, id) => { if (attributes.state === 'stop') { - APP.UI.stopSharedVideo(id); + APP.UI.stopSharedVideo(id, attributes); } else if (attributes.state === 'start') { APP.UI.showSharedVideo(id, value, attributes); } else if (attributes.state === 'playing' diff --git a/modules/UI/UI.js b/modules/UI/UI.js index ce3492332..a60c276aa 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -1115,7 +1115,7 @@ UI.updateSharedVideo = function (id, url, attributes) { */ UI.stopSharedVideo = function (id, attributes) { if (sharedVideoManager) - sharedVideoManager.stopSharedVideo(id); + sharedVideoManager.stopSharedVideo(id, attributes); }; module.exports = UI; diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 179ec6c31..addf0a238 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -44,7 +44,8 @@ export default class SharedVideoManager { if(APP.conference.isLocalId(this.from)) { showStopVideoPropmpt().then(() => - this.emitter.emit(UIEvents.UPDATE_SHARED_VIDEO, null, 'stop')); + this.emitter.emit( + UIEvents.UPDATE_SHARED_VIDEO, this.url, 'stop')); } else { messageHandler.openMessageDialog( "dialog.shareVideoTitle", @@ -84,7 +85,7 @@ export default class SharedVideoManager { // we need to operate with player after start playing // self.player will be defined once it start playing // and will process any initial attributes if any - this.initialAttributes = null; + this.initialAttributes = attributes; var self = this; if(self.isPlayerAPILoaded) @@ -157,13 +158,6 @@ export default class SharedVideoManager { self.updateCheck.bind(self), updateInterval); } - - if(self.player) - self.processAttributes( - self.player, attributes, self.playerPaused); - else { - self.initialAttributes = attributes; - } }; window.onPlayerError = function(event) { @@ -201,6 +195,8 @@ export default class SharedVideoManager { player.pauseVideo(); this.processTime(player, attributes, !playerPaused); + } else if (attributes.state == 'stop') { + this.stopSharedVideo(this.from); } } @@ -236,8 +232,10 @@ export default class SharedVideoManager { updateCheck(sendPauseEvent) { // ignore update checks if we are not the owner of the video - // or there is still no player defined - if(!APP.conference.isLocalId(this.from) || !this.player) + // or there is still no player defined or we are stopped + // (in a process of stopping) + if(!APP.conference.isLocalId(this.from) || !this.player + || !this.isSharedVideoShown) return; let state = this.player.getPlayerState(); @@ -287,13 +285,18 @@ export default class SharedVideoManager { * left and we want to remove video if the user sharing it left). * @param id the id of the sender of the command */ - stopSharedVideo (id) { + stopSharedVideo (id, attributes) { if (!this.isSharedVideoShown) return; if(this.from !== id) return; + if(!this.player){ + this.initialAttributes = attributes; + return; + } + if(this.intervalId) { clearInterval(this.intervalId); this.intervalId = null; From 5cbe71007501b760d35669afe3b4cb2da3345f17 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 31 Mar 2016 13:31:05 -0500 Subject: [PATCH 19/31] Fixes enter room name field in the welcome page. --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 12bff64b7..ba9ebf38d 100644 --- a/index.html +++ b/index.html @@ -35,7 +35,7 @@
- +
From ebe37ff98a1ea3fdebce6c381da5fa0099d37876 Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 31 Mar 2016 14:11:33 -0500 Subject: [PATCH 20/31] Handle on player error so we can remove the player. --- modules/UI/shared_video/SharedVideo.js | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index addf0a238..26996e397 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -162,6 +162,8 @@ export default class SharedVideoManager { window.onPlayerError = function(event) { console.error("Error in the player:", event.data); + // store the error player, so we can remove it + self.errorInPlayer = event.target; }; } @@ -293,8 +295,12 @@ export default class SharedVideoManager { return; if(!this.player){ - this.initialAttributes = attributes; - return; + // if there is no error in the player till now, + // store the initial attributes + if (!this.errorInPlayer) { + this.initialAttributes = attributes; + return; + } } if(this.intervalId) { @@ -309,12 +315,19 @@ export default class SharedVideoManager { VideoLayout.removeLargeVideoContainer( SHARED_VIDEO_CONTAINER_TYPE); - this.player.destroy(); - this.player = null; + if(this.player) { + this.player.destroy(); + this.player = null; + }// + else if (this.errorInPlayer) { + this.errorInPlayer.destroy(); + this.errorInPlayer = null; + } }); this.url = null; this.isSharedVideoShown = false; + this.initialAttributes = null; } } From ea9b909775938de088c49541e1a0a1d4bb9fa97d Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 31 Mar 2016 15:12:33 -0500 Subject: [PATCH 21/31] Fixes exception in follow-me when we receive an update for a video thumbnail that's not there yet. --- modules/FollowMe.js | 62 ++++++++++++++++++++++++---- modules/UI/videolayout/SmallVideo.js | 18 ++++++-- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/modules/FollowMe.js b/modules/FollowMe.js index 0d5086809..0626d1bd0 100644 --- a/modules/FollowMe.js +++ b/modules/FollowMe.js @@ -25,6 +25,15 @@ import FilmStrip from './UI/videolayout/FilmStrip'; */ const _COMMAND = "follow-me"; +/** + * The timeout after which a follow-me command that has been received will be + * ignored if not consumed. + * + * @type {number} in seconds + * @private + */ +const _FOLLOW_ME_RECEIVED_TIMEOUT = 30; + /** * Represents the set of {FollowMe}-related states (properties and their * respective values) which are to be followed by a participant. {FollowMe} @@ -112,6 +121,7 @@ class FollowMe { constructor (conference, UI) { this._conference = conference; this._UI = UI; + this.nextOnStageTimer = 0; // The states of the local participant which are to be followed (by the // remote participants when the local participant is in her right to @@ -265,6 +275,7 @@ class FollowMe { // issued by a defined commander. if (typeof id === 'undefined') return; + // The Command(s) API will send us our own commands and we don't want // to act upon them. if (this._conference.isLocalId(id)) @@ -297,22 +308,24 @@ class FollowMe { // writing whether calling UI.toggleFilmStrip() is acceptable (from // a design standpoint) either. if (filmStripVisible !== FilmStrip.isFilmStripVisible()) - this._UI.eventEmitter.emit( - UIEvents.TOGGLE_FILM_STRIP, - filmStripVisible); + this._UI.eventEmitter.emit(UIEvents.TOGGLE_FILM_STRIP); } } _onNextOnStage(id) { - var clickId = null; - if(typeof id !== 'undefined' && !VideoLayout.isPinned(id)) + var pin; + if(typeof id !== 'undefined' && !VideoLayout.isPinned(id)) { clickId = id; - else if (typeof id == 'undefined' && VideoLayout.getPinnedId()) + pin = true; + } + else if (typeof id == 'undefined' && VideoLayout.getPinnedId()) { clickId = VideoLayout.getPinnedId(); + pin = false; + } if (clickId) - VideoLayout.handleVideoThumbClicked(clickId); + this._pinVideoThumbnailById(clickId, pin); } _onSharedDocumentVisible(sharedDocumentVisible) { @@ -328,6 +341,41 @@ class FollowMe { this._UI.getSharedDocumentManager().toggleEtherpad(); } } + + /** + * Pins / unpins the video thumbnail given by clickId. + * + * @param clickId the identifier of the video thumbnail to pin or unpin + * @param pin {true} to pin, {false} to unpin + * @private + */ + _pinVideoThumbnailById(clickId, pin) { + var self = this; + var smallVideo = VideoLayout.getSmallVideo(clickId); + + // If the SmallVideo for the given clickId exists we proceed with the + // pin/unpin. + if (smallVideo) { + this.nextOnStageTimer = 0; + clearTimeout(this.nextOnStageTimout); + if (pin && !VideoLayout.isPinned(clickId) + || !pin && VideoLayout.isPinned(clickId)) + VideoLayout.handleVideoThumbClicked(clickId); + } + // If there's no SmallVideo object for the given id, lets wait and see + // if it's going to be created in the next 30sec. + else { + this.nextOnStageTimout = setTimeout(function () { + if (self.nextOnStageTimer > _FOLLOW_ME_RECEIVED_TIMEOUT) { + self.nextOnStageTimer = 0; + return; + } + + this.nextOnStageTimer++; + self._pinVideoThumbnailById(clickId, pin); + }, 1000); + } + } } export default FollowMe; diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 6a878e4d1..e20275b47 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -315,11 +315,21 @@ SmallVideo.prototype.selectVideoElement = function () { return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0])); }; +/** + * Enables / disables the css responsible for focusing/pinning a video + * thumbnail. + * + * @param isFocused indicates if the thumbnail should be focused/pinned or not + */ SmallVideo.prototype.focus = function(isFocused) { - if(!isFocused) { - this.container.classList.remove("videoContainerFocused"); - } else { - this.container.classList.add("videoContainerFocused"); + var focusedCssClass = "videoContainerFocused"; + var isFocusClassEnabled = $(this.container).hasClass(focusedCssClass); + + if (!isFocused && isFocusClassEnabled) { + $(this.container).removeClass(focusedCssClass); + } + else if (isFocused && !isFocusClassEnabled) { + $(this.container).addClass(focusedCssClass); } }; From 67e8118d7456834407778fee240305c3910b3ce8 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 31 Mar 2016 17:35:10 -0500 Subject: [PATCH 22/31] Shows/hides feedback button when the filmstrip is shown/hidden. --- css/main.css | 17 ++++++----------- css/videolayout_default.css | 2 +- index.html | 2 +- modules/UI/Feedback.js | 21 ++++++++++++++++++--- modules/UI/UI.js | 2 +- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/css/main.css b/css/main.css index a164f8516..190c0565b 100644 --- a/css/main.css +++ b/css/main.css @@ -142,15 +142,6 @@ a.bottomToolbarButton:hover { color: #636363; } -.header_button_separator { - display: inline-block; - position:relative; - top: 5px; - width: 1px; - height: 20px; - background: #373737; -} - .bottom_button_separator { display: inline-block; position: relative; @@ -235,7 +226,7 @@ form { color: rgba(255,255,255,.50); } -div.feedbackButton { +#feedbackButtonDiv { display: none; position: absolute; background-color: rgba(0,0,0,.50); @@ -246,7 +237,11 @@ div.feedbackButton { left: -50px; z-index: 100; overflow: hidden; - transition: all .2s ease-in-out; + transition: all 2s ease-in-out; +} + +#feedbackButtonDiv.hidden { + bottom: -246px; } div.feedbackButton:hover { diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 1c86854ee..acf68d44c 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -506,7 +506,7 @@ background: rgba(0,0,0,.5); color: #FFF; z-index: 10000; - border-radius: 4px; + border-radius: 2px; -webkit-transition: all 2s 2s linear; transition: all 2s 2s linear; } diff --git a/index.html b/index.html index 2a31fa12a..681a34429 100644 --- a/index.html +++ b/index.html @@ -253,7 +253,7 @@
- diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js index 463538bd9..457385112 100644 --- a/modules/UI/Feedback.js +++ b/modules/UI/Feedback.js @@ -1,4 +1,5 @@ /* global $, APP, config, interfaceConfig */ +import UIEvents from "../../service/UI/UIEvents"; /* * Created by Yana Stamcheva on 2/10/15. @@ -60,6 +61,14 @@ var constructDetailedFeedbackHtml = function() { */ var feedbackWindowCallback = null; +/** + * Shows / hides the feedback button. + * @private + */ +function _toggleFeedbackIcon() { + $('#feedbackButtonDiv').toggleClass("hidden"); +} + /** * Defines all methods in connection to the Feedback window. * @@ -73,17 +82,23 @@ var Feedback = { feedbackScore: -1, /** * Initialise the Feedback functionality. + * @param emitter the EventEmitter to associate with the Feedback. */ - init: function () { + init: function (emitter) { // CallStats is the way we send feedback, so we don't have to initialise // if callstats isn't enabled. if (!APP.conference.isCallstatsEnabled()) return; - - $("div.feedbackButton").css("display", "block"); + $("#feedbackButtonDiv").css("display", "block"); $("#feedbackButton").click(function (event) { Feedback.openFeedbackWindow(); }); + + // Show / hide the feedback button whenever the film strip is + // shown / hidden. + emitter.addListener(UIEvents.TOGGLE_FILM_STRIP, function () { + _toggleFeedbackIcon(); + }); }, /** * Indicates if the feedback functionality is enabled. diff --git a/modules/UI/UI.js b/modules/UI/UI.js index a60c276aa..b993f60f7 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -252,7 +252,7 @@ UI.initConference = function () { Toolbar.checkAutoEnableDesktopSharing(); if(!interfaceConfig.filmStripOnly) { - Feedback.init(); + Feedback.init(eventEmitter); } // FollowMe attempts to copy certain aspects of the moderator's UI into the From 144dd85e989d942b568058f9a9c37f6d2d78859a Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 31 Mar 2016 17:58:02 -0500 Subject: [PATCH 23/31] Unifies all strings in Settings. --- lang/main.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lang/main.json b/lang/main.json index 7ac013ca4..bf769362e 100644 --- a/lang/main.json +++ b/lang/main.json @@ -82,10 +82,10 @@ "title": "SETTINGS", "update": "Update", "name": "Name", - "startAudioMuted": "start without audio", - "startVideoMuted": "start without video", - "selectCamera": "select camera", - "selectMic": "select microphone", + "startAudioMuted": "Start without audio", + "startVideoMuted": "Start without video", + "selectCamera": "Select camera", + "selectMic": "Select microphone", "followMe": "Enable follow me" }, "videothumbnail": From b031fdc3980b7a5c841ba5570fa9526f7c80aee6 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Thu, 31 Mar 2016 18:03:27 -0500 Subject: [PATCH 24/31] Adds libs/external_connect.js to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b10c8f0d6..69e79e789 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ node_modules deploy-local.sh libs/app.bundle.* libs/lib-jitsi-meet* +libs/external_connect.js all.css .remote-sync.json From 56690037d0e982fec3e8c6ff732669e7a4097b17 Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 1 Apr 2016 11:30:05 -0500 Subject: [PATCH 25/31] Fix large black stripe on load. --- css/chat.css | 1 + css/contact_list.css | 1 + css/settingsmenu.css | 1 + 3 files changed, 3 insertions(+) diff --git a/css/chat.css b/css/chat.css index 65a7a068f..7b4a0be89 100644 --- a/css/chat.css +++ b/css/chat.css @@ -1,4 +1,5 @@ #chatspace { + display: none; background-color: black; border-left: 1px solid #424242; } diff --git a/css/contact_list.css b/css/contact_list.css index 7dab8a523..51d6650e7 100644 --- a/css/contact_list.css +++ b/css/contact_list.css @@ -1,4 +1,5 @@ #contactlist { + display: none; background-color: black; cursor: default; } diff --git a/css/settingsmenu.css b/css/settingsmenu.css index 1cd63203e..0d7d59467 100644 --- a/css/settingsmenu.css +++ b/css/settingsmenu.css @@ -1,4 +1,5 @@ #settingsmenu { + display: none; background: black; color: #00ccff; overflow-y: auto; From 8d77088f6d86acd837a32c51a83a4e9227bf64ee Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Fri, 1 Apr 2016 14:44:25 -0500 Subject: [PATCH 26/31] Stores measured times and exposes them --- app.js | 13 ++++++++++--- conference.js | 2 +- index.html | 5 ++++- modules/UI/videolayout/SmallVideo.js | 4 +++- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index cee8118e8..277cbb161 100644 --- a/app.js +++ b/app.js @@ -51,9 +51,14 @@ const APP = { status: "initialized", handler: null }, + // Used for automated performance tests + performanceTimes: { + "index.loaded": window.indexLoadedTime + }, UI, settings, conference, + connection: null, API, init () { this.keyboardshortcut = @@ -98,8 +103,9 @@ function obtainConfigAndInit() { // Get config result callback function(success, error) { if (success) { - console.log("(TIME) configuration fetched:\t", - window.performance.now()); + var now = APP.performanceTimes["configuration.fetched"] = + window.performance.now(); + console.log("(TIME) configuration fetched:\t", now); init(); } else { // Show obtain config error, @@ -118,7 +124,8 @@ function obtainConfigAndInit() { $(document).ready(function () { - console.log("(TIME) document ready:\t", window.performance.now()); + var now = APP.performanceTimes["document.ready"] = window.performance.now(); + console.log("(TIME) document ready:\t", now); URLProcessor.setConfigParametersFromUrl(); APP.init(); diff --git a/conference.js b/conference.js index bc8775e81..c68857899 100644 --- a/conference.js +++ b/conference.js @@ -328,7 +328,7 @@ export default { ]); }).then(([tracks, con]) => { console.log('initialized with %s local tracks', tracks.length); - connection = con; + APP.connection = connection = con; this._createRoom(tracks); this.isDesktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled(); diff --git a/index.html b/index.html index 681a34429..47f73cca2 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,10 @@ - + diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index e20275b47..f40d2e9be 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -141,8 +141,10 @@ SmallVideo.createStreamElement = function (stream) { element.id = SmallVideo.getStreamElementID(stream); element.onplay = function () { + var now = APP.performanceTimes["video.render"] + = window.performance.now(); console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t", - window.performance.now()); + now); }; element.oncontextmenu = function () { return false; }; From 9a984b7f84483775d75c6796c8d2d3941c1746d9 Mon Sep 17 00:00:00 2001 From: damencho Date: Thu, 31 Mar 2016 15:32:13 -0500 Subject: [PATCH 27/31] Instantly update volume changes. --- modules/UI/shared_video/SharedVideo.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 26996e397..a5a3b3316 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -119,6 +119,10 @@ export default class SharedVideoManager { self.player = event.target; + // add listener for volume changes + self.player.addEventListener( + "onVolumeChange", "onVolumeChange"); + if(self.initialAttributes) { self.processAttributes( @@ -133,6 +137,17 @@ export default class SharedVideoManager { } }; + /** + * Gets notified for volume state changed. + * @param event + */ + window.onVolumeChange = function (event) { + if(!self.player) + return; + + self.updateCheck(); + }; + window.onPlayerReady = function(event) { let player = event.target; // do not relay on autoplay as it is not sending all of the events From bede8feccc38350e7f7a7f8bf5cbe1b597ca2950 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 1 Apr 2016 14:53:11 -0500 Subject: [PATCH 28/31] Mutes local video when shared video is playing and mutes shared video if user wants to talk. --- modules/UI/shared_video/SharedVideo.js | 77 ++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index a5a3b3316..93a9023dc 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -73,6 +73,10 @@ export default class SharedVideoManager { // the owner of the video this.from = id; + //listen for local audio mute events + this.localAudioMutedListener = this.localAudioMuted.bind(this); + this.emitter.on(UIEvents.AUDIO_MUTED, this.localAudioMutedListener); + // This code loads the IFrame Player API code asynchronously. var tag = document.createElement('script'); @@ -110,7 +114,8 @@ export default class SharedVideoManager { 'onStateChange': onPlayerStateChange, 'onError': onPlayerError } - }); + }).addEventListener(// add listener for volume changes + "onVolumeChange", "onVolumeChange"); }; window.onPlayerStateChange = function(event) { @@ -119,10 +124,6 @@ export default class SharedVideoManager { self.player = event.target; - // add listener for volume changes - self.player.addEventListener( - "onVolumeChange", "onVolumeChange"); - if(self.initialAttributes) { self.processAttributes( @@ -142,10 +143,14 @@ export default class SharedVideoManager { * @param event */ window.onVolumeChange = function (event) { - if(!self.player) - return; - self.updateCheck(); + + // let's check, if player is not muted lets mute locally + if(event.data.volume > 0 && !event.data.muted + && !APP.conference.isLocalAudioMuted()){ + self.emitter.emit(UIEvents.AUDIO_MUTED, true); + self.notifyUserComfortableMicMute(true); + } }; window.onPlayerReady = function(event) { @@ -199,9 +204,11 @@ export default class SharedVideoManager { // lets check the volume if (attributes.volume !== undefined && - player.getVolume() != attributes.volume) { + player.getVolume() != attributes.volume + && APP.conference.isLocalAudioMuted()) { player.setVolume(attributes.volume); console.info("Player change of volume:" + attributes.volume); + this.notifyUserComfortableVideoMute(false); } if(playerPaused) @@ -323,6 +330,10 @@ export default class SharedVideoManager { this.intervalId = null; } + this.emitter.removeListener(UIEvents.AUDIO_MUTED, + this.localAudioMutedListener); + this.localAudioMutedListener = null; + VideoLayout.removeParticipantContainer(this.url); VideoLayout.showLargeVideoContainer(SHARED_VIDEO_CONTAINER_TYPE, false) @@ -333,7 +344,7 @@ export default class SharedVideoManager { if(this.player) { this.player.destroy(); this.player = null; - }// + } // if there is an error in player, remove that instance else if (this.errorInPlayer) { this.errorInPlayer.destroy(); this.errorInPlayer = null; @@ -344,6 +355,52 @@ export default class SharedVideoManager { this.isSharedVideoShown = false; this.initialAttributes = null; } + + /** + * Receives events for local audio mute/unmute by local user. + * @param muted boolena whether it is muted or not. + */ + localAudioMuted (muted) { + if(!this.player) + return; + + if(muted) + return; + + // if we are un-muting and player is not muted, lets muted + // to not pollute the conference + if(this.player.getVolume() > 0 || !this.player.isMuted()){ + this.player.setVolume(0); + this.notifyUserComfortableVideoMute(true); + } + } + + /** + * Notifies user for muting its audio due to video is unmuted. + * @param show boolean, show or hide the notification + */ + notifyUserComfortableMicMute (show) { + if(show) { + this.notifyUserComfortableVideoMute(false); + console.log("Your audio was muted to enjoy the video"); + } + else + console.log("Hide notification local audio muted"); + } + + /** + * Notifies user for muting the video due to audio is unmuted. + * @param show boolean, show or hide the notification + */ + notifyUserComfortableVideoMute (show) { + if(show) { + this.notifyUserComfortableMicMute(false); + console.log( + "Your shared video was muted in order to speak freely!"); + } + else + console.log("Hide notification share video muted"); + } } /** From a3bfce4c407d9c94280c9a9f5c196dbb60ec74b5 Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 1 Apr 2016 16:26:30 -0500 Subject: [PATCH 29/31] Set follow-me initial state after enable --- modules/FollowMe.js | 54 ++++++++++++++++++++++++++++++++++++++++----- modules/UI/UI.js | 8 +++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/modules/FollowMe.js b/modules/FollowMe.js index 0626d1bd0..a4b18f812 100644 --- a/modules/FollowMe.js +++ b/modules/FollowMe.js @@ -16,7 +16,6 @@ import UIEvents from '../service/UI/UIEvents'; import VideoLayout from './UI/videolayout/VideoLayout'; -import FilmStrip from './UI/videolayout/FilmStrip'; /** * The (name of the) command which transports the state (represented by @@ -137,6 +136,29 @@ class FollowMe { this._onFollowMeCommand.bind(this)); } + /** + * Sets the current state of all follow-me properties, which will fire a + * localPropertyChangeEvent and trigger a send of the follow-me command. + * @private + */ + _setFollowMeInitialState() { + this._filmStripToggled.bind(this, this._UI.isFilmStripVisible()); + + var pinnedId = VideoLayout.getPinnedId(); + var isPinned = false; + var smallVideo; + if (pinnedId) { + isPinned = true; + smallVideo = VideoLayout.getSmallVideo(pinnedId); + } + + this._nextOnStage(smallVideo, isPinned); + + this._sharedDocumentToggled + .bind(this, this._UI.getSharedDocumentManager().isVisible()); + + } + /** * Adds listeners for the UI states of the local participant which are * to be followed (by the remote participants). A non-moderator (very @@ -181,9 +203,10 @@ class FollowMe { * to disable it */ enableFollowMe (enable) { - this.isEnabled = enable; - if (this.isEnabled) + if (enable) { + this._setFollowMeInitialState(); this._addFollowMeListeners(); + } else this._removeFollowMeListeners(); } @@ -211,7 +234,7 @@ class FollowMe { } /** - * Changes the nextOnPage property value. + * Changes the nextOnStage property value. * * @param smallVideo the {SmallVideo} that was pinned or unpinned * @param isPinned indicates if the given {SmallVideo} was pinned or @@ -295,6 +318,13 @@ class FollowMe { this._onSharedDocumentVisible(attributes.sharedDocumentVisible); } + /** + * Process a film strip open / close event received from FOLLOW-ME + * command. + * @param filmStripVisible indicates if the film strip has been shown or + * hidden + * @private + */ _onFilmStripVisible(filmStripVisible) { if (typeof filmStripVisible !== 'undefined') { // XXX The Command(s) API doesn't preserve the types (of @@ -307,11 +337,18 @@ class FollowMe { // eventEmitter as a public field. I'm not sure at the time of this // writing whether calling UI.toggleFilmStrip() is acceptable (from // a design standpoint) either. - if (filmStripVisible !== FilmStrip.isFilmStripVisible()) + if (filmStripVisible !== this._UI.isFilmStripVisible()) this._UI.eventEmitter.emit(UIEvents.TOGGLE_FILM_STRIP); } } + /** + * Process the id received from a FOLLOW-ME command. + * @param id the identifier of the next participant to show on stage or + * undefined if we're clearing the stage (we're unpining all pined and we + * rely on dominant speaker events) + * @private + */ _onNextOnStage(id) { var clickId = null; var pin; @@ -328,6 +365,13 @@ class FollowMe { this._pinVideoThumbnailById(clickId, pin); } + /** + * Process a shared document open / close event received from FOLLOW-ME + * command. + * @param sharedDocumentVisible indicates if the shared document has been + * opened or closed + * @private + */ _onSharedDocumentVisible(sharedDocumentVisible) { if (typeof sharedDocumentVisible !== 'undefined') { // XXX The Command(s) API doesn't preserve the types (of diff --git a/modules/UI/UI.js b/modules/UI/UI.js index b993f60f7..97cdddf82 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -627,6 +627,14 @@ UI.toggleFilmStrip = function () { self.toggleFilmStrip.apply(self, arguments); }; +/** + * Indicates if the film strip is currently visible or not. + * @returns {true} if the film strip is currently visible, otherwise + */ +UI.isFilmStripVisible = function () { + return FilmStrip.isFilmStripVisible(); +}; + /** * Toggles chat panel. */ From 7dad981112565a6f072adc6222d317180a489a30 Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 1 Apr 2016 17:08:35 -0500 Subject: [PATCH 30/31] Follow seeking while player is paused. --- modules/UI/shared_video/SharedVideo.js | 29 +++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index 93a9023dc..fb7875442 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -98,7 +98,7 @@ export default class SharedVideoManager { window.onYouTubeIframeAPIReady = function() { self.isPlayerAPILoaded = true; let showControls = APP.conference.isLocalId(self.from) ? 1 : 0; - new YT.Player('sharedVideoIFrame', { + let p = new YT.Player('sharedVideoIFrame', { height: '100%', width: '100%', videoId: self.url, @@ -114,8 +114,19 @@ export default class SharedVideoManager { 'onStateChange': onPlayerStateChange, 'onError': onPlayerError } - }).addEventListener(// add listener for volume changes + }); + + // add listener for volume changes + p.addEventListener( "onVolumeChange", "onVolumeChange"); + + if (APP.conference.isLocalId(self.from)){ + // adds progress listener that will be firing events + // while we are paused and we change the progress of the + // video (seeking forward or backward on the video) + p.addEventListener( + "onVideoProgress", "onVideoProgress"); + } }; window.onPlayerStateChange = function(event) { @@ -138,6 +149,17 @@ export default class SharedVideoManager { } }; + /** + * Track player progress while paused. + * @param event + */ + window.onVideoProgress = function (event) { + let state = event.target.getPlayerState(); + if (state == YT.PlayerState.PAUSED) { + self.updateCheck(true); + } + }; + /** * Gets notified for volume state changed. * @param event @@ -218,7 +240,7 @@ export default class SharedVideoManager { // if its not paused, pause it player.pauseVideo(); - this.processTime(player, attributes, !playerPaused); + this.processTime(player, attributes, true); } else if (attributes.state == 'stop') { this.stopSharedVideo(this.from); } @@ -233,6 +255,7 @@ export default class SharedVideoManager { processTime (player, attributes, forceSeek) { if(forceSeek) { + console.info("Player seekTo:", attributes.time); player.seekTo(attributes.time); return; } From b949ffdda1b7bf7dbdb70b2854ac909f42d5ef7a Mon Sep 17 00:00:00 2001 From: damencho Date: Fri, 1 Apr 2016 17:23:30 -0500 Subject: [PATCH 31/31] Prevents users that are not sharing the video of pausing the video and leaves the control to the user sharing the video. --- modules/UI/shared_video/SharedVideo.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index fb7875442..a57a41fc3 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -189,6 +189,12 @@ export default class SharedVideoManager { self.sharedVideo = new SharedVideoContainer( {url, iframe, player}); + //prevents pausing participants not sharing the video + // to pause the video + if (!APP.conference.isLocalId(self.from)) { + $("#sharedVideo").css("pointer-events","none"); + } + VideoLayout.addLargeVideoContainer( SHARED_VIDEO_CONTAINER_TYPE, self.sharedVideo); VideoLayout.handleVideoThumbClicked(self.url); @@ -372,6 +378,9 @@ export default class SharedVideoManager { this.errorInPlayer.destroy(); this.errorInPlayer = null; } + // revert to original behavior (prevents pausing + // for participants not sharing the video to pause it) + $("#sharedVideo").css("pointer-events","auto"); }); this.url = null;