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.
pull/1566/merge
paweldomas 8 years ago committed by yanas
parent 5c5864e94a
commit 12d7e61362
  1. 12
      conference.js
  2. 5
      css/_videolayout_default.scss
  3. 35
      modules/UI/videolayout/ConnectionIndicator.js
  4. 122
      modules/UI/videolayout/LargeVideoManager.js
  5. 43
      modules/UI/videolayout/RemoteVideo.js
  6. 8
      modules/UI/videolayout/SmallVideo.js
  7. 17
      modules/UI/videolayout/VideoLayout.js

@ -2125,18 +2125,6 @@ export default {
eventEmitter.removeListener(eventName, listener); 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 * Changes the display name for the local user
* @param nickname {string} the new display name * @param nickname {string} the new display name

@ -97,6 +97,11 @@
color: #FFFFFF;/*#15A1ED*/ color: #FFFFFF;/*#15A1ED*/
overflow: hidden; overflow: hidden;
} }
&_ninja
{
font-size: 1.5em;
}
} }
.icon-connection, .icon-connection,

@ -1,9 +1,12 @@
/* global $, APP, interfaceConfig */ /* global $, APP, interfaceConfig, JitsiMeetJS */
/* jshint -W101 */ /* jshint -W101 */
import JitsiPopover from "../util/JitsiPopover"; import JitsiPopover from "../util/JitsiPopover";
import UIUtil from "../util/UIUtil"; import UIUtil from "../util/UIUtil";
const ParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus;
/** /**
* Maps a connection quality value (in percent) to the width of the "full" icon. * 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")); createIcon(["connection_full"], "icon-connection"));
this.interruptedIndicator = connectionIconContainer.appendChild( this.interruptedIndicator = connectionIconContainer.appendChild(
createIcon(["connection_lost"],"icon-connection-lost")); createIcon(["connection_lost"],"icon-connection-lost"));
this.ninjaIndicator = connectionIconContainer.appendChild(
createIcon(["connection_ninja"],"icon-ninja"));
$(this.interruptedIndicator).hide(); $(this.interruptedIndicator).hide();
$(this.ninjaIndicator).hide();
this.connectionIndicatorContainer.appendChild(connectionIconContainer); this.connectionIndicatorContainer.appendChild(connectionIconContainer);
}; };
@ -351,22 +357,29 @@ 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 * @param {ParticipantConnectionStatus} connectionStatus
* the user is having connectivity issues.
*/ */
ConnectionIndicator.prototype.updateConnectionStatusIndicator ConnectionIndicator.prototype.updateConnectionStatusIndicator
= function (isActive) { = function (connectionStatus) {
this.isConnectionActive = isActive; this.connectionStatus = connectionStatus;
if (this.isConnectionActive) { if (connectionStatus === ParticipantConnectionStatus.INTERRUPTED) {
$(this.interruptedIndicator).hide();
$(this.emptyIcon).show();
$(this.fullIcon).show();
} else {
$(this.interruptedIndicator).show(); $(this.interruptedIndicator).show();
$(this.emptyIcon).hide(); $(this.emptyIcon).hide();
$(this.fullIcon).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();
} }
}; };

