diff --git a/conference.js b/conference.js index ea35690a2..c70914ebb 100644 --- a/conference.js +++ b/conference.js @@ -2125,18 +2125,6 @@ export default { eventEmitter.removeListener(eventName, listener); }, - /** - * Checks if the participant given by participantId is currently in the - * last N set if there's one supported. - * - * @param participantId the identifier of the participant - * @returns {boolean} {true} if the participant given by the participantId - * is currently in the last N set or if there's no last N set at this point - * and {false} otherwise - */ - isInLastN(participantId) { - return room.isInLastN(participantId); - }, /** * Changes the display name for the local user * @param nickname {string} the new display name diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index f3bdb45ec..26012f17a 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -97,6 +97,11 @@ color: #FFFFFF;/*#15A1ED*/ overflow: hidden; } + + &_ninja + { + font-size: 1.5em; + } } .icon-connection, diff --git a/modules/UI/videolayout/ConnectionIndicator.js b/modules/UI/videolayout/ConnectionIndicator.js index 470d0fc3b..0d95ff58e 100644 --- a/modules/UI/videolayout/ConnectionIndicator.js +++ b/modules/UI/videolayout/ConnectionIndicator.js @@ -1,9 +1,12 @@ -/* global $, APP, interfaceConfig */ +/* global $, APP, interfaceConfig, JitsiMeetJS */ /* jshint -W101 */ import JitsiPopover from "../util/JitsiPopover"; import UIUtil from "../util/UIUtil"; +const ParticipantConnectionStatus + = JitsiMeetJS.constants.participantConnectionStatus; + /** * Maps a connection quality value (in percent) to the width of the "full" icon. */ @@ -334,8 +337,11 @@ ConnectionIndicator.prototype.create = function () { createIcon(["connection_full"], "icon-connection")); this.interruptedIndicator = connectionIconContainer.appendChild( createIcon(["connection_lost"],"icon-connection-lost")); + this.ninjaIndicator = connectionIconContainer.appendChild( + createIcon(["connection_ninja"],"icon-ninja")); $(this.interruptedIndicator).hide(); + $(this.ninjaIndicator).hide(); this.connectionIndicatorContainer.appendChild(connectionIconContainer); }; @@ -351,23 +357,30 @@ ConnectionIndicator.prototype.remove = function() { }; /** - * Updates the UI which displays warning about user's connectivity problems. + * Updates the UI which displays or not a warning about user's connectivity + * problems. * - * @param {boolean} isActive true if the connection is working fine or false if - * the user is having connectivity issues. + * @param {ParticipantConnectionStatus} connectionStatus */ ConnectionIndicator.prototype.updateConnectionStatusIndicator - = function (isActive) { - this.isConnectionActive = isActive; - if (this.isConnectionActive) { - $(this.interruptedIndicator).hide(); - $(this.emptyIcon).show(); - $(this.fullIcon).show(); - } else { - $(this.interruptedIndicator).show(); - $(this.emptyIcon).hide(); - $(this.fullIcon).hide(); - } += function (connectionStatus) { + this.connectionStatus = connectionStatus; + if (connectionStatus === ParticipantConnectionStatus.INTERRUPTED) { + $(this.interruptedIndicator).show(); + $(this.emptyIcon).hide(); + $(this.fullIcon).hide(); + $(this.ninjaIndicator).hide(); + } else if (connectionStatus === ParticipantConnectionStatus.INACTIVE) { + $(this.interruptedIndicator).hide(); + $(this.emptyIcon).hide(); + $(this.fullIcon).hide(); + $(this.ninjaIndicator).show(); + } else { + $(this.interruptedIndicator).hide(); + $(this.emptyIcon).show(); + $(this.fullIcon).show(); + $(this.ninjaIndicator).hide(); + } }; /** diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 4130cc5fd..a53af211e 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -26,6 +26,18 @@ const VIDEO_RESOLUTION_POLL_INTERVAL = 2000; * Manager for all Large containers. */ export default class LargeVideoManager { + /** + * Checks whether given container is a {@link VIDEO_CONTAINER_TYPE}. + * FIXME currently this is a workaround for the problem where video type is + * mixed up with container type. + * @param {string} containerType + * @return {boolean} + */ + static isVideoContainer(containerType) { + return containerType === VIDEO_CONTAINER_TYPE + || containerType === DESKTOP_CONTAINER_TYPE; + } + constructor (emitter) { /** * The map of LargeContainers where the key is the video @@ -116,7 +128,8 @@ export default class LargeVideoManager { this.enableLocalConnectionProblemFilter(true); this._setLocalConnectionMessage("connection.RECONNECTING"); // Show the message only if the video is currently being displayed - this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE); + this.showLocalConnectionMessage( + LargeVideoManager.isVideoContainer(this.state)); } /** @@ -146,7 +159,12 @@ export default class LargeVideoManager { preUpdate.then(() => { const { id, stream, videoType, resolve } = this.newStreamData; - const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE; + + // FIXME this does not really make sense, because the videoType + // (camera or desktop) is a completely different thing than + // the video container type (Etherpad, SharedVideo, VideoContainer). + const isVideoContainer + = LargeVideoManager.isVideoContainer(videoType); this.newStreamData = null; @@ -158,34 +176,26 @@ export default class LargeVideoManager { // change the avatar url on large this.updateAvatar(Avatar.getAvatarUrl(id)); - // FIXME that does not really make sense, because the videoType - // (camera or desktop) is a completely different thing than - // the video container type (Etherpad, SharedVideo, VideoContainer). - // ---------------------------------------------------------------- - // If the container is VIDEO_CONTAINER_TYPE, we need to check - // its stream whether exist and is muted to set isVideoMuted - // in rest of the cases it is false - let showAvatar = isVideoFromCamera && (!stream || stream.isMuted()); - // If the user's connection is disrupted then the avatar will be // displayed in case we have no video image cached. That is if - // there was a user switch(image is lost on stream detach) or if + // there was a user switch (image is lost on stream detach) or if // the video was not rendered, before the connection has failed. - const isConnectionActive = this._isConnectionActive(id); + const wasUsersImageCached + = !isUserSwitch && container.wasVideoRendered; + const isVideoMuted = !stream || stream.isMuted(); - if (isVideoFromCamera - && !isConnectionActive - && (isUserSwitch || !container.wasVideoRendered)) { - showAvatar = true; - } + const connectionStatus + = APP.conference.getParticipantConnectionStatus(id); + const isVideoRenderable + = !isVideoMuted + && (APP.conference.isLocalId(id) + || connectionStatus + === ParticipantConnectionStatus.ACTIVE + || wasUsersImageCached); - // If audio only mode is enabled, always show the avatar for - // videos from another participant. - if (APP.conference.isAudioOnly() - && (isVideoFromCamera - || videoType === DESKTOP_CONTAINER_TYPE)) { - showAvatar = true; - } + let showAvatar + = isVideoContainer + && (APP.conference.isAudioOnly() || !isVideoRenderable); let promise; @@ -208,28 +218,31 @@ export default class LargeVideoManager { this.updateLargeVideoAudioLevel(0); } + const isConnectionInterrupted + = APP.conference.getParticipantConnectionStatus(id) + === ParticipantConnectionStatus.INTERRUPTED; + let messageKey = null; + + if (isConnectionInterrupted) { + messageKey = "connection.USER_CONNECTION_INTERRUPTED"; + } else if (connectionStatus + === ParticipantConnectionStatus.INACTIVE) { + messageKey = "connection.LOW_BANDWIDTH"; + } + // Make sure no notification about remote failure is shown as // its UI conflicts with the one for local connection interrupted. // For the purposes of UI indicators, audio only is considered as // an "active" connection. - const isConnected + const overrideAndHide = APP.conference.isAudioOnly() - || APP.conference.isConnectionInterrupted() - || isConnectionActive; - - // when isHavingConnectivityIssues, state can be inactive, - // interrupted or restoring. We show different message for - // interrupted and the rest. - const isConnectionInterrupted = - APP.conference.getParticipantConnectionStatus(id) - === ParticipantConnectionStatus.INTERRUPTED; + || APP.conference.isConnectionInterrupted(); this.updateParticipantConnStatusIndication( id, - isConnected, - (isConnectionInterrupted) - ? "connection.USER_CONNECTION_INTERRUPTED" - : "connection.LOW_BANDWIDTH"); + !overrideAndHide && isConnectionInterrupted, + !overrideAndHide && messageKey !== null, + messageKey); // resolve updateLargeVideo promise after everything is done promise.then(resolve); @@ -265,18 +278,20 @@ export default class LargeVideoManager { * shown on the large video area. * * @param {string} id the id of remote participant(MUC nickname) - * @param {boolean} isConnected true if the connection is active or false - * when the user is having connectivity issues. + * @param {boolean} showProblemsIndication + * @param {boolean} showMessage * @param {string} messageKey the i18n key of the message * * @private */ - updateParticipantConnStatusIndication (id, isConnected, messageKey) { + updateParticipantConnStatusIndication ( + id, showProblemsIndication, showMessage, messageKey) { // Apply grey filter on the large video - this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected); + this.videoContainer.showRemoteConnectionProblemIndicator( + showProblemsIndication); - if (isConnected) { + if (!showMessage) { // Hide the message this.showRemoteConnectionMessage(false); } else { @@ -289,7 +304,7 @@ export default class LargeVideoManager { // Show it now only if the VideoContainer is on top this.showRemoteConnectionMessage( - this.state === VIDEO_CONTAINER_TYPE); + LargeVideoManager.isVideoContainer(this.state)); } } @@ -412,15 +427,20 @@ export default class LargeVideoManager { * Shows hides the "avatar" message which is to be displayed either in * the middle of the screen or below the avatar image. * - * @param {null|boolean} show (optional) true to show the avatar + * @param {null|boolean} [show=null] true to show the avatar * message or false to hide it. If not provided then the connection * status of the user currently on the large video will be obtained form * "APP.conference" and the message will be displayed if the user's - * connection is interrupted. + * connection is either interrupted or inactive. */ showRemoteConnectionMessage (show) { if (typeof show !== 'boolean') { - show = !this._isConnectionActive(this.id); + const connStatus + = APP.conference.getParticipantConnectionStatus(this.id); + + show = !APP.conference.isLocalId(this.id) + && (connStatus === ParticipantConnectionStatus.INTERRUPTED + || connStatus === ParticipantConnectionStatus.INACTIVE); } if (show) { @@ -526,7 +546,7 @@ export default class LargeVideoManager { // FIXME when video is being replaced with other content we need to hide // companion icons/messages. It would be best if the container would // be taking care of it by itself, but that is a bigger refactoring - if (this.state === VIDEO_CONTAINER_TYPE) { + if (LargeVideoManager.isVideoContainer(this.state)) { this.showWatermark(false); this.showLocalConnectionMessage(false); this.showRemoteConnectionMessage(false); @@ -537,7 +557,7 @@ export default class LargeVideoManager { let container = this.getContainer(type); return container.show().then(() => { - if (type === VIDEO_CONTAINER_TYPE) { + if (LargeVideoManager.isVideoContainer(type)) { // FIXME when video appears on top of other content we need to // show companion icons/messages. It would be best if // the container would be taking care of it by itself, but that diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 36da7bc98..a8beb3835 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -448,29 +448,27 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) { /** * @inheritDoc + * @override */ -RemoteVideo.prototype.setMutedView = function(isMuted) { - SmallVideo.prototype.setMutedView.call(this, isMuted); +RemoteVideo.prototype.setVideoMutedView = function(isMuted) { + SmallVideo.prototype.setVideoMutedView.call(this, isMuted); // Update 'mutedWhileDisconnected' flag - this._figureOutMutedWhileDisconnected(this.isConnectionInterrupted()); + this._figureOutMutedWhileDisconnected(); }; /** * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into * account remote participant's network connectivity and video muted status. * - * @param {boolean} isDisconnected true if the remote participant is - * currently having connectivity issues or false otherwise. - * * @private */ -RemoteVideo.prototype._figureOutMutedWhileDisconnected - = function(isDisconnected) { - if (isDisconnected && this.isVideoMuted) { - this.mutedWhileDisconnected = true; - } else if (!isDisconnected && !this.isVideoMuted) { - this.mutedWhileDisconnected = false; - } +RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() { + const isActive = this.isConnectionActive(); + if (!isActive && this.isVideoMuted) { + this.mutedWhileDisconnected = true; + } else if (isActive && !this.isVideoMuted) { + this.mutedWhileDisconnected = false; + } }; /** @@ -572,26 +570,25 @@ RemoteVideo.prototype.updateView = function () { * Updates the UI to reflect user's connectivity status. */ RemoteVideo.prototype.updateConnectionStatusIndicator = function () { - const isActive = this.isConnectionActive(); + const connectionStatus = this.user.getConnectionStatus(); - if (isActive === null) { - // Cancel processing at this point - no update - return; + logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`); + + // FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering' + // Update 'mutedWhileDisconnected' flag + this._figureOutMutedWhileDisconnected(); + if(this.connectionIndicator) { + this.connectionIndicator.updateConnectionStatusIndicator( + connectionStatus); } - logger.debug(this.id + " thumbnail is connection active ? " + isActive); - - // Update 'mutedWhileDisconnected' flag - this._figureOutMutedWhileDisconnected(!isActive); - - if(this.connectionIndicator) - this.connectionIndicator.updateConnectionStatusIndicator(isActive); - + const isInterrupted + = connectionStatus === ParticipantConnectionStatus.INTERRUPTED; // Toggle thumbnail video problem filter this.selectVideoElement().toggleClass( - "videoThumbnailProblemFilter", !isActive); + "videoThumbnailProblemFilter", isInterrupted); this.$avatar().toggleClass( - "videoThumbnailProblemFilter", !isActive); + "videoThumbnailProblemFilter", isInterrupted); }; /** diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 81d313cbb..54a4da53e 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -6,6 +6,8 @@ import UIUtil from "../util/UIUtil"; import UIEvents from "../../../service/UI/UIEvents"; import AudioLevels from "../audio_levels/AudioLevels"; +const ParticipantConnectionStatus + = JitsiMeetJS.constants.participantConnectionStatus; const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper; /** @@ -444,9 +446,13 @@ SmallVideo.prototype.isCurrentlyOnLargeVideo = function () { * or false otherwise. */ SmallVideo.prototype.isVideoPlayable = function() { + const connectionState + = APP.conference.getParticipantConnectionStatus(this.id); + return this.videoStream // Is there anything to display ? && !this.isVideoMuted && !this.videoStream.isMuted() // Muted ? - && (this.isLocal || APP.conference.isInLastN(this.id)); + && (this.isLocal + || connectionState === ParticipantConnectionStatus.ACTIVE); }; /** diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index e6038e186..caa00615d 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -1,4 +1,4 @@ -/* global APP, $, interfaceConfig */ +/* global APP, $, interfaceConfig, JitsiMeetJS */ const logger = require("jitsi-meet-logger").getLogger(__filename); import Filmstrip from "./Filmstrip"; @@ -10,6 +10,9 @@ import LargeVideoManager from "./LargeVideoManager"; import {VIDEO_CONTAINER_TYPE} from "./VideoContainer"; import LocalVideo from "./LocalVideo"; +const ParticipantConnectionStatus + = JitsiMeetJS.constants.participantConnectionStatus; + var remoteVideos = {}; var localVideoThumbnail = null; @@ -559,8 +562,16 @@ var VideoLayout = { * is fine. */ showLocalConnectionInterrupted (isInterrupted) { - localVideoThumbnail.connectionIndicator - .updateConnectionStatusIndicator(!isInterrupted); + // Currently local video thumbnail displays only "active" or + // "interrupted" despite the fact that ConnectionIndicator supports more + // states. + const status + = isInterrupted + ? ParticipantConnectionStatus.INTERRUPTED + : ParticipantConnectionStatus.ACTIVE; + + localVideoThumbnail + .connectionIndicator.updateConnectionStatusIndicator(status); }, /**