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
//