From e43c5ab54c37bbfcb6dfea5ea4c632007925f208 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 7 Apr 2016 12:08:00 -0500 Subject: [PATCH 01/17] Add custom-role to presence and special view for Recorders --- conference.js | 142 ++++++++++++++------------ modules/UI/Feedback.js | 32 +++++- modules/UI/UI.js | 17 ++- modules/UI/toolbars/BottomToolbar.js | 21 +++- modules/UI/toolbars/Toolbar.js | 76 +++++++++++++- modules/UI/toolbars/ToolbarToggler.js | 50 ++++----- modules/UI/videolayout/SmallVideo.js | 18 ++++ modules/UI/videolayout/VideoLayout.js | 19 ++++ 8 files changed, 265 insertions(+), 110 deletions(-) diff --git a/conference.js b/conference.js index c68857899..16df73728 100644 --- a/conference.js +++ b/conference.js @@ -21,16 +21,6 @@ const TrackErrors = JitsiMeetJS.errors.track; let room, connection, localAudio, localVideo, roomLocker; -/** - * Known custom conference commands. - */ -const Commands = { - CONNECTION_QUALITY: "stats", - EMAIL: "email", - ETHERPAD: "etherpad", - SHARED_VIDEO: "shared-video" -}; - import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/LargeVideo"; /** @@ -52,10 +42,11 @@ function connect(roomName) { /** * Share email with other users. + * @param emailCommand the email command * @param {string} email new email */ -function sendEmail (email) { - room.sendCommand(Commands.EMAIL, { +function sendEmail (emailCommand, email) { + room.sendCommand(emailCommand, { value: email, attributes: { id: room.myUserId() @@ -494,6 +485,61 @@ export default { getLogs () { return room.getLogs(); }, + + /** + * Exposes a Command(s) API on this instance. It is necessitated by (1) the + * desire to keep room private to this instance and (2) the need of other + * modules to send and receive commands to and from participants. + * Eventually, this instance remains in control with respect to the + * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's + * JitsiConference) is to be used in the implementation of the Command(s) + * API of this instance. + */ + commands: { + /** + * Known custom conference commands. + */ + defaults: { + CONNECTION_QUALITY: "stats", + EMAIL: "email", + ETHERPAD: "etherpad", + SHARED_VIDEO: "shared-video", + CUSTOM_ROLE: "custom-role" + }, + /** + * Receives notifications from other participants about commands aka + * custom events (sent by sendCommand or sendCommandOnce methods). + * @param command {String} the name of the command + * @param handler {Function} handler for the command + */ + addCommandListener () { + room.addCommandListener.apply(room, arguments); + }, + /** + * Removes command. + * @param name {String} the name of the command. + */ + removeCommand () { + room.removeCommand.apply(room, arguments); + }, + /** + * Sends command. + * @param name {String} the name of the command. + * @param values {Object} with keys and values that will be sent. + */ + sendCommand () { + room.sendCommand.apply(room, arguments); + }, + /** + * Sends command one time. + * @param name {String} the name of the command. + * @param values {Object} with keys and values that will be sent. + */ + sendCommandOnce () { + room.sendCommandOnce.apply(room, arguments); + } + }, + _createRoom (localTracks) { room = connection.initJitsiConference(APP.conference.roomName, this._getConferenceOptions()); @@ -509,7 +555,7 @@ export default { this._room = room; // FIXME do not use this let email = APP.settings.getEmail(); - email && sendEmail(email); + email && sendEmail(this.commands.defaults.EMAIL, email); let nick = APP.settings.getDisplayName(); if (config.useNicks && !nick) { @@ -521,50 +567,6 @@ export default { this._setupListeners(); }, - /** - * Exposes a Command(s) API on this instance. It is necessitated by (1) the - * desire to keep room private to this instance and (2) the need of other - * modules to send and receive commands to and from participants. - * Eventually, this instance remains in control with respect to the - * decision whether the Command(s) API of room (i.e. lib-jitsi-meet's - * JitsiConference) is to be used in the implementation of the Command(s) - * API of this instance. - */ - commands: { - /** - * Receives notifications from other participants about commands aka - * custom events (sent by sendCommand or sendCommandOnce methods). - * @param command {String} the name of the command - * @param handler {Function} handler for the command - */ - addCommandListener () { - room.addCommandListener.apply(room, arguments); - }, - /** - * Removes command. - * @param name {String} the name of the command. - */ - removeCommand () { - room.removeCommand.apply(room, arguments); - }, - /** - * Sends command. - * @param name {String} the name of the command. - * @param values {Object} with keys and values that will be sent. - */ - sendCommand () { - room.sendCommand.apply(room, arguments); - }, - /** - * Sends command one time. - * @param name {String} the name of the command. - * @param values {Object} with keys and values that will be sent. - */ - sendCommandOnce () { - room.sendCommandOnce.apply(room, arguments); - }, - }, - _getConferenceOptions() { let options = config; if(config.enableRecording && !config.recordingType) { @@ -721,6 +723,8 @@ export default { * Setup interaction between conference and UI. */ _setupListeners () { + var self = this; + // add local streams when joined to the conference room.on(ConferenceEvents.CONFERENCE_JOINED, () => { APP.UI.mucJoined(); @@ -903,7 +907,8 @@ export default { APP.UI.updateLocalStats(percent, stats); // send local stats to other users - room.sendCommandOnce(Commands.CONNECTION_QUALITY, { + room.sendCommandOnce(self.commands.defaults.CONNECTION_QUALITY, + { children: ConnectionQuality.convertToMUCStats(stats), attributes: { xmlns: 'http://jitsi.org/jitmeet/stats' @@ -913,8 +918,9 @@ export default { ); // listen to remote stats - room.addCommandListener(Commands.CONNECTION_QUALITY,(values, from) => { - ConnectionQuality.updateRemoteStats(from, values); + room.addCommandListener(self.commands.defaults.CONNECTION_QUALITY, + (values, from) => { + ConnectionQuality.updateRemoteStats(from, values); }); ConnectionQuality.addListener(CQEvents.REMOTESTATS_UPDATED, @@ -922,7 +928,7 @@ export default { APP.UI.updateRemoteStats(id, percent, stats); }); - room.addCommandListener(Commands.ETHERPAD, ({value}) => { + room.addCommandListener(self.commands.defaults.ETHERPAD, ({value}) => { APP.UI.initEtherpad(value); }); @@ -935,9 +941,9 @@ export default { APP.settings.setEmail(email); APP.UI.setUserAvatar(room.myUserId(), email); - sendEmail(email); + sendEmail(self.commands.defaults.EMAIL, email); }); - room.addCommandListener(Commands.EMAIL, (data) => { + room.addCommandListener(self.commands.defaults.EMAIL, (data) => { APP.UI.setUserAvatar(data.attributes.id, data.value); }); @@ -1082,8 +1088,8 @@ export default { // send start and stop commands once, and remove any updates // that had left if (state === 'stop' || state === 'start' || state === 'playing') { - room.removeCommand(Commands.SHARED_VIDEO); - room.sendCommandOnce(Commands.SHARED_VIDEO, { + room.removeCommand(self.commands.defaults.SHARED_VIDEO); + room.sendCommandOnce(self.commands.defaults.SHARED_VIDEO, { value: url, attributes: { state: state, @@ -1095,8 +1101,8 @@ export default { else { // in case of paused, in order to allow late users to join // paused - room.removeCommand(Commands.SHARED_VIDEO); - room.sendCommand(Commands.SHARED_VIDEO, { + room.removeCommand(self.commands.defaults.SHARED_VIDEO); + room.sendCommand(self.commands.defaults.SHARED_VIDEO, { value: url, attributes: { state: state, @@ -1107,7 +1113,7 @@ export default { } }); room.addCommandListener( - Commands.SHARED_VIDEO, ({value, attributes}, id) => { + self.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => { if (attributes.state === 'stop') { APP.UI.stopSharedVideo(id, attributes); diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js index 457385112..92f79bc37 100644 --- a/modules/UI/Feedback.js +++ b/modules/UI/Feedback.js @@ -69,6 +69,21 @@ function _toggleFeedbackIcon() { $('#feedbackButtonDiv').toggleClass("hidden"); } +/** + * Shows / hides the feedback button. + * @param {show} set to {true} to show the feedback button or to {false} + * to hide it + * @private + */ +function _showFeedbackButton (show) { + var feedbackButton = $("#feedbackButtonDiv"); + + if (show) + feedbackButton.css("display", "block"); + else + feedbackButton.css("display", "none"); +} + /** * Defines all methods in connection to the Feedback window. * @@ -85,11 +100,15 @@ var Feedback = { * @param emitter the EventEmitter to associate with the Feedback. */ init: function (emitter) { + // Initialise to enabled. + this.enabled = true; + // CallStats is the way we send feedback, so we don't have to initialise // if callstats isn't enabled. if (!APP.conference.isCallstatsEnabled()) return; - $("#feedbackButtonDiv").css("display", "block"); + + _showFeedbackButton(true); $("#feedbackButton").click(function (event) { Feedback.openFeedbackWindow(); }); @@ -100,13 +119,22 @@ var Feedback = { _toggleFeedbackIcon(); }); }, + /** + * Enables/ disabled the feedback feature. + */ + enableFeedback: function (enable) { + if (this.enabled !== enable) + _showFeedbackButton(enable); + this.enabled = enable; + }, + /** * Indicates if the feedback functionality is enabled. * * @return true if the feedback functionality is enabled, false otherwise. */ isEnabled: function() { - return APP.conference.isCallstatsEnabled(); + return this.enabled && APP.conference.isCallstatsEnabled(); }, /** * Opens the feedback window. diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 97cdddf82..d9d46deb4 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -29,6 +29,8 @@ var JitsiPopover = require("./util/JitsiPopover"); var Feedback = require("./Feedback"); import FollowMe from "../FollowMe"; +import Recorder from "../recorder/Recorder"; + var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; @@ -42,7 +44,8 @@ let followMeHandler; * Prompt user for nickname. */ function promptDisplayName() { - let nickRequiredMsg = APP.translation.translateString("dialog.displayNameRequired"); + let nickRequiredMsg + = APP.translation.translateString("dialog.displayNameRequired"); let defaultNickMsg = APP.translation.translateString( "defaultNickname", {name: "Jane Pink"} ); @@ -110,8 +113,8 @@ function setupToolbars() { * @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API */ function toggleFullScreen () { - let isNotFullScreen = !document.fullscreenElement && // alternative standard method - + // alternative standard method + let isNotFullScreen = !document.fullscreenElement && !document.mozFullScreenElement && // current working methods !document.webkitFullscreenElement && !document.msFullscreenElement; @@ -124,7 +127,8 @@ function toggleFullScreen () { } else if (document.documentElement.mozRequestFullScreen) { document.documentElement.mozRequestFullScreen(); } else if (document.documentElement.webkitRequestFullscreen) { - document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + document.documentElement + .webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } } else { if (document.exitFullscreen) { @@ -238,6 +242,11 @@ UI.initConference = function () { //if local role changes buttons state will be again updated UI.updateLocalRole(false); + // Initialise the recorder handler. We're doing this explicitly before + // calling showToolbar, because the recorder may want to disable all + // toolbars. + new Recorder(APP.conference, UI); + // Once we've joined the muc show the toolbar ToolbarToggler.showToolbar(); diff --git a/modules/UI/toolbars/BottomToolbar.js b/modules/UI/toolbars/BottomToolbar.js index eae97444a..4ef781203 100644 --- a/modules/UI/toolbars/BottomToolbar.js +++ b/modules/UI/toolbars/BottomToolbar.js @@ -12,8 +12,27 @@ const defaultBottomToolbarButtons = { const BottomToolbar = { init () { this.toolbar = $('#bottomToolbar'); - }, + // The bottom toolbar is enabled by default. + this.enabled = true; + }, + /** + * Enables / disables the bottom toolbar. + * @param {e} set to {true} to enable the bottom toolbar or {false} + * to disable it + */ + enable (e) { + this.enabled = e; + if (!e && this.isVisible()) + this.hide(false); + }, + /** + * Indicates if the bottom toolbar is currently enabled. + * @return {this.enabled} + */ + isEnabled() { + return this.enabled; + }, setupListeners (emitter) { UIUtil.hideDisabledButtons(defaultBottomToolbarButtons); diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 03e76ba67..7a95487ee 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -171,6 +171,9 @@ function showSipNumberInput () { const Toolbar = { init (eventEmitter) { emitter = eventEmitter; + // The toolbar is enabled by default. + this.enabled = true; + this.toolbarSelector = $("#header"); UIUtil.hideDisabledButtons(defaultToolbarButtons); @@ -178,14 +181,31 @@ const Toolbar = { buttonId => $(`#${buttonId}`).click(buttonHandlers[buttonId]) ); }, - + /** + * Enables / disables the toolbar. + * @param {e} set to {true} to enable the toolbar or {false} + * to disable it + */ + enable (e) { + this.enabled = e; + if (!e && this.isVisible()) + this.hide(false); + }, + /** + * Indicates if the bottom toolbar is currently enabled. + * @return {this.enabled} + */ + isEnabled() { + return this.enabled; + }, /** * Updates the room invite url. */ updateRoomUrl (newRoomUrl) { roomUrl = newRoomUrl; - // If the invite dialog has been already opened we update the information. + // If the invite dialog has been already opened we update the + // information. let inviteLink = document.getElementById('inviteLinkRef'); if (inviteLink) { inviteLink.value = roomUrl; @@ -244,14 +264,16 @@ const Toolbar = { // checks whether desktop sharing is enabled and whether // we have params to start automatically sharing checkAutoEnableDesktopSharing () { - if (UIUtil.isButtonEnabled('desktop') && config.autoEnableDesktopSharing) { + if (UIUtil.isButtonEnabled('desktop') + && config.autoEnableDesktopSharing) { emitter.emit(UIEvents.TOGGLE_SCREENSHARING); } }, // Shows or hides SIP calls button showSipCallButton (show) { - if (APP.conference.sipGatewayEnabled() && UIUtil.isButtonEnabled('sip') && show) { + if (APP.conference.sipGatewayEnabled() + && UIUtil.isButtonEnabled('sip') && show) { $('#toolbar_button_sip').css({display: "inline-block"}); } else { $('#toolbar_button_sip').css({display: "none"}); @@ -331,7 +353,51 @@ const Toolbar = { * @param {boolean} muted if icon should look like muted or not */ markAudioIconAsMuted (muted) { - $('#toolbar_button_mute').toggleClass("icon-microphone", !muted).toggleClass("icon-mic-disabled", muted); + $('#toolbar_button_mute').toggleClass("icon-microphone", + !muted).toggleClass("icon-mic-disabled", muted); + }, + + /** + * Indicates if the toolbar is currently hovered. + * @return {true} if the toolbar is currently hovered, {false} otherwise + */ + isHovered() { + this.toolbarSelector.find('*').each(function () { + let id = $(this).attr('id'); + if ($(`#${id}:hover`).length > 0) { + return true; + } + }); + if ($("#bottomToolbar:hover").length > 0) { + return true; + } + return false; + }, + + /** + * Returns true if this toolbar is currently visible, or false otherwise. + * @return true if currently visible, false - otherwise + */ + isVisible() { + return this.toolbarSelector.is(":visible"); + }, + + /** + * Hides the toolbar with animation or not depending on the animate + * parameter. + */ + hide() { + this.toolbarSelector.hide( + "slide", { direction: "up", duration: 300}); + }, + + /** + * Shows the toolbar with animation or not depending on the animate + * parameter. + */ + show() { + this.toolbarSelector.show( + "slide", { direction: "up", duration: 300}); } }; diff --git a/modules/UI/toolbars/ToolbarToggler.js b/modules/UI/toolbars/ToolbarToggler.js index ac13c6910..09bff508f 100644 --- a/modules/UI/toolbars/ToolbarToggler.js +++ b/modules/UI/toolbars/ToolbarToggler.js @@ -2,6 +2,7 @@ import UIUtil from '../util/UIUtil'; import BottomToolbar from './BottomToolbar'; +import Toolbar from './Toolbar'; import FilmStrip from '../videolayout/FilmStrip.js'; let toolbarTimeoutObject; @@ -16,10 +17,6 @@ function showDesktopSharingButton() { } } -function isToolbarVisible () { - return $('#header').is(':visible'); -} - /** * Hides the toolbar. */ @@ -28,25 +25,13 @@ function hideToolbar() { return; } - let header = $("#header"); - let isToolbarHover = false; - header.find('*').each(function () { - let id = $(this).attr('id'); - if ($(`#${id}:hover`).length > 0) { - isToolbarHover = true; - } - }); - if ($("#bottomToolbar:hover").length > 0) { - isToolbarHover = true; - } - clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; - if (isToolbarHover) { + if (Toolbar.isHovered()) { toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout); } else { - header.hide("slide", { direction: "up", duration: 300}); + Toolbar.hide(); $('#subject').animate({top: "-=40"}, 300); if (!FilmStrip.isFilmStripVisible()) { BottomToolbar.hide(true); @@ -59,18 +44,23 @@ const ToolbarToggler = { * Shows the main toolbar. */ showToolbar () { - // if we are a recorder we do not want to show the toolbar - if (interfaceConfig.filmStripOnly || config.iAmRecorder) { + if (interfaceConfig.filmStripOnly) { return; } - let header = $("#header"); - if (!header.is(':visible') || !BottomToolbar.isVisible()) { - header.show("slide", { direction: "up", duration: 300}); - $('#subject').animate({top: "+=40"}, 300); - if (!BottomToolbar.isVisible()) { - BottomToolbar.show(true); - } + var updateTimeout = false; + if (Toolbar.isEnabled() && !Toolbar.isVisible()) { + Toolbar.show(); + $('#subject').animate({top: "+=40"}, 300); + updateTimeout = true; + } + + if (BottomToolbar.isEnabled() && !BottomToolbar.isVisible()) { + BottomToolbar.show(true); + updateTimeout = true; + } + + if (updateTimeout) { if (toolbarTimeoutObject) { clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; @@ -89,13 +79,13 @@ const ToolbarToggler = { * @param isDock indicates what operation to perform */ dockToolbar (isDock) { - if (interfaceConfig.filmStripOnly) { + if (interfaceConfig.filmStripOnly || !Toolbar.isEnabled()) { return; } if (isDock) { // First make sure the toolbar is shown. - if (!isToolbarVisible()) { + if (!Toolbar.isVisible()) { this.showToolbar(); } @@ -103,7 +93,7 @@ const ToolbarToggler = { clearTimeout(toolbarTimeoutObject); toolbarTimeoutObject = null; } else { - if (isToolbarVisible()) { + if (Toolbar.isVisible()) { toolbarTimeoutObject = setTimeout(hideToolbar, toolbarTimeout); } else { this.showToolbar(); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index f40d2e9be..a809aaebb 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -50,7 +50,25 @@ SmallVideo.prototype.showDisplayName = function(isShow) { } }; +/** + * Enables / disables the device availability icons for this small video. + * @param {enable} set to {true} to enable and {false} to disable + */ +SmallVideo.prototype.enableDeviceAvailabilityIcons = function (enable) { + if (typeof enable === "undefined") + return; + + this.deviceAvailabilityIconsEnabled = enable; +}; + +/** + * Sets the device "non" availability icons. + * @param devices the devices, which will be checked for availability + */ SmallVideo.prototype.setDeviceAvailabilityIcons = function (devices) { + if (!this.deviceAvailabilityIconsEnabled) + return; + if(!this.container) return; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 772e05f37..7c94da9db 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -196,6 +196,25 @@ var VideoLayout = { video.setDeviceAvailabilityIcons(devices); }, + /** + * Enables/disables device availability icons for the given participant id. + * The default value is {true}. + * @param id the identifier of the participant + * @param enable {true} to enable device availability icons + */ + enableDeviceAvailabilityIcons (id, enable) { + let video; + if (APP.conference.isLocalId(id)) { + video = localVideoThumbnail; + } + else if (remoteVideos[id]) { + video = remoteVideos[id]; + } + + if (video) + video.enableDeviceAvailabilityIcons(enable); + }, + /** * Checks if removed video is currently displayed and tries to display * another one instead. From 99d9b16c13878bebb9f6f1392c9948ea6f29a665 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 7 Apr 2016 13:06:28 -0500 Subject: [PATCH 02/17] Add Recorder controller. --- modules/recorder/Recorder.js | 101 +++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 modules/recorder/Recorder.js diff --git a/modules/recorder/Recorder.js b/modules/recorder/Recorder.js new file mode 100644 index 000000000..e44438072 --- /dev/null +++ b/modules/recorder/Recorder.js @@ -0,0 +1,101 @@ +/* global config, APP */ +/* + * 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 VideoLayout from '../UI/videolayout/VideoLayout'; +import Feedback from '../UI/Feedback.js'; +import Toolbar from '../UI/toolbars/Toolbar'; +import BottomToolbar from '../UI/toolbars/BottomToolbar'; + +const _RECORDER_CUSTOM_ROLE = "recorder-role"; + +class Recorder { + /** + * Initializes a new {Recorder} instance. + * + * @param conference the {conference} which is to transport + * {Recorder}-related information between participants + * @param UI the {UI} which is the source (model/state) to be sent to + * remote participants if the local participant is the moderator or the + * destination (model/state) to receive from the remote moderator if the + * local participant is not the moderator + */ + constructor (conference, UI) { + var self = this; + this._conference = conference; + this._UI = UI; + + // If I am a recorder then I publish my recorder custom role to notify + // everyone. + if (config.iAmRecorder) { + VideoLayout.enableDeviceAvailabilityIcons(conference.localId, true); + this._publishMyRecorderRole(); + Feedback.enableFeedback(false); + Toolbar.enable(false); + BottomToolbar.enable(false); + } + + // Listen to "CUSTOM_ROLE" commands. + this._conference.commands.addCommandListener( + this._conference.commands.defaults.CUSTOM_ROLE, + this._onCustomRoleCommand.bind(this)); + } + + /** + * Publish the recorder custom role. + * @private + */ + _publishMyRecorderRole () { + var conference = this._conference; + + var commands = conference.commands; + + commands.removeCommand(commands.defaults.CUSTOM_ROLE); + var self = this; + commands.sendCommandOnce( + commands.defaults.CUSTOM_ROLE, + { + attributes: { + recorderRole: true + } + }); + } + + /** + * Notifies this instance about a &qout;Custom Role&qout; command (delivered + * by the Command(s) API of {this._conference}). + * + * @param attributes the attributes {Object} carried by the command + * @param id the identifier of the participant who issued the command. A + * notable idiosyncrasy of the Command(s) API to be mindful of here is that + * the command may be issued by the local participant. + */ + _onCustomRoleCommand ({ attributes }, id) { + // We require to know who issued the command because (1) only a + // moderator is allowed to send commands and (2) a command MUST be + // issued by a defined commander. + if (typeof id === 'undefined' + || this._conference.isLocalId(id) + || !attributes.recorderRole) + return; + + var isRecorder = (attributes.recorderRole == 'true'); + + if (isRecorder) + VideoLayout.enableDeviceAvailabilityIcons(id, isRecorder); + } +} + +export default Recorder; From 351775a1c02c39a50e45314831853f365d7c31bc Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 7 Apr 2016 13:09:19 -0500 Subject: [PATCH 03/17] Add jsdocs to the recording view manager --- modules/UI/recording/Recording.js | 41 +++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/modules/UI/recording/Recording.js b/modules/UI/recording/Recording.js index 38e65504e..23a660b29 100644 --- a/modules/UI/recording/Recording.js +++ b/modules/UI/recording/Recording.js @@ -133,6 +133,13 @@ function _requestRecordingToken () { }); } +/** + * Shows a prompt dialog to the user when they have toggled off the recording. + * + * @param recordingType the recording type + * @returns {Promise} + * @private + */ function _showStopRecordingPrompt (recordingType) { var title; var message; @@ -167,6 +174,12 @@ function _showStopRecordingPrompt (recordingType) { }); } +/** + * Moves the element given by {selector} to the top right corner of the screen. + * @param selector the selector for the element to move + * @param move {true} to move the element, {false} to move it back to its intial + * position + */ function moveToCorner(selector, move) { let moveToCornerClass = "moveToCorner"; @@ -176,6 +189,12 @@ function moveToCorner(selector, move) { selector.removeClass(moveToCornerClass); } +/** + * The status of the recorder. + * FIXME: Those constants should come from the library. + * @type {{ON: string, OFF: string, AVAILABLE: string, + * UNAVAILABLE: string, PENDING: string}} + */ var Status = { ON: "on", OFF: "off", @@ -184,6 +203,11 @@ var Status = { PENDING: "pending" }; +/** + * Manages the recording user interface and user experience. + * @type {{init, initRecordingButton, showRecordingButton, updateRecordingState, + * setRecordingButtonState, checkAutoRecord}} + */ var Recording = { /** * Initializes the recording UI. @@ -196,6 +220,9 @@ var Recording = { this.initRecordingButton(recordingType); }, + /** + * Initialise the recording button. + */ initRecordingButton(recordingType) { let selector = $('#toolbar_button_record'); @@ -261,7 +288,10 @@ var Recording = { }); }, - // Shows or hides the 'recording' button. + /** + * Shows or hides the 'recording' button. + * @param show {true} to show the recording button, {false} to hide it + */ showRecordingButton (show) { if (_isRecordingButtonEnabled() && show) { $('#toolbar_button_record').css({display: "inline-block"}); @@ -270,6 +300,10 @@ var Recording = { } }, + /** + * Updates the recording state UI. + * @param recordingState gives us the current recording state + */ updateRecordingState(recordingState) { // I'm the recorder, so I don't want to see any UI related to states. if (config.iAmRecorder) @@ -282,7 +316,10 @@ var Recording = { this.setRecordingButtonState(recordingState); }, - // Sets the state of the recording button + /** + * Sets the state of the recording button. + * @param recordingState gives us the current recording state + */ setRecordingButtonState (recordingState) { let buttonSelector = $('#toolbar_button_record'); let labelSelector = $('#recordingLabel'); From 562eba8d7855979afb6a52d87bbfb367bd9c2b42 Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 7 Apr 2016 13:58:15 -0500 Subject: [PATCH 04/17] Removed unused variable --- modules/recorder/Recorder.js | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/recorder/Recorder.js b/modules/recorder/Recorder.js index e44438072..9f2f7d870 100644 --- a/modules/recorder/Recorder.js +++ b/modules/recorder/Recorder.js @@ -33,7 +33,6 @@ class Recorder { * local participant is not the moderator */ constructor (conference, UI) { - var self = this; this._conference = conference; this._UI = UI; From 9ef43d1fe7f820dd5a893b51ba00e31e82e81c8a Mon Sep 17 00:00:00 2001 From: yanas Date: Thu, 7 Apr 2016 14:11:59 -0500 Subject: [PATCH 05/17] Remove self and use this instead in conference.js --- conference.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/conference.js b/conference.js index 16df73728..a4f9ee41c 100644 --- a/conference.js +++ b/conference.js @@ -723,8 +723,6 @@ export default { * Setup interaction between conference and UI. */ _setupListeners () { - var self = this; - // add local streams when joined to the conference room.on(ConferenceEvents.CONFERENCE_JOINED, () => { APP.UI.mucJoined(); @@ -907,7 +905,7 @@ export default { APP.UI.updateLocalStats(percent, stats); // send local stats to other users - room.sendCommandOnce(self.commands.defaults.CONNECTION_QUALITY, + room.sendCommandOnce(this.commands.defaults.CONNECTION_QUALITY, { children: ConnectionQuality.convertToMUCStats(stats), attributes: { @@ -918,7 +916,7 @@ export default { ); // listen to remote stats - room.addCommandListener(self.commands.defaults.CONNECTION_QUALITY, + room.addCommandListener(this.commands.defaults.CONNECTION_QUALITY, (values, from) => { ConnectionQuality.updateRemoteStats(from, values); }); @@ -928,7 +926,7 @@ export default { APP.UI.updateRemoteStats(id, percent, stats); }); - room.addCommandListener(self.commands.defaults.ETHERPAD, ({value}) => { + room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => { APP.UI.initEtherpad(value); }); @@ -941,9 +939,9 @@ export default { APP.settings.setEmail(email); APP.UI.setUserAvatar(room.myUserId(), email); - sendEmail(self.commands.defaults.EMAIL, email); + sendEmail(this.commands.defaults.EMAIL, email); }); - room.addCommandListener(self.commands.defaults.EMAIL, (data) => { + room.addCommandListener(this.commands.defaults.EMAIL, (data) => { APP.UI.setUserAvatar(data.attributes.id, data.value); }); @@ -1088,8 +1086,8 @@ export default { // send start and stop commands once, and remove any updates // that had left if (state === 'stop' || state === 'start' || state === 'playing') { - room.removeCommand(self.commands.defaults.SHARED_VIDEO); - room.sendCommandOnce(self.commands.defaults.SHARED_VIDEO, { + room.removeCommand(this.commands.defaults.SHARED_VIDEO); + room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, { value: url, attributes: { state: state, @@ -1101,8 +1099,8 @@ export default { else { // in case of paused, in order to allow late users to join // paused - room.removeCommand(self.commands.defaults.SHARED_VIDEO); - room.sendCommand(self.commands.defaults.SHARED_VIDEO, { + room.removeCommand(this.commands.defaults.SHARED_VIDEO); + room.sendCommand(this.commands.defaults.SHARED_VIDEO, { value: url, attributes: { state: state, @@ -1113,7 +1111,7 @@ export default { } }); room.addCommandListener( - self.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => { + this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => { if (attributes.state === 'stop') { APP.UI.stopSharedVideo(id, attributes); From d9c3eec9a8d25e7311794b884baa8644f491ae1e Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 8 Apr 2016 10:55:19 -0500 Subject: [PATCH 06/17] Fix the black stripe (another try) --- modules/UI/side_pannels/chat/Chat.js | 2 +- modules/UI/side_pannels/contactlist/ContactList.js | 3 ++- modules/UI/side_pannels/settings/SettingsMenu.js | 2 +- modules/UI/util/UIUtil.js | 13 ++++++++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/modules/UI/side_pannels/chat/Chat.js b/modules/UI/side_pannels/chat/Chat.js index 00fe47dc7..33c5f0dc2 100644 --- a/modules/UI/side_pannels/chat/Chat.js +++ b/modules/UI/side_pannels/chat/Chat.js @@ -315,7 +315,7 @@ var Chat = { * Indicates if the chat is currently visible. */ isVisible () { - return $('#chatspace').is(":visible"); + return UIUtil.isVisible(document.getElementById("chatspace")); }, /** * Shows and hides the window with the smileys diff --git a/modules/UI/side_pannels/contactlist/ContactList.js b/modules/UI/side_pannels/contactlist/ContactList.js index aa743afa9..267b78d7e 100644 --- a/modules/UI/side_pannels/contactlist/ContactList.js +++ b/modules/UI/side_pannels/contactlist/ContactList.js @@ -1,6 +1,7 @@ /* global $, APP */ import Avatar from '../../avatar/Avatar'; import UIEvents from '../../../../service/UI/UIEvents'; +import UIUtil from '../../util/UIUtil'; let numberOfContacts = 0; let notificationInterval; @@ -87,7 +88,7 @@ var ContactList = { * otherwise */ isVisible () { - return $('#contactlist').is(":visible"); + return UIUtil.isVisible(document.getElementById("contactlist")); }, /** diff --git a/modules/UI/side_pannels/settings/SettingsMenu.js b/modules/UI/side_pannels/settings/SettingsMenu.js index 266f10e78..ffebeaf20 100644 --- a/modules/UI/side_pannels/settings/SettingsMenu.js +++ b/modules/UI/side_pannels/settings/SettingsMenu.js @@ -161,7 +161,7 @@ export default { * @returns {boolean} */ isVisible () { - return $('#settingsmenu').is(':visible'); + return UIUtil.isVisible(document.getElementById("settingsmenu")); }, /** diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index ab284805f..4e3e381e3 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -155,7 +155,18 @@ return Object.keys(attrs).map( key => ` ${key}="${attrs[key]}"` ).join(' '); - } + }, + + /** + * Checks if the given DOM element is currently visible. The offsetParent + * will be null if the "display" property of the element or any of its + * parent containers is set to "none". This method will NOT check the + * visibility property though. + * @param {el} The DOM element we'd like to check for visibility + */ + isVisible(el) { + return (el.offsetParent !== null); + } }; export default UIUtil; From b597f44605e1fe17b98d06ccd06664a5bcc45ea9 Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 8 Apr 2016 14:46:13 -0500 Subject: [PATCH 07/17] Fix early call of SidePanel isVisible --- index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 550b444cf..8c201d8e3 100644 --- a/index.html +++ b/index.html @@ -206,7 +206,7 @@ -
+
-
+ -
+ ", skin: "black"}); + // override popover show method to make sure we will update the content + // before showing the popover + var origShowFunc = this.popover.show; + this.popover.show = function () { + // update content by forcing it, to finish even if popover + // is not visible + this.updatePopoverData(true); + // call the original show, passing its actual this + origShowFunc.call(this.popover); + }.bind(this); + this.emptyIcon = this.connectionIndicatorContainer.appendChild( createIcon(["connection", "connection_empty"])); this.fullIcon = this.connectionIndicatorContainer.appendChild( @@ -335,13 +346,18 @@ ConnectionIndicator.prototype.updateResolution = function (resolution) { }; /** - * Updates the content of the popover + * Updates the content of the popover if its visible + * @param force to work even if popover is not visible */ -ConnectionIndicator.prototype.updatePopoverData = function () { - this.popover.updateContent( - `
${this.generateText()}
` - ); - APP.translation.translateElement($(".connection_info")); +ConnectionIndicator.prototype.updatePopoverData = function (force) { + // generate content, translate it and add it to document only if + // popover is visible or we force to do so. + if(this.popover.popoverShown || force) { + this.popover.updateContent( + `
${this.generateText()}
` + ); + APP.translation.translateElement($(".connection_info")); + } }; /** From 0e4f4cbd745de5c8de66768ad6b1454cbeddac2d Mon Sep 17 00:00:00 2001 From: damencho Date: Sat, 9 Apr 2016 22:08:06 -0500 Subject: [PATCH 10/17] Initializes display name with initial value and does not depend on initial on display name changed event. --- modules/UI/UI.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/UI/UI.js b/modules/UI/UI.js index d9d46deb4..a05000ced 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -537,6 +537,10 @@ UI.addUser = function (id, displayName) { // Configure avatar UI.setUserAvatar(id); + + // set initial display name + if(displayName) + UI.changeDisplayName(id, displayName); }; /** From ddf39a20b8d92a657c5c962265f3fcf20e82cd40 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Sun, 10 Apr 2016 12:25:37 -0500 Subject: [PATCH 11/17] Fixes time console.log text --- modules/UI/videolayout/SmallVideo.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index a809aaebb..481e680d9 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -159,9 +159,10 @@ SmallVideo.createStreamElement = function (stream) { element.id = SmallVideo.getStreamElementID(stream); element.onplay = function () { - var now = APP.performanceTimes["video.render"] + var type = (isVideo ? 'video' : 'audio'); + var now = APP.performanceTimes[type + ".render"] = window.performance.now(); - console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t", + console.log("(TIME) Render " + type + ":\t", now); }; From 655b7019d1bdc357f6cefc7ffb7484d304ab92e8 Mon Sep 17 00:00:00 2001 From: hristoterezov Date: Mon, 11 Apr 2016 10:01:23 -0500 Subject: [PATCH 12/17] Renames performanceTimes to ConnectionTimes --- app.js | 6 +++--- modules/UI/videolayout/SmallVideo.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 277cbb161..e8a02a2ee 100644 --- a/app.js +++ b/app.js @@ -52,7 +52,7 @@ const APP = { handler: null }, // Used for automated performance tests - performanceTimes: { + connectionTimes: { "index.loaded": window.indexLoadedTime }, UI, @@ -103,7 +103,7 @@ function obtainConfigAndInit() { // Get config result callback function(success, error) { if (success) { - var now = APP.performanceTimes["configuration.fetched"] = + var now = APP.connectionTimes["configuration.fetched"] = window.performance.now(); console.log("(TIME) configuration fetched:\t", now); init(); @@ -124,7 +124,7 @@ function obtainConfigAndInit() { $(document).ready(function () { - var now = APP.performanceTimes["document.ready"] = window.performance.now(); + var now = APP.connectionTimes["document.ready"] = window.performance.now(); console.log("(TIME) document ready:\t", now); URLProcessor.setConfigParametersFromUrl(); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 481e680d9..d3acdaa35 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -160,7 +160,7 @@ SmallVideo.createStreamElement = function (stream) { element.onplay = function () { var type = (isVideo ? 'video' : 'audio'); - var now = APP.performanceTimes[type + ".render"] + var now = APP.connectionTimes[type + ".render"] = window.performance.now(); console.log("(TIME) Render " + type + ":\t", now); From 13e3e999948dfbc06d9c278f192f6ac38f99f024 Mon Sep 17 00:00:00 2001 From: damencho Date: Mon, 11 Apr 2016 10:16:08 -0500 Subject: [PATCH 13/17] Updates websockets example. --- doc/example-config-files/jitsi.example.com.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/example-config-files/jitsi.example.com.example b/doc/example-config-files/jitsi.example.com.example index 96ae864f9..eddd796ce 100755 --- a/doc/example-config-files/jitsi.example.com.example +++ b/doc/example-config-files/jitsi.example.com.example @@ -23,7 +23,7 @@ server { # xmpp websockets location /xmpp-websocket { - proxy_pass http://localhost:5280; + proxy_pass http://localhost:5280/xmpp-websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; From 7ba8b0a24e8fd7ea2a878c1565557cdb4baa62a5 Mon Sep 17 00:00:00 2001 From: yanas Date: Fri, 15 Apr 2016 15:41:51 -0500 Subject: [PATCH 14/17] Fix enable feedback button check --- modules/UI/Feedback.js | 11 +++++++---- modules/UI/UI.js | 4 ++-- modules/recorder/Recorder.js | 7 +------ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/modules/UI/Feedback.js b/modules/UI/Feedback.js index 92f79bc37..5fca4464e 100644 --- a/modules/UI/Feedback.js +++ b/modules/UI/Feedback.js @@ -100,15 +100,18 @@ var Feedback = { * @param emitter the EventEmitter to associate with the Feedback. */ init: function (emitter) { - // Initialise to enabled. - this.enabled = true; - // CallStats is the way we send feedback, so we don't have to initialise // if callstats isn't enabled. if (!APP.conference.isCallstatsEnabled()) return; - _showFeedbackButton(true); + // If enabled property is still undefined, i.e. it hasn't been set from + // some other module already, we set it to true by default. + if (typeof this.enabled == "undefined") + this.enabled = true; + + _showFeedbackButton(this.enabled); + $("#feedbackButton").click(function (event) { Feedback.openFeedbackWindow(); }); diff --git a/modules/UI/UI.js b/modules/UI/UI.js index a05000ced..260b003b9 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -31,7 +31,6 @@ var Feedback = require("./Feedback"); import FollowMe from "../FollowMe"; import Recorder from "../recorder/Recorder"; - var eventEmitter = new EventEmitter(); UI.eventEmitter = eventEmitter; @@ -245,7 +244,7 @@ UI.initConference = function () { // Initialise the recorder handler. We're doing this explicitly before // calling showToolbar, because the recorder may want to disable all // toolbars. - new Recorder(APP.conference, UI); + new Recorder(APP.conference); // Once we've joined the muc show the toolbar ToolbarToggler.showToolbar(); @@ -260,6 +259,7 @@ UI.initConference = function () { UI.setUserAvatar(id, Settings.getEmail()); Toolbar.checkAutoEnableDesktopSharing(); + if(!interfaceConfig.filmStripOnly) { Feedback.init(eventEmitter); } diff --git a/modules/recorder/Recorder.js b/modules/recorder/Recorder.js index 9f2f7d870..ca156f264 100644 --- a/modules/recorder/Recorder.js +++ b/modules/recorder/Recorder.js @@ -27,14 +27,9 @@ class Recorder { * * @param conference the {conference} which is to transport * {Recorder}-related information between participants - * @param UI the {UI} which is the source (model/state) to be sent to - * remote participants if the local participant is the moderator or the - * destination (model/state) to receive from the remote moderator if the - * local participant is not the moderator */ - constructor (conference, UI) { + constructor (conference) { this._conference = conference; - this._UI = UI; // If I am a recorder then I publish my recorder custom role to notify // everyone. From d08e37b42b8e39effefac2ca71a09f42767ce852 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 19 Apr 2016 13:07:04 -0500 Subject: [PATCH 15/17] Adds smart un-mute to the shared video logic --- conference.js | 5 +- index.html | 3 + lang/main.json | 3 +- modules/UI/UI.js | 8 +++ modules/UI/shared_video/SharedVideo.js | 93 ++++++++++++-------------- modules/UI/toolbars/Toolbar.js | 19 +++++- modules/UI/util/UIUtil.js | 33 +++++++++ 7 files changed, 107 insertions(+), 57 deletions(-) diff --git a/conference.js b/conference.js index 618b230a2..75333fcc3 100644 --- a/conference.js +++ b/conference.js @@ -73,8 +73,11 @@ function getDisplayName (id) { /** * Mute or unmute local audio stream if it exists. * @param {boolean} muted if audio stream should be muted or unmuted. + * @param {boolean} indicates if this local audio mute was a result of user + * interaction + * */ -function muteLocalAudio (muted) { +function muteLocalAudio (muted, userInteraction) { if (!localAudio) { return; } diff --git a/index.html b/index.html index 8c201d8e3..f4ac8e737 100644 --- a/index.html +++ b/index.html @@ -124,6 +124,9 @@
+
    +
  • +
diff --git a/lang/main.json b/lang/main.json index 963c6e553..f05fe8791 100644 --- a/lang/main.json +++ b/lang/main.json @@ -65,7 +65,8 @@ "logout": "Logout", "dialpad": "Show dialpad", "sharedVideoMutedPopup": "Your shared video has been muted so
that you can talk to the other participants.", - "micMutedPopup": "Your microphone has been muted so that you
would fully enjoy your shared video." + "micMutedPopup": "Your microphone has been muted so that you
would fully enjoy your shared video.", + "unableToUnmutePopup": "You're not able to un-mute while the shared video is on." }, "bottomtoolbar": { "chat": "Open / close chat", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 260b003b9..cafe3ef9d 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -331,6 +331,14 @@ function bindEvents() { $(window).resize(onResize); } +/** + * Returns the shared document manager object. + * @return {EtherpadManager} the shared document manager object + */ +UI.getSharedVideoManager = function () { + return sharedVideoManager; +}; + /** * Starts the UI module and initializes all related components. * diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js index ea2ca86e3..aa9af4deb 100644 --- a/modules/UI/shared_video/SharedVideo.js +++ b/modules/UI/shared_video/SharedVideo.js @@ -26,6 +26,24 @@ export default class SharedVideoManager { this.emitter = emitter; this.isSharedVideoShown = false; this.isPlayerAPILoaded = false; + this.mutedWithUserInteraction = false; + } + + /** + * Indicates if the player volume is currently on. + * + * @returns {*|player|boolean} + */ + isSharedVideoVolumeOn() { + return (this.player && this.player.getVolume() > 0); + } + + /** + * Indicates if the local user is the owner of the shared video. + * @returns {*|boolean} + */ + isSharedVideoOwner() { + return this.from && APP.conference.isLocalId(this.from); } /** @@ -169,10 +187,15 @@ export default class SharedVideoManager { // 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); + && !APP.conference.isLocalAudioMuted()) { + self.emitter.emit(UIEvents.AUDIO_MUTED, true, false); self.showMicMutedPopup(true); } + else if (!self.mutedWithUserInteraction + && (event.data.volume <=0 || event.data.muted) + && APP.conference.isLocalAudioMuted()) { + self.emitter.emit(UIEvents.AUDIO_MUTED, false, false); + } }; window.onPlayerReady = function(event) { @@ -231,9 +254,11 @@ export default class SharedVideoManager { this.processTime(player, attributes, playerPaused); // lets check the volume - if (attributes.volume !== undefined && - player.getVolume() != attributes.volume - && APP.conference.isLocalAudioMuted()) { + if (attributes.volume !== undefined + && player.getVolume() != attributes.volume + && (APP.conference.isLocalAudioMuted() + || !this.mutedWithUserInteraction)) { + player.setVolume(attributes.volume); console.info("Player change of volume:" + attributes.volume); this.showSharedVideoMutedPopup(false); @@ -393,17 +418,21 @@ export default class SharedVideoManager { /** * Receives events for local audio mute/unmute by local user. * @param muted boolena whether it is muted or not. + * @param {boolean} indicates if this mute was a result of user interaction, + * i.e. pressing the mute button or it was programatically triggerred */ - localAudioMuted (muted) { + localAudioMuted (muted, userInteraction) { if(!this.player) return; - if(muted) + if (muted) { + this.mutedWithUserInteraction = userInteraction; 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()){ + if (this.player.getVolume() > 0 || !this.player.isMuted()) { this.player.setVolume(0); this.showSharedVideoMutedPopup(true); } @@ -415,30 +444,10 @@ export default class SharedVideoManager { * @param show boolean, show or hide the notification */ showMicMutedPopup (show) { - var micMutedPopupSelector = $("#micMutedPopup"); - if(show) { + if(show) this.showSharedVideoMutedPopup(false); - if (!micMutedPopupSelector.is(":visible")) - micMutedPopupSelector.css("display", "inline-block"); - - // FIXME: we need an utility method for that. - micMutedPopupSelector.fadeIn(300, - () => {micMutedPopupSelector.css({opacity: 1});} - ); - - setTimeout( - function () { - micMutedPopupSelector.fadeOut(300, - () => {micMutedPopupSelector.css({opacity: 0});} - ); - }, 5000); - } - else { - micMutedPopupSelector.fadeOut(300, - () => {micMutedPopupSelector.css({opacity: 0});} - ); - } + UIUtil.animateShowElement($("#micMutedPopup"), show, 5000); } /** @@ -448,30 +457,10 @@ export default class SharedVideoManager { * @param show boolean, show or hide the notification */ showSharedVideoMutedPopup (show) { - var sharedVideoMutedPopupSelector = $("#sharedVideoMutedPopup"); - if(show) { + if(show) this.showMicMutedPopup(false); - if (!sharedVideoMutedPopupSelector.is(":visible")) - sharedVideoMutedPopupSelector.css("display", "inline-block"); - - // FIXME: we need an utility method for that. - sharedVideoMutedPopupSelector.fadeIn(300, - () => {sharedVideoMutedPopupSelector.css({opacity: 1});} - ); - - setTimeout( - function () { - sharedVideoMutedPopupSelector.fadeOut(300, - () => {sharedVideoMutedPopupSelector.css({opacity: 0});} - ); - }, 5000); - } - else { - sharedVideoMutedPopupSelector.fadeOut(300, - () => {sharedVideoMutedPopupSelector.css({opacity: 0});} - ); - } + UIUtil.animateShowElement($("#sharedVideoMutedPopup"), show, 5000); } } diff --git a/modules/UI/toolbars/Toolbar.js b/modules/UI/toolbars/Toolbar.js index 7a95487ee..e98abc9cc 100644 --- a/modules/UI/toolbars/Toolbar.js +++ b/modules/UI/toolbars/Toolbar.js @@ -44,12 +44,25 @@ function openLinkDialog () { const buttonHandlers = { "toolbar_button_mute": function () { + let sharedVideoManager = APP.UI.getSharedVideoManager(); + if (APP.conference.audioMuted) { - AnalyticsAdapter.sendEvent('toolbar.audio.unmuted'); - emitter.emit(UIEvents.AUDIO_MUTED, false); + // If there's a shared video with the volume "on" and we aren't + // the video owner, we warn the user + // that currently it's not possible to unmute. + if (sharedVideoManager + && sharedVideoManager.isSharedVideoVolumeOn() + && !sharedVideoManager.isSharedVideoOwner()) { + UIUtil.animateShowElement( + $("#unableToUnmutePopup"), true, 5000); + } + else { + AnalyticsAdapter.sendEvent('toolbar.audio.unmuted'); + emitter.emit(UIEvents.AUDIO_MUTED, false, true); + } } else { AnalyticsAdapter.sendEvent('toolbar.audio.muted'); - emitter.emit(UIEvents.AUDIO_MUTED, true); + emitter.emit(UIEvents.AUDIO_MUTED, true, true); } }, "toolbar_button_camera": function () { diff --git a/modules/UI/util/UIUtil.js b/modules/UI/util/UIUtil.js index 4e3e381e3..0e37b5b11 100644 --- a/modules/UI/util/UIUtil.js +++ b/modules/UI/util/UIUtil.js @@ -166,6 +166,39 @@ */ isVisible(el) { return (el.offsetParent !== null); + }, + + /** + * Shows / hides the element given by {selector} and sets a timeout if the + * {hideDelay} is set to a value > 0. + * @param selector the jquery selector of the element to show/hide. + * @param show a {boolean} that indicates if the element should be shown or + * hidden + * @param hideDelay the value in milliseconds to wait before hiding the + * element + */ + animateShowElement(selector, show, hideDelay) { + if(show) { + if (!selector.is(":visible")) + selector.css("display", "inline-block"); + + selector.fadeIn(300, + () => {selector.css({opacity: 1});} + ); + + if (hideDelay && hideDelay > 0) + setTimeout( + function () { + selector.fadeOut(300, + () => {selector.css({opacity: 0});} + ); + }, hideDelay); + } + else { + selector.fadeOut(300, + () => {selector.css({opacity: 0});} + ); + } } }; From 326dedaf400dcba0e630b1add868e16ff658a99f Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 19 Apr 2016 13:24:27 -0500 Subject: [PATCH 16/17] Fix string --- lang/main.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/main.json b/lang/main.json index f05fe8791..9297b4ebd 100644 --- a/lang/main.json +++ b/lang/main.json @@ -66,7 +66,7 @@ "dialpad": "Show dialpad", "sharedVideoMutedPopup": "Your shared video has been muted so
that you can talk to the other participants.", "micMutedPopup": "Your microphone has been muted so that you
would fully enjoy your shared video.", - "unableToUnmutePopup": "You're not able to un-mute while the shared video is on." + "unableToUnmutePopup": "You cannot un-mute while the shared video is on." }, "bottomtoolbar": { "chat": "Open / close chat", From 0bf46603094f86c04d46829104a96411213018e4 Mon Sep 17 00:00:00 2001 From: yanas Date: Tue, 19 Apr 2016 16:06:16 -0500 Subject: [PATCH 17/17] Fix require display name dialog --- lang/main.json | 4 ++-- modules/UI/UI.js | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lang/main.json b/lang/main.json index 9297b4ebd..0e5233b05 100644 --- a/lang/main.json +++ b/lang/main.json @@ -8,7 +8,7 @@ "participant": "Participant", "me": "me", "speaker": "Speaker", - "defaultNickname": "ex. __name__", + "defaultNickname": "ex. Jane Pink", "defaultLink": "e.g. __url__", "welcomepage":{ "go": "GO", @@ -204,7 +204,7 @@ "userPassword": "user password", "token": "token", "tokenAuthFailed": "Failed to authenticate with XMPP server: invalid token", - "displayNameRequired": "Please enter your display name:", + "displayNameRequired": "Please enter your display name", "extensionRequired": "Extension required:", "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?", diff --git a/modules/UI/UI.js b/modules/UI/UI.js index cafe3ef9d..8a2e2fa98 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -45,31 +45,32 @@ let followMeHandler; function promptDisplayName() { let nickRequiredMsg = APP.translation.translateString("dialog.displayNameRequired"); - let defaultNickMsg = APP.translation.translateString( - "defaultNickname", {name: "Jane Pink"} - ); + let defaultNickMsg = APP.translation.translateString("defaultNickname"); let message = `

${nickRequiredMsg}

`; - let buttonTxt = APP.translation.generateTranslationHTML("dialog.Ok"); - let buttons = [{title: buttonTxt, value: "ok"}]; + // Don't use a translation string, because we're too early in the call and + // the translation may not be initialised. + let buttons = {Ok:true}; - messageHandler.openDialog( - null, message, + let dialog = messageHandler.openDialog( + null, + message, true, buttons, function (e, v, m, f) { - if (v == "ok") { + e.preventDefault(); + if (v) { let displayName = f.displayName; if (displayName) { UI.inputDisplayNameHandler(displayName); - return true; + dialog.close(); + return; } } - e.preventDefault(); }, function () { let form = $.prompt.getPrompt();