feat(VideoLayout): add remote connection problems UI

Grey filter will be applied to the remote video/avatar displayed on
"large" and a message indicating remote connectivity issues will be
shown on top of that.
This commit is contained in:
paweldomas 2016-09-24 13:24:18 -05:00
parent 42fd3097de
commit 661ea2cf45
6 changed files with 200 additions and 8 deletions

View File

@ -450,6 +450,11 @@
filter: grayscale(.5) opacity(0.8); filter: grayscale(.5) opacity(0.8);
} }
.remoteVideoProblemFilter {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
.videoProblemFilter { .videoProblemFilter {
-webkit-filter: blur(10px) grayscale(.5) opacity(0.8); -webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
filter: blur(10px) grayscale(.5) opacity(0.8); filter: blur(10px) grayscale(.5) opacity(0.8);
@ -460,6 +465,28 @@
filter: grayscale(100%); 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 { #videoConnectionMessage {
display: none; display: none;
position: absolute; position: absolute;

View File

@ -228,6 +228,7 @@
<img id="dominantSpeakerAvatar" src=""/> <img id="dominantSpeakerAvatar" src=""/>
<canvas id="dominantSpeakerAudioLevel"></canvas> <canvas id="dominantSpeakerAudioLevel"></canvas>
</div> </div>
<span id="remoteConnectionMessage"></span>
<div id="largeVideoWrapper"> <div id="largeVideoWrapper">
<video id="largeVideo" muted="true" autoplay></video> <video id="largeVideo" muted="true" autoplay></video>
</div> </div>

View File

@ -324,7 +324,8 @@
"ATTACHED": "Attached", "ATTACHED": "Attached",
"FETCH_SESSION_ID": "Obtaining session-id...", "FETCH_SESSION_ID": "Obtaining session-id...",
"GOT_SESSION_ID": "Obtaining session-id... Done", "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": "recording":
{ {

View File

@ -121,7 +121,8 @@ export default class LargeVideoManager {
// Include hide()/fadeOut only if we're switching between users // Include hide()/fadeOut only if we're switching between users
let preUpdate; let preUpdate;
if (this.newStreamData.id != this.id) { let isUserSwitch = this.newStreamData.id != this.id;
if (isUserSwitch) {
preUpdate = container.hide(); preUpdate = container.hide();
} else { } else {
preUpdate = Promise.resolve(); preUpdate = Promise.resolve();
@ -146,25 +147,46 @@ export default class LargeVideoManager {
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check // If we the continer is VIDEO_CONTAINER_TYPE, we need to check
// its stream whether exist and is muted to set isVideoMuted // its stream whether exist and is muted to set isVideoMuted
// in rest of the cases it is false // in rest of the cases it is false
let isVideoMuted = false; let showAvatar = false;
if (videoType == VIDEO_CONTAINER_TYPE) if (videoType == VIDEO_CONTAINER_TYPE)
isVideoMuted = stream ? stream.isMuted() : true; showAvatar = stream ? stream.isMuted() : true;
// show the avatar on large if needed // If the user's connection is disrupted then the avatar will be
container.showAvatar(isVideoMuted); // 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; let promise;
// do not show stream if video is muted // do not show stream if video is muted
// but we still should show watermark // but we still should show watermark
if (isVideoMuted) { if (showAvatar) {
this.showWatermark(true); 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(); promise = container.hide();
} else { } else {
promise = container.show(); 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 // resolve updateLargeVideo promise after everything is done
promise.then(resolve); 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. * Update large video.
* Switches to large video even if previously other container was visible. * Switches to large video even if previously other container was visible.
@ -274,11 +328,59 @@ export default class LargeVideoManager {
if (show) { if (show) {
$('#videoConnectionMessage').css({display: "block"}); $('#videoConnectionMessage').css({display: "block"});
// Avatar message conflicts with 'videoConnectionMessage',
// so it must be hidden
this.showRemoteConnectionMessage(false);
} else { } else {
$('#videoConnectionMessage').css({display: "none"}); $('#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) <tt>true</tt> to show the avatar
* message or <tt>false</tt> 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. * 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) { if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false); this.showWatermark(false);
this.showVideoConnectionMessage(false); this.showVideoConnectionMessage(false);
this.showRemoteConnectionMessage(false);
} }
oldContainer.hide(); oldContainer.hide();
@ -366,6 +469,10 @@ export default class LargeVideoManager {
// the container would be taking care of it by itself, but that // the container would be taking care of it by itself, but that
// is a bigger refactoring // is a bigger refactoring
this.showWatermark(true); 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 */); this.showVideoConnectionMessage(/* fetch the current state */);
} }
}); });

View File

@ -173,8 +173,19 @@ export class VideoContainer extends LargeContainer {
this.isVisible = false; this.isVisible = false;
/**
* Flag indicates whether or not the avatar is currently displayed.
* @type {boolean}
*/
this.avatarDisplayed = false;
this.$avatar = $('#dominantSpeaker'); 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 * Indicates whether or not the video stream attached to the video
* element has started(which means that there is any image rendered * 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) { resize (containerWidth, containerHeight, animate = false) {
let [width, height] let [width, height]
= this.getVideoSize(containerWidth, containerHeight); = this.getVideoSize(containerWidth, containerHeight);
@ -278,6 +313,8 @@ export class VideoContainer extends LargeContainer {
this.$avatar.css('top', top); this.$avatar.css('top', top);
this.positionRemoteConnectionMessage();
this.$wrapper.animate({ this.$wrapper.animate({
width: width, width: width,
height: height, height: height,
@ -362,10 +399,23 @@ export class VideoContainer extends LargeContainer {
(show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000"); (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
this.$avatar.css("visibility", show ? "visible" : "hidden"); this.$avatar.css("visibility", show ? "visible" : "hidden");
this.avatarDisplayed = show;
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, 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 <tt>true</tt> to show or <tt>false</tt> 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 // We are doing fadeOut/fadeIn animations on parent div which wraps
// largeVideo, because when Temasys plugin is in use it replaces // largeVideo, because when Temasys plugin is in use it replaces
// <video> elements with plugin <object> tag. In Safari jQuery is // <video> elements with plugin <object> tag. In Safari jQuery is

View File

@ -650,6 +650,12 @@ var VideoLayout = {
* the user is having connectivity issues. * the user is having connectivity issues.
*/ */
onParticipantConnectionStatusChanged (id, isActive) { onParticipantConnectionStatusChanged (id, isActive) {
// Show/hide warning on the large video
if (this.isCurrentlyOnLarge(id)) {
if (largeVideo) {
largeVideo.updateParticipantConnStatusIndication(id, isActive);
}
}
// Show/hide warning on the thumbnail // Show/hide warning on the thumbnail
let remoteVideo = remoteVideos[id]; let remoteVideo = remoteVideos[id];
if (remoteVideo) { if (remoteVideo) {