diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index ee1d49629..7dbc3528b 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -450,6 +450,11 @@ filter: grayscale(.5) opacity(0.8); } +.remoteVideoProblemFilter { + -webkit-filter: grayscale(100%); + filter: grayscale(100%); +} + .videoProblemFilter { -webkit-filter: blur(10px) grayscale(.5) opacity(0.8); filter: blur(10px) grayscale(.5) opacity(0.8); @@ -460,6 +465,28 @@ filter: grayscale(100%); } +#remoteConnectionMessage { + display: none; + position: absolute; + width: auto; + z-index: 1011; + font-weight: 600; + font-size: 14px; + text-align: center; + color: #FFF; + opacity: .80; + text-shadow: 0px 0px 1px rgba(0,0,0,0.3), + 0px 1px 1px rgba(0,0,0,0.3), + 1px 0px 1px rgba(0,0,0,0.3), + 0px 0px 1px rgba(0,0,0,0.3); + + background: rgba(0,0,0,.5); + border-radius: 5px; + padding: 5px; + padding-left: 10px; + padding-right: 10px; +} + #videoConnectionMessage { display: none; position: absolute; diff --git a/index.html b/index.html index 48c8e1e3a..24a009220 100644 --- a/index.html +++ b/index.html @@ -228,6 +228,7 @@ +
diff --git a/lang/main.json b/lang/main.json index 0e5f69792..d891fb625 100644 --- a/lang/main.json +++ b/lang/main.json @@ -324,7 +324,8 @@ "ATTACHED": "Attached", "FETCH_SESSION_ID": "Obtaining session-id...", "GOT_SESSION_ID": "Obtaining session-id... Done", - "GET_SESSION_ID_ERROR": "Get session-id error: " + "GET_SESSION_ID_ERROR": "Get session-id error: ", + "USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..." }, "recording": { diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index ba5ee7264..56bb9ccdb 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -121,7 +121,8 @@ export default class LargeVideoManager { // Include hide()/fadeOut only if we're switching between users let preUpdate; - if (this.newStreamData.id != this.id) { + let isUserSwitch = this.newStreamData.id != this.id; + if (isUserSwitch) { preUpdate = container.hide(); } else { preUpdate = Promise.resolve(); @@ -146,25 +147,46 @@ export default class LargeVideoManager { // If we the continer 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 isVideoMuted = false; + let showAvatar = false; if (videoType == VIDEO_CONTAINER_TYPE) - isVideoMuted = stream ? stream.isMuted() : true; + showAvatar = stream ? stream.isMuted() : true; - // show the avatar on large if needed - container.showAvatar(isVideoMuted); + // 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 + // the video was not rendered, before the connection has failed. + let isHavingConnectivityIssues + = APP.conference.isParticipantConnectionActive(id) === false; + if (isHavingConnectivityIssues + && (isUserSwitch | !container.wasVideoRendered)) { + showAvatar = true; + } let promise; // do not show stream if video is muted // but we still should show watermark - if (isVideoMuted) { + if (showAvatar) { this.showWatermark(true); - // If the avatar is to be displayed the video should be hidden + // If the intention of this switch is to show the avatar + // we need to make sure that the video is hidden promise = container.hide(); } else { promise = container.show(); } + // show the avatar on large if needed + container.showAvatar(showAvatar); + + // Make sure no notification about remote failure is shown as + // it's UI conflicts with the one for local connection interrupted. + if (APP.conference.isConnectionInterrupted()) { + this.updateParticipantConnStatusIndication(id, true); + } else { + this.updateParticipantConnStatusIndication( + id, !isHavingConnectivityIssues); + } + // resolve updateLargeVideo promise after everything is done promise.then(resolve); @@ -177,6 +199,38 @@ export default class LargeVideoManager { }); } + /** + * Shows/hides notification about participant's connectivity issues to be + * 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. + * + * @private + */ + updateParticipantConnStatusIndication (id, isConnected) { + + // Apply grey filter on the large video + this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected); + + if (isConnected) { + // Hide the message + this.showRemoteConnectionMessage(false); + } else { + // Get user's display name + let displayName + = APP.conference.getParticipantDisplayName(id); + this._setRemoteConnectionMessage( + "connection.USER_CONNECTION_INTERRUPTED", + { displayName: displayName }); + + // Show it now only if the VideoContainer is on top + this.showRemoteConnectionMessage( + this.state === VIDEO_CONTAINER_TYPE); + } + } + /** * Update large video. * Switches to large video even if previously other container was visible. @@ -274,11 +328,59 @@ export default class LargeVideoManager { if (show) { $('#videoConnectionMessage').css({display: "block"}); + // Avatar message conflicts with 'videoConnectionMessage', + // so it must be hidden + this.showRemoteConnectionMessage(false); } else { $('#videoConnectionMessage').css({display: "none"}); } } + /** + * 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 + * 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. + */ + showRemoteConnectionMessage (show) { + if (typeof show !== 'boolean') { + show = APP.conference.isParticipantConnectionActive(this.id); + } + + if (show) { + $('#remoteConnectionMessage').css({display: "block"}); + // 'videoConnectionMessage' message conflicts with 'avatarMessage', + // so it must be hidden + this.showVideoConnectionMessage(false); + } else { + $('#remoteConnectionMessage').hide(); + } + } + + /** + * Updates the text which describes that the remote user is having + * connectivity issues. + * + * @param {string} msgKey the translation key which will be used to get + * the message text. + * @param {object} msgOptions translation options object. + * + * @private + */ + _setRemoteConnectionMessage (msgKey, msgOptions) { + if (msgKey) { + let text = APP.translation.translateString(msgKey, msgOptions); + $('#remoteConnectionMessage') + .attr("data-i18n", msgKey).text(text); + } + + this.videoContainer.positionRemoteConnectionMessage(); + } + /** * Updated the text which is to be shown on the top of large video. * @@ -353,6 +455,7 @@ export default class LargeVideoManager { if (this.state === VIDEO_CONTAINER_TYPE) { this.showWatermark(false); this.showVideoConnectionMessage(false); + this.showRemoteConnectionMessage(false); } oldContainer.hide(); @@ -366,6 +469,10 @@ export default class LargeVideoManager { // the container would be taking care of it by itself, but that // is a bigger refactoring this.showWatermark(true); + // "avatar" and "video connection" can not be displayed both + // at the same time, but the latter is of higher priority and it + // will hide the avatar one if will be displayed. + this.showRemoteConnectionMessage(/* fet the current state */); this.showVideoConnectionMessage(/* fetch the current state */); } }); diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index e2a7bd3eb..5ebfe979c 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -173,8 +173,19 @@ export class VideoContainer extends LargeContainer { this.isVisible = false; + /** + * Flag indicates whether or not the avatar is currently displayed. + * @type {boolean} + */ + this.avatarDisplayed = false; this.$avatar = $('#dominantSpeaker'); + /** + * A jQuery selector of the remote connection message. + * @type {jQuery|HTMLElement} + */ + this.$remoteConnectionMessage = $('#remoteConnectionMessage'); + /** * Indicates whether or not the video stream attached to the video * element has started(which means that there is any image rendered @@ -266,6 +277,30 @@ export class VideoContainer extends LargeContainer { } } + /** + * Update position of the remote connection message which describes that + * the remote user is having connectivity issues. + */ + positionRemoteConnectionMessage () { + + if (this.avatarDisplayed) { + let $avatarImage = $("#dominantSpeakerAvatar"); + this.$remoteConnectionMessage.css( + 'top', + $avatarImage.offset().top + $avatarImage.height() + 10); + } else { + let height = this.$remoteConnectionMessage.height(); + let parentHeight = this.$remoteConnectionMessage.parent().height(); + this.$remoteConnectionMessage.css( + 'top', (parentHeight/2) - (height/2)); + } + + let width = this.$remoteConnectionMessage.width(); + let parentWidth = this.$remoteConnectionMessage.parent().width(); + this.$remoteConnectionMessage.css( + 'left', ((parentWidth/2) - (width/2))); + } + resize (containerWidth, containerHeight, animate = false) { let [width, height] = this.getVideoSize(containerWidth, containerHeight); @@ -278,6 +313,8 @@ export class VideoContainer extends LargeContainer { this.$avatar.css('top', top); + this.positionRemoteConnectionMessage(); + this.$wrapper.animate({ width: width, height: height, @@ -362,10 +399,23 @@ export class VideoContainer extends LargeContainer { (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000"); this.$avatar.css("visibility", show ? "visible" : "hidden"); + this.avatarDisplayed = show; this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show); } + /** + * Indicates that the remote user who is currently displayed by this video + * container is having connectivity issues. + * + * @param {boolean} show true to show or false to hide + * the indication. + */ + showRemoteConnectionProblemIndicator (show) { + this.$video.toggleClass("remoteVideoProblemFilter", show); + this.$avatar.toggleClass("remoteVideoProblemFilter", show); + } + // We are doing fadeOut/fadeIn animations on parent div which wraps // largeVideo, because when Temasys plugin is in use it replaces //