diff --git a/conference.js b/conference.js
index ed346eca2..c06c19181 100644
--- a/conference.js
+++ b/conference.js
@@ -691,6 +691,51 @@ export default {
isConnectionInterrupted () {
return connectionIsInterrupted;
},
+ /**
+ * Finds JitsiParticipant for given id.
+ *
+ * @param {string} id participant's identifier(MUC nickname).
+ *
+ * @returns {JitsiParticipant|null} participant instance for given id or
+ * null if not found.
+ */
+ getParticipantById (id) {
+ return room ? room.getParticipantById(id) : null;
+ },
+ /**
+ * Checks whether the user identified by given id is currently connected.
+ *
+ * @param {string} id participant's identifier(MUC nickname)
+ *
+ * @returns {boolean|null} true if participant's connection is ok or false
+ * if the user is having connectivity issues.
+ */
+ isParticipantConnectionActive (id) {
+ let participant = this.getParticipantById(id);
+ return participant ? participant.isConnectionActive() : null;
+ },
+ /**
+ * Gets the display name foe the JitsiParticipant identified by
+ * the given id.
+ *
+ * @param id {string} the participant's id(MUC nickname/JVB endpoint id)
+ *
+ * @return {string} the participant's display name or the default string if
+ * absent.
+ */
+ getParticipantDisplayName (id) {
+ let displayName = getDisplayName(id);
+ if (displayName) {
+ return displayName;
+ } else {
+ if (APP.conference.isLocalId(id)) {
+ return APP.translation.generateTranslationHTML(
+ interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
+ } else {
+ return interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
+ }
+ }
+ },
getMyUserId () {
return this._room
&& this._room.myUserId();
@@ -1085,7 +1130,7 @@ export default {
console.log('USER %s connnected', id, user);
APP.API.notifyUserJoined(id);
- APP.UI.addUser(id, user.getDisplayName());
+ APP.UI.addUser(user);
// check the roles for the new user and reflect them
APP.UI.updateUserRole(user);
@@ -1174,6 +1219,10 @@ export default {
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
APP.UI.handleLastNEndpoints(ids, enteringIds);
});
+ room.on(
+ ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED, (id, isActive) => {
+ APP.UI.participantConnectionStatusChanged(id, isActive);
+ });
room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
if (this.isLocalId(id)) {
this.isDominantSpeaker = true;
@@ -1205,10 +1254,12 @@ export default {
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
connectionIsInterrupted = true;
ConnectionQuality.updateLocalConnectionQuality(0);
+ APP.UI.showLocalConnectionInterrupted(true);
});
room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
connectionIsInterrupted = false;
+ APP.UI.showLocalConnectionInterrupted(false);
});
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss
index 9a276de2c..b5346e729 100644
--- a/css/_videolayout_default.scss
+++ b/css/_videolayout_default.scss
@@ -233,6 +233,12 @@
overflow: hidden;
}
+.connection.connection_lost
+{
+ color: #8B8B8B;
+ overflow: visible;
+}
+
.connection.connection_full
{
color: #FFFFFF;/*#15A1ED*/
@@ -456,12 +462,44 @@
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);
}
-#videoConnectionMessage {
+.videoThumbnailProblemFilter {
+ -webkit-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;
+}
+
+#localConnectionMessage {
display: none;
position: absolute;
width: 100%;
diff --git a/index.html b/index.html
index 48c8e1e3a..760cfd276 100644
--- a/index.html
+++ b/index.html
@@ -228,10 +228,11 @@
+
-
+
HD
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/UI.js b/modules/UI/UI.js
index eae77ddb8..bbd62e25f 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -261,6 +261,17 @@ UI.changeDisplayName = function (id, displayName) {
}
};
+/**
+ * Shows/hides the indication about local connection being interrupted.
+ *
+ * @param {boolean} isInterrupted true if local connection is
+ * currently in the interrupted state or false if the connection
+ * is fine.
+ */
+UI.showLocalConnectionInterrupted = function (isInterrupted) {
+ VideoLayout.showLocalConnectionInterrupted(isInterrupted);
+};
+
/**
* Sets the "raised hand" status for a participant.
*/
@@ -602,10 +613,11 @@ UI.getSharedDocumentManager = function () {
/**
* Show user on UI.
- * @param {string} id user id
- * @param {string} displayName user nickname
+ * @param {JitsiParticipant} user
*/
-UI.addUser = function (id, displayName) {
+UI.addUser = function (user) {
+ var id = user.getId();
+ var displayName = user.getDisplayName();
UI.hideRingOverLay();
ContactList.addContact(id);
@@ -618,7 +630,7 @@ UI.addUser = function (id, displayName) {
UIUtil.playSoundNotification('userJoined');
// Add Peer's container
- VideoLayout.addParticipantContainer(id);
+ VideoLayout.addParticipantContainer(user);
// Configure avatar
UI.setUserEmail(id);
@@ -983,6 +995,17 @@ UI.handleLastNEndpoints = function (ids, enteringIds) {
VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
};
+/**
+ * Will handle notification about participant's connectivity status change.
+ *
+ * @param {string} id the id of remote participant(MUC jid)
+ * @param {boolean} isActive true if the connection is ok or false if the user
+ * is having connectivity issues.
+ */
+UI.participantConnectionStatusChanged = function (id, isActive) {
+ VideoLayout.onParticipantConnectionStatusChanged(id, isActive);
+};
+
/**
* Update audio level visualization for specified user.
* @param {string} id user id
diff --git a/modules/UI/shared_video/SharedVideo.js b/modules/UI/shared_video/SharedVideo.js
index 92e153190..3d777c6ed 100644
--- a/modules/UI/shared_video/SharedVideo.js
+++ b/modules/UI/shared_video/SharedVideo.js
@@ -243,7 +243,7 @@ export default class SharedVideoManager {
let thumb = new SharedVideoThumb(self.url);
thumb.setDisplayName(player.getVideoData().title);
- VideoLayout.addParticipantContainer(self.url, thumb);
+ VideoLayout.addRemoteVideoContainer(self.url, thumb);
let iframe = player.getIframe();
self.sharedVideo = new SharedVideoContainer(
diff --git a/modules/UI/videolayout/ConnectionIndicator.js b/modules/UI/videolayout/ConnectionIndicator.js
index ff5eb9b61..ddcd65d4f 100644
--- a/modules/UI/videolayout/ConnectionIndicator.js
+++ b/modules/UI/videolayout/ConnectionIndicator.js
@@ -245,13 +245,13 @@ ConnectionIndicator.prototype.showMore = function () {
};
-function createIcon(classes) {
+function createIcon(classes, iconClass) {
var icon = document.createElement("span");
for(var i in classes) {
icon.classList.add(classes[i]);
}
icon.appendChild(
- document.createElement("i")).classList.add("icon-connection");
+ document.createElement("i")).classList.add(iconClass);
return icon;
}
@@ -282,9 +282,12 @@ ConnectionIndicator.prototype.create = function () {
}.bind(this);
this.emptyIcon = this.connectionIndicatorContainer.appendChild(
- createIcon(["connection", "connection_empty"]));
+ createIcon(["connection", "connection_empty"], "icon-connection"));
this.fullIcon = this.connectionIndicatorContainer.appendChild(
- createIcon(["connection", "connection_full"]));
+ createIcon(["connection", "connection_full"], "icon-connection"));
+ this.interruptedIndicator = this.connectionIndicatorContainer.appendChild(
+ createIcon(["connection", "connection_lost"],"icon-connection-lost"));
+ $(this.interruptedIndicator).hide();
};
/**
@@ -298,6 +301,27 @@ ConnectionIndicator.prototype.remove = function() {
this.popover.forceHide();
};
+/**
+ * Updates the UI which displays 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.
+ */
+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();
+ this.updateConnectionQuality(0 /* zero bars */);
+ }
+};
+
/**
* Updates the data of the indicator
* @param percent the percent of connection quality
@@ -314,12 +338,14 @@ ConnectionIndicator.prototype.updateConnectionQuality =
this.connectionIndicatorContainer.style.display = "block";
}
}
- this.bandwidth = object.bandwidth;
- this.bitrate = object.bitrate;
- this.packetLoss = object.packetLoss;
- this.transport = object.transport;
- if (object.resolution) {
- this.resolution = object.resolution;
+ if (object) {
+ this.bandwidth = object.bandwidth;
+ this.bitrate = object.bitrate;
+ this.packetLoss = object.packetLoss;
+ this.transport = object.transport;
+ if (object.resolution) {
+ this.resolution = object.resolution;
+ }
}
for (var quality in ConnectionIndicator.connectionQualityValues) {
if (percent >= quality) {
@@ -327,7 +353,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
ConnectionIndicator.connectionQualityValues[quality];
}
}
- if (object.isResolutionHD) {
+ if (object && typeof object.isResolutionHD === 'boolean') {
this.isResolutionHD = object.isResolutionHD;
}
this.updateResolutionIndicator();
diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js
index e0934ee74..4a342f1d2 100644
--- a/modules/UI/videolayout/LargeVideoManager.js
+++ b/modules/UI/videolayout/LargeVideoManager.js
@@ -5,12 +5,18 @@ import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';
import UIUtil from "../util/UIUtil";
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
+import LargeContainer from "./LargeContainer";
/**
* Manager for all Large containers.
*/
export default class LargeVideoManager {
constructor (emitter) {
+ /**
+ * The map of LargeContainers where the key is the video
+ * container type.
+ * @type {Object.}
+ */
this.containers = {};
this.state = VIDEO_CONTAINER_TYPE;
@@ -85,21 +91,18 @@ export default class LargeVideoManager {
* Called when the media connection has been interrupted.
*/
onVideoInterrupted () {
- this.enableVideoProblemFilter(true);
- let reconnectingKey = "connection.RECONNECTING";
- $('#videoConnectionMessage')
- .attr("data-i18n", reconnectingKey)
- .text(APP.translation.translateString(reconnectingKey));
+ this.enableLocalConnectionProblemFilter(true);
+ this._setLocalConnectionMessage("connection.RECONNECTING")
// Show the message only if the video is currently being displayed
- this.showVideoConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
+ this.showLocalConnectionMessage(this.state === VIDEO_CONTAINER_TYPE);
}
/**
* Called when the media connection has been restored.
*/
onVideoRestored () {
- this.enableVideoProblemFilter(false);
- this.showVideoConnectionMessage(false);
+ this.enableLocalConnectionProblemFilter(false);
+ this.showLocalConnectionMessage(false);
}
get id () {
@@ -118,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();
@@ -136,27 +140,53 @@ 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 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);
- promise = Promise.resolve();
+ // 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);
@@ -169,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.
@@ -229,12 +291,13 @@ export default class LargeVideoManager {
}
/**
- * Enables/disables the filter indicating a video problem to the user.
+ * Enables/disables the filter indicating a video problem to the user caused
+ * by the problems with local media connection.
*
* @param enable true to enable, false to disable
*/
- enableVideoProblemFilter (enable) {
- this.videoContainer.enableVideoProblemFilter(enable);
+ enableLocalConnectionProblemFilter (enable) {
+ this.videoContainer.enableLocalConnectionProblemFilter(enable);
}
/**
@@ -253,23 +316,87 @@ export default class LargeVideoManager {
}
/**
- * Shows/hides the "video connection message".
+ * Shows/hides the message indicating problems with local media connection.
* @param {boolean|null} show(optional) tells whether the message is to be
* displayed or not. If missing the condition will be based on the value
* obtained from {@link APP.conference.isConnectionInterrupted}.
*/
- showVideoConnectionMessage (show) {
+ showLocalConnectionMessage (show) {
if (typeof show !== 'boolean') {
show = APP.conference.isConnectionInterrupted();
}
if (show) {
- $('#videoConnectionMessage').css({display: "block"});
+ $('#localConnectionMessage').css({display: "block"});
+ // Avatar message conflicts with 'videoConnectionMessage',
+ // so it must be hidden
+ this.showRemoteConnectionMessage(false);
} else {
- $('#videoConnectionMessage').css({display: "none"});
+ $('#localConnectionMessage').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.showLocalConnectionMessage(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, when
+ * local media connection is interrupted.
+ *
+ * @param {string} msgKey the translation key which will be used to get
+ * the message text to be displayed on the large video.
+ * @param {object} msgOptions translation options object
+ *
+ * @private
+ */
+ _setLocalConnectionMessage (msgKey, msgOptions) {
+ $('#localConnectionMessage')
+ .attr("data-i18n", msgKey)
+ .text(APP.translation.translateString(msgKey, msgOptions));
+ }
+
/**
* Add container of specified type.
* @param {string} type container type
@@ -328,7 +455,8 @@ export default class LargeVideoManager {
// be taking care of it by itself, but that is a bigger refactoring
if (this.state === VIDEO_CONTAINER_TYPE) {
this.showWatermark(false);
- this.showVideoConnectionMessage(false);
+ this.showLocalConnectionMessage(false);
+ this.showRemoteConnectionMessage(false);
}
oldContainer.hide();
@@ -342,7 +470,11 @@ export default class LargeVideoManager {
// the container would be taking care of it by itself, but that
// is a bigger refactoring
this.showWatermark(true);
- this.showVideoConnectionMessage(/* fetch the current state */);
+ // "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.showLocalConnectionMessage(/* fetch the current state */);
}
});
}
diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js
index 9c2111e4e..1a070652b 100644
--- a/modules/UI/videolayout/LocalVideo.js
+++ b/modules/UI/videolayout/LocalVideo.js
@@ -201,7 +201,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
localVideoContainer.removeChild(localVideo);
// when removing only the video element and we are on stage
// update the stage
- if(this.VideoLayout.isCurrentlyOnLarge(this.id))
+ if(this.isCurrentlyOnLargeVideo())
this.VideoLayout.updateLargeVideo(this.id);
stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index 52a769c15..6ae3a1657 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -8,17 +8,45 @@ import UIUtils from "../util/UIUtil";
import UIEvents from '../../../service/UI/UIEvents';
import JitsiPopover from "../util/JitsiPopover";
-function RemoteVideo(id, VideoLayout, emitter) {
- this.id = id;
+/**
+ * Creates new instance of the RemoteVideo.
+ * @param user {JitsiParticipant} the user for whom remote video instance will
+ * be created.
+ * @param {VideoLayout} VideoLayout the video layout instance.
+ * @param {EventEmitter} emitter the event emitter which will be used by
+ * the new instance to emit events.
+ * @constructor
+ */
+function RemoteVideo(user, VideoLayout, emitter) {
+ this.user = user;
+ this.id = user.getId();
this.emitter = emitter;
- this.videoSpanId = `participant_${id}`;
+ this.videoSpanId = `participant_${this.id}`;
SmallVideo.call(this, VideoLayout);
this.hasRemoteVideoMenu = false;
this.addRemoteVideoContainer();
- this.connectionIndicator = new ConnectionIndicator(this, id);
+ this.connectionIndicator = new ConnectionIndicator(this, this.id);
this.setDisplayName();
this.flipX = false;
this.isLocal = false;
+ /**
+ * The flag is set to true after the 'onplay' event has been
+ * triggered on the current video element. It goes back to false
+ * when the stream is removed. It is used to determine whether the video
+ * playback has ever started.
+ * @type {boolean}
+ */
+ this.wasVideoPlayed = false;
+ /**
+ * The flag is set to true if remote participant's video gets muted
+ * during his media connection disruption. This is to prevent black video
+ * being render on the thumbnail, because even though once the video has
+ * been played the image usually remains on the video element it seems that
+ * after longer period of the video element being hidden this image can be
+ * lost.
+ * @type {boolean}
+ */
+ this.mutedWhileDisconnected = false;
}
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@@ -162,6 +190,33 @@ RemoteVideo.prototype.updateRemoteVideoMenu = function (isMuted, force) {
}
};
+/**
+ * @inheritDoc
+ */
+RemoteVideo.prototype.setMutedView = function(isMuted) {
+ SmallVideo.prototype.setMutedView.call(this, isMuted);
+ // Update 'mutedWhileDisconnected' flag
+ this._figureOutMutedWhileDisconnected(this.isConnectionActive() === false);
+}
+
+/**
+ * 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;
+ }
+}
+
/**
* Adds the remote video menu element for the given id in the
* given parentElement.
@@ -209,13 +264,88 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
var select = $('#' + elementID);
select.remove();
+ if (isVideo) {
+ this.wasVideoPlayed = false;
+ }
+
console.info((isVideo ? "Video" : "Audio") +
" removed " + this.id, select);
// when removing only the video element and we are on stage
// update the stage
- if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
+ if (isVideo && this.isCurrentlyOnLargeVideo())
this.VideoLayout.updateLargeVideo(this.id);
+ else
+ // Missing video stream will affect display mode
+ this.updateView();
+};
+
+/**
+ * Checks whether the remote user associated with this RemoteVideo
+ * has connectivity issues.
+ *
+ * @return {boolean} true if the user's connection is fine or
+ * false otherwise.
+ */
+RemoteVideo.prototype.isConnectionActive = function() {
+ return this.user.isConnectionActive();
+};
+
+/**
+ * The remote video is considered "playable" once the stream has started
+ * according to the {@link #hasVideoStarted} result.
+ *
+ * @inheritdoc
+ * @override
+ */
+RemoteVideo.prototype.isVideoPlayable = function () {
+ return SmallVideo.prototype.isVideoPlayable.call(this)
+ && this.hasVideoStarted() && !this.mutedWhileDisconnected;
+};
+
+/**
+ * @inheritDoc
+ */
+RemoteVideo.prototype.updateView = function () {
+
+ this.updateConnectionStatusIndicator(
+ null /* will obtain the status from 'conference' */);
+
+ // This must be called after 'updateConnectionStatusIndicator' because it
+ // affects the display mode by modifying 'mutedWhileDisconnected' flag
+ SmallVideo.prototype.updateView.call(this);
+};
+
+/**
+ * Updates the UI to reflect user's connectivity status.
+ * @param isActive {boolean|null} 'true' if user's connection is active or
+ * 'false' when the use is having some connectivity issues and a warning
+ * should be displayed. When 'null' is passed then the current value will be
+ * obtained from the conference instance.
+ */
+RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
+ // Check for initial value if 'isActive' is not defined
+ if (typeof isActive !== "boolean") {
+ isActive = this.isConnectionActive();
+ if (isActive === null) {
+ // Cancel processing at this point - no update
+ return;
+ }
+ }
+
+ console.debug(this.id + " thumbnail is connection active ? " + isActive);
+
+ // Update 'mutedWhileDisconnected' flag
+ this._figureOutMutedWhileDisconnected(!isActive);
+
+ if(this.connectionIndicator)
+ this.connectionIndicator.updateConnectionStatusIndicator(isActive);
+
+ // Toggle thumbnail video problem filter
+ this.selectVideoElement().toggleClass(
+ "videoThumbnailProblemFilter", !isActive);
+ this.$avatar().toggleClass(
+ "videoThumbnailProblemFilter", !isActive);
};
/**
@@ -246,22 +376,23 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) {
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
// when video playback starts
var onPlayingHandler = function () {
+ self.wasVideoPlayed = true;
self.VideoLayout.videoactive(streamElement, self.id);
streamElement.onplaying = null;
+ // Refresh to show the video
+ self.updateView();
};
streamElement.onplaying = onPlayingHandler;
};
/**
- * Checks whether or not video stream exists and has started for this
- * RemoteVideo instance. This is checked by trying to select video element in
- * this container and checking if 'currentTime' field's value is greater than 0.
+ * Checks whether the video stream has started for this RemoteVideo instance.
*
- * @returns {*|boolean} true if this RemoteVideo has active video stream running
+ * @returns {boolean} true if this RemoteVideo has a video stream for which
+ * the playback has been started.
*/
RemoteVideo.prototype.hasVideoStarted = function () {
- var videoSelector = this.selectVideoElement();
- return videoSelector.length && videoSelector[0].currentTime > 0;
+ return this.wasVideoPlayed;
};
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js
index 7b7192fae..65a433469 100644
--- a/modules/UI/videolayout/SmallVideo.js
+++ b/modules/UI/videolayout/SmallVideo.js
@@ -5,6 +5,27 @@ import UIEvents from "../../../service/UI/UIEvents";
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
+/**
+ * Display mode constant used when video is being displayed on the small video.
+ * @type {number}
+ * @constant
+ */
+const DISPLAY_VIDEO = 0;
+/**
+ * Display mode constant used when the user's avatar is being displayed on
+ * the small video.
+ * @type {number}
+ * @constant
+ */
+const DISPLAY_AVATAR = 1;
+/**
+ * Display mode constant used when neither video nor avatar is being displayed
+ * on the small video.
+ * @type {number}
+ * @constant
+ */
+const DISPLAY_BLACKNESS = 2;
+
function SmallVideo(VideoLayout) {
this.isAudioMuted = false;
this.hasAvatar = false;
@@ -337,6 +358,16 @@ SmallVideo.prototype.selectVideoElement = function () {
return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
};
+/**
+ * Selects the HTML image element which displays user's avatar.
+ *
+ * @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
+ * element which displays the user's avatar.
+ */
+SmallVideo.prototype.$avatar = function () {
+ return $('#' + this.videoSpanId + ' .userAvatar');
+};
+
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
@@ -359,6 +390,47 @@ SmallVideo.prototype.hasVideo = function () {
return this.selectVideoElement().length !== 0;
};
+/**
+ * Checks whether the user associated with this SmallVideo is currently
+ * being displayed on the "large video".
+ *
+ * @return {boolean} true if the user is displayed on the large video
+ * or false otherwise.
+ */
+SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
+ return this.VideoLayout.isCurrentlyOnLarge(this.id);
+};
+
+/**
+ * Checks whether there is a playable video stream available for the user
+ * associated with this SmallVideo.
+ *
+ * @return {boolean} true if there is a playable video stream available
+ * or false otherwise.
+ */
+SmallVideo.prototype.isVideoPlayable = function() {
+ return this.videoStream // Is there anything to display ?
+ && !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
+ && (this.isLocal || this.VideoLayout.isInLastN(this.id));
+};
+
+/**
+ * Determines what should be display on the thumbnail.
+ *
+ * @return {number} one of DISPLAY_VIDEO,DISPLAY_AVATAR
+ * or DISPLAY_BLACKNESS.
+ */
+SmallVideo.prototype.selectDisplayMode = function() {
+ // Display name is always and only displayed when user is on the stage
+ if (this.isCurrentlyOnLargeVideo()) {
+ return DISPLAY_BLACKNESS;
+ } else if (this.isVideoPlayable() && this.selectVideoElement().length) {
+ return DISPLAY_VIDEO;
+ } else {
+ return DISPLAY_AVATAR;
+ }
+};
+
/**
* Hides or shows the user's avatar.
* This update assumes that large video had been updated and we will
@@ -378,46 +450,28 @@ SmallVideo.prototype.updateView = function () {
}
}
- let video = this.selectVideoElement();
-
- let avatar = $('#' + this.videoSpanId + ' .userAvatar');
-
- var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
-
- var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge;
- var showAvatar;
- if ((!this.isLocal
- && !this.VideoLayout.isInLastN(this.id))
- || this.isVideoMuted) {
- showAvatar = true;
- } else {
- // We want to show the avatar when the video is muted or not exists
- // that is when 'true' or 'null' is returned
- showAvatar = !this.videoStream || this.videoStream.isMuted();
- }
-
- showAvatar = showAvatar && !isCurrentlyOnLarge;
-
- if (video && video.length > 0) {
- setVisibility(video, showVideo);
- }
- setVisibility(avatar, showAvatar);
+ // Determine whether video, avatar or blackness should be displayed
+ let displayMode = this.selectDisplayMode();
+ // Show/hide video
+ setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
+ // Show/hide the avatar
+ setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
};
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
var thumbnail = $('#' + this.videoSpanId);
- var avatar = $('#' + this.videoSpanId + ' .userAvatar');
+ var avatarSel = this.$avatar();
this.hasAvatar = true;
// set the avatar in the thumbnail
- if (avatar && avatar.length > 0) {
- avatar[0].src = avatarUrl;
+ if (avatarSel && avatarSel.length > 0) {
+ avatarSel[0].src = avatarUrl;
} else {
if (thumbnail && thumbnail.length > 0) {
- avatar = document.createElement('img');
- avatar.className = 'userAvatar';
- avatar.src = avatarUrl;
- thumbnail.append(avatar);
+ var avatarElement = document.createElement('img');
+ avatarElement.className = 'userAvatar';
+ avatarElement.src = avatarUrl;
+ thumbnail.append(avatarElement);
}
}
};
diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js
index 2043344c2..5ebfe979c 100644
--- a/modules/UI/videolayout/VideoContainer.js
+++ b/modules/UI/videolayout/VideoContainer.js
@@ -173,25 +173,51 @@ 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
+ * even if the video is stalled).
+ * @type {boolean}
+ */
+ this.wasVideoRendered = false;
+
this.$wrapper = $('#largeVideoWrapper');
this.avatarHeight = $("#dominantSpeakerAvatar").height();
+ var onPlayCallback = function (event) {
+ if (typeof onPlay === 'function') {
+ onPlay(event);
+ }
+ this.wasVideoRendered = true;
+ }.bind(this);
// This does not work with Temasys plugin - has to be a property to be
// copied between new