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/conference.js b/conference.js index 447859c5b..75333fcc3 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() @@ -82,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; } @@ -507,6 +501,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()); @@ -522,7 +571,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) { @@ -534,50 +583,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) { @@ -916,7 +921,8 @@ export default { APP.UI.updateLocalStats(percent, stats); // send local stats to other users - room.sendCommandOnce(Commands.CONNECTION_QUALITY, { + room.sendCommandOnce(this.commands.defaults.CONNECTION_QUALITY, + { children: ConnectionQuality.convertToMUCStats(stats), attributes: { xmlns: 'http://jitsi.org/jitmeet/stats' @@ -926,8 +932,9 @@ export default { ); // listen to remote stats - room.addCommandListener(Commands.CONNECTION_QUALITY,(values, from) => { - ConnectionQuality.updateRemoteStats(from, values); + room.addCommandListener(this.commands.defaults.CONNECTION_QUALITY, + (values, from) => { + ConnectionQuality.updateRemoteStats(from, values); }); ConnectionQuality.addListener(CQEvents.REMOTESTATS_UPDATED, @@ -935,7 +942,7 @@ export default { APP.UI.updateRemoteStats(id, percent, stats); }); - room.addCommandListener(Commands.ETHERPAD, ({value}) => { + room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => { APP.UI.initEtherpad(value); }); @@ -948,9 +955,9 @@ export default { APP.settings.setEmail(email); APP.UI.setUserAvatar(room.myUserId(), email); - sendEmail(email); + sendEmail(this.commands.defaults.EMAIL, email); }); - room.addCommandListener(Commands.EMAIL, (data) => { + room.addCommandListener(this.commands.defaults.EMAIL, (data) => { APP.UI.setUserAvatar(data.attributes.id, data.value); }); @@ -1095,8 +1102,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(this.commands.defaults.SHARED_VIDEO); + room.sendCommandOnce(this.commands.defaults.SHARED_VIDEO, { value: url, attributes: { state: state, @@ -1108,8 +1115,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(this.commands.defaults.SHARED_VIDEO); + room.sendCommand(this.commands.defaults.SHARED_VIDEO, { value: url, attributes: { state: state, @@ -1120,7 +1127,7 @@ export default { } }); room.addCommandListener( - Commands.SHARED_VIDEO, ({value, attributes}, id) => { + this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => { if (attributes.state === 'stop') { APP.UI.stopSharedVideo(id, attributes); 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"; diff --git a/index.html b/index.html index a88777286..4af58e714 100644 --- a/index.html +++ b/index.html @@ -146,6 +146,9 @@ + @@ -228,7 +231,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")); + } }; /** diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 7747ea822..32c3fd114 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -357,7 +357,9 @@ RemoteVideo.prototype.setDisplayName = function(displayName, key) { // If we already have a display name for this video. if (nameSpan.length > 0) { if (displayName && displayName.length > 0) { - $('#' + this.videoSpanId + '_name').text(displayName); + var displaynameSpan = $('#' + this.videoSpanId + '_name'); + if (displaynameSpan.text() !== displayName) + displaynameSpan.text(displayName); } else if (key && key.length > 0) { var nameHtml = APP.translation.generateTranslationHTML(key); diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index f40d2e9be..d3acdaa35 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; @@ -141,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.connectionTimes[type + ".render"] = window.performance.now(); - console.log("(TIME) Render " + (isVideo ? 'video' : 'audio') + ":\t", + console.log("(TIME) Render " + type + ":\t", now); }; 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. diff --git a/modules/recorder/Recorder.js b/modules/recorder/Recorder.js new file mode 100644 index 000000000..ca156f264 --- /dev/null +++ b/modules/recorder/Recorder.js @@ -0,0 +1,95 @@ +/* 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 + */ + constructor (conference) { + this._conference = conference; + + // 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;