From 12d7e61362a9df4a20588d07371df779a3e76cbf Mon Sep 17 00:00:00 2001 From: paweldomas Date: Thu, 25 May 2017 08:48:06 -0500 Subject: [PATCH] feat(VideoLayout): add ninja icon Add ninja icon which wil be displayed when user's connection status is inactive. Apply grey filter only for interrupted state. Do not use isLastN directly, but check ParticipantConnectionStatus. --- conference.js | 12 -- css/_videolayout_default.scss | 5 + modules/UI/videolayout/ConnectionIndicator.js | 43 ++++--- modules/UI/videolayout/LargeVideoManager.js | 120 ++++++++++-------- modules/UI/videolayout/RemoteVideo.js | 51 ++++---- modules/UI/videolayout/SmallVideo.js | 8 +- modules/UI/videolayout/VideoLayout.js | 17 ++- 7 files changed, 148 insertions(+), 108 deletions(-) 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); }, /**