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 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..277cbb161 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,43 +23,42 @@ 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 + }, + // Used for automated performance tests + performanceTimes: { + "index.loaded": window.indexLoadedTime + }, UI, settings, conference, + connection: null, API, init () { this.keyboardshortcut = @@ -104,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, @@ -124,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 90d8bc007..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(); @@ -567,7 +567,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"; @@ -848,7 +848,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; @@ -1008,15 +1009,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) => { @@ -1116,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/connection.js b/connection.js index b6348a774..aa0ca0f7c 100644 --- a/connection.js +++ b/connection.js @@ -5,6 +5,42 @@ 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) { + 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; + } + + 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 +54,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 +87,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..b4b9fbc58 --- /dev/null +++ b/connection_optimization/do_external_connect.js @@ -0,0 +1,80 @@ +/* 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){ + 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 = { + 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/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/main.css b/css/main.css index 463018499..1a0b013f4 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 { @@ -157,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; @@ -248,19 +224,24 @@ form { position: absolute; } -div.feedbackButton { - background-color: rgba(0, 0, 0, 0.5); +#feedbackButtonDiv { + display: none; + position: absolute; + background-color: rgba(0,0,0,.50); border-radius: 50%; bottom: 0; font-size: 1em; height: 3em; left: 0; overflow: hidden; - position: absolute; text-align: center; - transition: all 0.2s ease-in-out 0s; width: 3em; z-index: 100; + transition: all 2s ease-in-out; +} + +#feedbackButtonDiv.hidden { + bottom: -246px; } div.feedbackButton:hover { @@ -299,7 +280,7 @@ div.feedbackButton:hover { } .active { - color: #00ccff; + background-color: #00ccff; } .bottomToolbar_span>span { 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; diff --git a/css/videolayout_default.css b/css/videolayout_default.css index 1e02c4aa4..0f09d60ad 100644 --- a/css/videolayout_default.css +++ b/css/videolayout_default.css @@ -504,3 +504,33 @@ 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: 2px; + -webkit-transition: all 2s 2s linear; + transition: all 2s 2s linear; +} + +.moveToCorner { + top: 5px; + right: 50px; /*leave free space for the HD label*/ + margin-right: 0px; + margin-left: auto; + background: rgba(0,0,0,.3); + color: rgba(255,255,255,.5); +} 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 848e8fe88..09ebffd3b 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,17 @@ + + + + + + + + @@ -14,11 +25,6 @@ - - - - - @@ -138,14 +144,14 @@ - + - + @@ -176,6 +182,7 @@ HD +
@@ -271,7 +278,7 @@
-
+
diff --git a/lang/main.json b/lang/main.json index 24199d190..bf769362e 100644 --- a/lang/main.json +++ b/lang/main.json @@ -51,12 +51,11 @@ "mute": "Mute / Unmute", "videomute": "Start / stop camera", "authenticate": "Authenticate", - "record": "Record", "lock": "Lock / unlock room", "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", @@ -83,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": @@ -179,6 +178,7 @@ "joinAgain": "Join again", "Share": "Share", "Save": "Save", + "recording": "Recording", "recordingToken": "Enter recording token", "Dial": "Dial", "sipMsg": "Enter SIP number", @@ -206,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": { @@ -254,8 +261,20 @@ }, "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", + "buttonTooltip": "Start / stop recording" + }, + "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", + "buttonTooltip": "Start / stop live stream", + "streamIdRequired": "Please fill in the stream id in order to launch the live streaming." } } diff --git a/modules/FollowMe.js b/modules/FollowMe.js index 0d5086809..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 @@ -25,6 +24,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 +120,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 @@ -127,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 @@ -171,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(); } @@ -201,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 @@ -265,6 +298,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)) @@ -284,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 @@ -296,25 +337,41 @@ 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()) - this._UI.eventEmitter.emit( - UIEvents.TOGGLE_FILM_STRIP, - filmStripVisible); + 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; - 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); } + /** + * 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 @@ -328,6 +385,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/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 4e4a9d99f..97cdddf82 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"; @@ -251,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 @@ -363,12 +364,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); @@ -566,15 +571,15 @@ 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(); } }; @@ -622,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. */ @@ -977,37 +990,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 () { @@ -1139,7 +1123,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/recording/Recording.js b/modules/UI/recording/Recording.js new file mode 100644 index 000000000..38e65504e --- /dev/null +++ b/modules/UI/recording/Recording.js @@ -0,0 +1,357 @@ +/* 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'; + +/** + * Indicates if the recording button should be enabled. + * + * @returns {boolean} {true} if the + * @private + */ +function _isRecordingButtonEnabled() { + return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1 + && config.enableRecording; +} + +/** + * Request live stream token from the user. + * @returns {Promise} + */ +function _requestLiveStreamId() { + 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) { + let dialog = APP.UI.messageHandler.openDialogWithStates({ + state0: { + html: + `

${msg}

+ `, + 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; + } + } + }, + + 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'); + } + } + } + }); + }); +} + +/** + * 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 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); + }, + + 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"; + this.recordingButtonTooltip = "liveStreaming.buttonTooltip"; + } + else { + this.baseClass = "icon-recEnable"; + this.recordingOnKey = "recording.on"; + 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 () { + switch (self.currentState) { + case Status.ON: + case Status.PENDING: { + _showStopRecordingPrompt(recordingType).then(() => + self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED)); + break; + } + case Status.AVAILABLE: + case Status.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) { + // 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; + + 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 === Status.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 === Status.OFF + || recordingState === Status.UNAVAILABLE) { + + // We don't want to do any changes if this is + // an availability change. + if (this.currentState !== Status.ON + && this.currentState !== Status.PENDING) + return; + + buttonSelector.removeClass(this.baseClass + " active"); + buttonSelector.addClass(this.baseClass); + + moveToCorner(labelSelector, false); + let messageKey; + if (this.currentState === Status.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 === Status.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; + + // 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 + // 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; diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index f2b578a63..a57a41fc3 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", @@ -64,12 +65,18 @@ export default class SharedVideoManager { if (this.isSharedVideoShown) return; + this.isSharedVideoShown = true; + // the video url this.url = url; // 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'); @@ -82,7 +89,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) @@ -91,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, @@ -108,6 +115,18 @@ export default class SharedVideoManager { 'onError': onPlayerError } }); + + // 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) { @@ -130,6 +149,32 @@ 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 + */ + window.onVolumeChange = function (event) { + 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) { let player = event.target; // do not relay on autoplay as it is not sending all of the events @@ -144,12 +189,16 @@ 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); - 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)) { @@ -157,17 +206,12 @@ 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) { - console.error("Error in the player:" + event.data); + console.error("Error in the player:", event.data); + // store the error player, so we can remove it + self.errorInPlayer = event.target; }; } @@ -184,13 +228,15 @@ 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 && - 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) @@ -200,7 +246,9 @@ export default class SharedVideoManager { // if its not paused, pause it player.pauseVideo(); - this.processTime(player, attributes); + this.processTime(player, attributes, true); + } else if (attributes.state == 'stop') { + this.stopSharedVideo(this.from); } } @@ -208,9 +256,16 @@ 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) { + console.info("Player seekTo:", attributes.time); + player.seekTo(attributes.time); + return; + } + // check received time and current time let currentPosition = player.getCurrentTime(); let diff = Math.abs(attributes.time - currentPosition); @@ -230,8 +285,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(); @@ -281,18 +338,31 @@ 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){ + // if there is no error in the player till now, + // store the initial attributes + if (!this.errorInPlayer) { + this.initialAttributes = attributes; + return; + } + } + if(this.intervalId) { clearInterval(this.intervalId); 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) @@ -300,12 +370,68 @@ 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; + } // if there is an error in player, remove that instance + else if (this.errorInPlayer) { + 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; 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"); } } @@ -333,6 +459,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 +467,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; diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 687e91783..03e76ba67 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 () { if (UIUtil.isButtonEnabled('sharedvideo') @@ -298,14 +241,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 () { @@ -383,10 +318,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/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"); 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/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 6a878e4d1..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; }; @@ -315,11 +317,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); } }; 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/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"; 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", 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; +}