@ -26,6 +26,18 @@ const VIDEO_RESOLUTION_POLL_INTERVAL = 2000;
* Manager for all Large containers. * Manager for all Large containers.
*/ */
export default class LargeVideoManager { 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) { constructor (emitter) {
/** /**
* The map of <tt>LargeContainer</tt>s where the key is the video * The map of <tt>LargeContainer</tt>s where the key is the video
@ -116,7 +128,8 @@ export default class LargeVideoManager {
this.enableLocalConnectionProblemFilter(true); this.enableLocalConnectionProblemFilter(true);
this._setLocalConnectionMessage("connection.RECONNECTING"); this._setLocalConnectionMessage("connection.RECONNECTING");
// Show the message only if the video is currently being displayed // 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(() => { preUpdate.then(() => {
const { id, stream, videoType, resolve } = this.newStreamData; 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; this.newStreamData = null;
@ -158,34 +176,26 @@ export default class LargeVideoManager {
// change the avatar url on large // change the avatar url on large
this.updateAvatar(Avatar.getAvatarUrl(id)); 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 // If the user's connection is disrupted then the avatar will be
// displayed in case we have no video image cached. That is if // 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. // the video was not rendered, before the connection has failed.
const isConnectionActive = this._isConnectionActive(id); const wasUsersImageCached
= !isUserSwitch && container.wasVideoRendered;
if (isVideoFromCamera const isVideoMuted = !stream || stream.isMuted();
&& !isConnectionActive
&& (isUserSwitch || !container.wasVideoRendered)) { const connectionStatus
showAvatar = true; = APP.conference.getParticipantConnectionStatus(id);
} const isVideoRenderable
= !isVideoMuted
// If audio only mode is enabled, always show the avatar for && (APP.conference.isLocalId(id)
// videos from another participant. || connectionStatus
if (APP.conference.isAudioOnly() === ParticipantConnectionStatus.ACTIVE
&& (isVideoFromCamera || wasUsersImageCached);
|| videoType === DESKTOP_CONTAINER_TYPE)) {
showAvatar = true; let showAvatar
} = isVideoContainer
&& (APP.conference.isAudioOnly() || !isVideoRenderable);
let promise; let promise;
@ -208,28 +218,31 @@ export default class LargeVideoManager {
this.updateLargeVideoAudioLevel(0); 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 // Make sure no notification about remote failure is shown as
// its UI conflicts with the one for local connection interrupted. // its UI conflicts with the one for local connection interrupted.
// For the purposes of UI indicators, audio only is considered as // For the purposes of UI indicators, audio only is considered as
// an "active" connection. // an "active" connection.
const isConnected const overrideAndHide
= APP.conference.isAudioOnly() = APP.conference.isAudioOnly()
|| APP.conference.isConnectionInterrupted() || 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;
this.updateParticipantConnStatusIndication( this.updateParticipantConnStatusIndication(
id, id,
isConnected, !overrideAndHide && isConnectionInterrupted,
(isConnectionInterrupted) !overrideAndHide && messageKey !== null,
? "connection.USER_CONNECTION_INTERRUPTED" messageKey);
: "connection.LOW_BANDWIDTH");
// resolve updateLargeVideo promise after everything is done // resolve updateLargeVideo promise after everything is done
promise.then(resolve); promise.then(resolve);
@ -265,18 +278,20 @@ export default class LargeVideoManager {
* shown on the large video area. * shown on the large video area.
* *
* @param {string} id the id of remote participant(MUC nickname) * @param {string} id the id of remote participant(MUC nickname)
* @param {boolean} isConnected true if the connection is active or false * @param {boolean} showProblemsIndication
* when the user is having connectivity issues. * @param {boolean} showMessage
* @param {string} messageKey the i18n key of the message * @param {string} messageKey the i18n key of the message
* *
* @private * @private
*/ */
updateParticipantConnStatusIndication (id, isConnected, messageKey) { updateParticipantConnStatusIndication (
id, showProblemsIndication, showMessage, messageKey) {
// Apply grey filter on the large video // Apply grey filter on the large video
this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected); this.videoContainer.showRemoteConnectionProblemIndicator(
showProblemsIndication);
if (isConnected) { if (!showMessage) {
// Hide the message // Hide the message
this.showRemoteConnectionMessage(false); this.showRemoteConnectionMessage(false);
} else { } else {
@ -289,7 +304,7 @@ export default class LargeVideoManager {
// Show it now only if the VideoContainer is on top // Show it now only if the VideoContainer is on top
this.showRemoteConnectionMessage( 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 * Shows hides the "avatar" message which is to be displayed either in
* the middle of the screen or below the avatar image. * the middle of the screen or below the avatar image.
* *
* @param {null|boolean} show (optional) <tt>true</tt> to show the avatar * @param {null|boolean} [show=null] <tt>true</tt> to show the avatar
* message or <tt>false</tt> to hide it. If not provided then the connection * 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 * 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 * "APP.conference" and the message will be displayed if the user's
* connection is interrupted. * connection is either interrupted or inactive.
*/ */
showRemoteConnectionMessage (show) { showRemoteConnectionMessage (show) {
if (typeof show !== 'boolean') { 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) { if (show) {
@ -526,7 +546,7 @@ export default class LargeVideoManager {
// FIXME when video is being replaced with other content we need to hide // FIXME when video is being replaced with other content we need to hide
// companion icons/messages. It would be best if the container would // companion icons/messages. It would be best if the container would
// be taking care of it by itself, but that is a bigger refactoring // 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.showWatermark(false);
this.showLocalConnectionMessage(false); this.showLocalConnectionMessage(false);
this.showRemoteConnectionMessage(false); this.showRemoteConnectionMessage(false);
@ -537,7 +557,7 @@ export default class LargeVideoManager {
let container = this.getContainer(type); let container = this.getContainer(type);
return container.show().then(() => { 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 // FIXME when video appears on top of other content we need to
// show companion icons/messages. It would be best if // show companion icons/messages. It would be best if
// the container would be taking care of it by itself, but that // the container would be taking care of it by itself, but that

@ -448,27 +448,25 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
/** /**
* @inheritDoc * @inheritDoc
* @override
*/ */
RemoteVideo.prototype.setMutedView = function(isMuted) { RemoteVideo.prototype.setVideoMutedView = function(isMuted) {
SmallVideo.prototype.setMutedView.call(this, isMuted); SmallVideo.prototype.setVideoMutedView.call(this, isMuted);
// Update 'mutedWhileDisconnected' flag // Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected(this.isConnectionInterrupted()); this._figureOutMutedWhileDisconnected();
}; };
/** /**
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into * Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
* account remote participant's network connectivity and video muted status. * account remote participant's network connectivity and video muted status.
* *
* @param {boolean} isDisconnected <tt>true</tt> if the remote participant is
* currently having connectivity issues or <tt>false</tt> otherwise.
*
* @private * @private
*/ */
RemoteVideo.prototype._figureOutMutedWhileDisconnected RemoteVideo.prototype._figureOutMutedWhileDisconnected = function() {
= function(isDisconnected) { const isActive = this.isConnectionActive();
if (isDisconnected && this.isVideoMuted) { if (!isActive && this.isVideoMuted) {
this.mutedWhileDisconnected = true; this.mutedWhileDisconnected = true;
} else if (!isDisconnected && !this.isVideoMuted) { } else if (isActive && !this.isVideoMuted) {
this.mutedWhileDisconnected = false; this.mutedWhileDisconnected = false;
} }
}; };
@ -572,26 +570,25 @@ RemoteVideo.prototype.updateView = function () {
* Updates the UI to reflect user's connectivity status. * Updates the UI to reflect user's connectivity status.
*/ */
RemoteVideo.prototype.updateConnectionStatusIndicator = function () { RemoteVideo.prototype.updateConnectionStatusIndicator = function () {
const isActive = this.isConnectionActive(); const connectionStatus = this.user.getConnectionStatus();
if (isActive === null) { logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
// Cancel processing at this point - no update
return;
}
logger.debug(this.id + " thumbnail is connection active ? " + isActive);
// FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering'
// Update 'mutedWhileDisconnected' flag // Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected(!isActive); this._figureOutMutedWhileDisconnected();
if(this.connectionIndicator) {
if(this.connectionIndicator) this.connectionIndicator.updateConnectionStatusIndicator(
this.connectionIndicator.updateConnectionStatusIndicator(isActive); connectionStatus);
}
const isInterrupted
= connectionStatus === ParticipantConnectionStatus.INTERRUPTED;
// Toggle thumbnail video problem filter // Toggle thumbnail video problem filter
this.selectVideoElement().toggleClass( this.selectVideoElement().toggleClass(
"videoThumbnailProblemFilter", !isActive); "videoThumbnailProblemFilter", isInterrupted);
this.$avatar().toggleClass( this.$avatar().toggleClass(
"videoThumbnailProblemFilter", !isActive); "videoThumbnailProblemFilter", isInterrupted);
}; };
/** /**

@ -6,6 +6,8 @@ import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents"; import UIEvents from "../../../service/UI/UIEvents";
import AudioLevels from "../audio_levels/AudioLevels"; import AudioLevels from "../audio_levels/AudioLevels";
const ParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus;
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper; const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
/** /**
@ -444,9 +446,13 @@ SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
* or <tt>false</tt> otherwise. * or <tt>false</tt> otherwise.
*/ */
SmallVideo.prototype.isVideoPlayable = function() { SmallVideo.prototype.isVideoPlayable = function() {
const connectionState
= APP.conference.getParticipantConnectionStatus(this.id);
return this.videoStream // Is there anything to display ? return this.videoStream // Is there anything to display ?
&& !this.isVideoMuted && !this.videoStream.isMuted() // Muted ? && !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
&& (this.isLocal || APP.conference.isInLastN(this.id)); && (this.isLocal
|| connectionState === ParticipantConnectionStatus.ACTIVE);
}; };
/** /**

@ -1,4 +1,4 @@
/* global APP, $, interfaceConfig */ /* global APP, $, interfaceConfig, JitsiMeetJS */
const logger = require("jitsi-meet-logger").getLogger(__filename); const logger = require("jitsi-meet-logger").getLogger(__filename);
import Filmstrip from "./Filmstrip"; import Filmstrip from "./Filmstrip";
@ -10,6 +10,9 @@ import LargeVideoManager from "./LargeVideoManager";
import {VIDEO_CONTAINER_TYPE} from "./VideoContainer"; import {VIDEO_CONTAINER_TYPE} from "./VideoContainer";
import LocalVideo from "./LocalVideo"; import LocalVideo from "./LocalVideo";
const ParticipantConnectionStatus
= JitsiMeetJS.constants.participantConnectionStatus;
var remoteVideos = {}; var remoteVideos = {};
var localVideoThumbnail = null; var localVideoThumbnail = null;
@ -559,8 +562,16 @@ var VideoLayout = {
* is fine. * is fine.
*/ */
showLocalConnectionInterrupted (isInterrupted) { showLocalConnectionInterrupted (isInterrupted) {
localVideoThumbnail.connectionIndicator // Currently local video thumbnail displays only "active" or
.updateConnectionStatusIndicator(!isInterrupted); // "interrupted" despite the fact that ConnectionIndicator supports more
// states.
const status
= isInterrupted
? ParticipantConnectionStatus.INTERRUPTED
: ParticipantConnectionStatus.ACTIVE;
localVideoThumbnail
.connectionIndicator.updateConnectionStatusIndicator(status);
}, },
/** /**

Loading…
Cancel
Save