Merge pull request #938 from jitsi/participant_conn_status

Adds participant connection status notifications
This commit is contained in:
hristoterezov 2016-09-27 17:54:22 -05:00 committed by GitHub
commit 2a8700bca3
13 changed files with 680 additions and 95 deletions

View File

@ -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 <tt>JitsiParticipant</tt> identified by
* the given <tt>id</tt>.
*
* @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) => {

View File

@ -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%;

View File

@ -228,10 +228,11 @@
<img id="dominantSpeakerAvatar" src=""/>
<canvas id="dominantSpeakerAudioLevel"></canvas>
</div>
<span id="remoteConnectionMessage"></span>
<div id="largeVideoWrapper">
<video id="largeVideo" muted="true" autoplay></video>
</div>
<span id="videoConnectionMessage"></span>
<span id="localConnectionMessage"></span>
<span id="videoResolutionLabel">HD</span>
<span id="recordingLabel" class="centeredVideoLabel">
<span id="recordingLabelText"></span>

View File

@ -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":
{

View File

@ -261,6 +261,17 @@ UI.changeDisplayName = function (id, displayName) {
}
};
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> 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

View File

@ -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(

View File

@ -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();

View File

@ -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 <tt>LargeContainer</tt>s where the key is the video
* container type.
* @type {Object.<string, LargeContainer>}
*/
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 <tt>true</tt> to enable, <tt>false</tt> 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) <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.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 */);
}
});
}

View File

@ -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);
};

View File

@ -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 <tt>RemoteVideo</tt>.
* @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 <tt>true</tt> after the 'onplay' event has been
* triggered on the current video element. It goes back to <tt>false</tt>
* 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 <tt>true</tt> 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 <tt>true</tt> if the remote participant is
* currently having connectivity issues or <tt>false</tt> 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 <tt>id</tt> in the
* given <tt>parentElement</tt>.
@ -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 <tt>RemoteVideo</tt>
* has connectivity issues.
*
* @return {boolean} <tt>true</tt> if the user's connection is fine or
* <tt>false</tt> 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) {

View File

@ -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 <tt>SmallVideo</tt> is currently
* being displayed on the "large video".
*
* @return {boolean} <tt>true</tt> if the user is displayed on the large video
* or <tt>false</tt> 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 <tt>SmallVideo</tt>.
*
* @return {boolean} <tt>true</tt> if there is a playable video stream available
* or <tt>false</tt> 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 <tt>DISPLAY_VIDEO</tt>,<tt>DISPLAY_AVATAR</tt>
* or <tt>DISPLAY_BLACKNESS</tt>.
*/
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);
}
}
};

View File

@ -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 <object> elements
//this.$video.on('play', onPlay);
this.$video[0].onplay = onPlay;
this.$video[0].onplay = onPlayCallback;
}
/**
* Enables a filter on the video which indicates that there are some
* problems with the media connection.
* problems with the local media connection.
*
* @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
* <tt>false</tt> otherwise.
*/
enableVideoProblemFilter (enable) {
enableLocalConnectionProblemFilter (enable) {
this.$video.toggleClass("videoProblemFilter", enable);
}
@ -251,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);
@ -263,6 +313,8 @@ export class VideoContainer extends LargeContainer {
this.$avatar.css('top', top);
this.positionRemoteConnectionMessage();
this.$wrapper.animate({
width: width,
height: height,
@ -284,6 +336,14 @@ export class VideoContainer extends LargeContainer {
* @param {string} videoType video type
*/
setStream (stream, videoType) {
if (this.stream === stream) {
return;
} else {
// The stream has changed, so the image will be lost on detach
this.wasVideoRendered = false;
}
// detach old stream
if (this.stream) {
this.stream.detach(this.$video[0]);
@ -339,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 <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
// largeVideo, because when Temasys plugin is in use it replaces
// <video> elements with plugin <object> tag. In Safari jQuery is

View File

@ -384,18 +384,30 @@ var VideoLayout = {
},
/**
* Creates a participant container for the given id and smallVideo.
* Creates or adds a participant container for the given id and smallVideo.
*
* @param id the id of the participant to add
* @param {JitsiParticipant} user the participant to add
* @param {SmallVideo} smallVideo optional small video instance to add as a
* remote video, if undefined RemoteVideo will be created
* remote video, if undefined <tt>RemoteVideo</tt> will be created
*/
addParticipantContainer (id, smallVideo) {
addParticipantContainer (user, smallVideo) {
let id = user.getId();
let remoteVideo;
if(smallVideo)
remoteVideo = smallVideo;
else
remoteVideo = new RemoteVideo(id, VideoLayout, eventEmitter);
remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
this.addRemoteVideoContainer(id, remoteVideo);
},
/**
* Adds remote video container for the given id and <tt>SmallVideo</tt>.
*
* @param {string} the id of the video to add
* @param {SmallVideo} smallVideo the small video instance to add as a
* remote video
*/
addRemoteVideoContainer (id, remoteVideo) {
remoteVideos[id] = remoteVideo;
let videoType = VideoLayout.getRemoteVideoType(id);
@ -413,6 +425,8 @@ var VideoLayout = {
} else {
VideoLayout.resizeThumbnails(false, true);
}
// Initialize the view
remoteVideo.updateView();
},
videoactive (videoelem, resourceJid) {
@ -487,6 +501,18 @@ var VideoLayout = {
localVideoThumbnail.showAudioIndicator(isMuted);
},
/**
* Shows/hides the indication about local connection being interrupted.
*
* @param {boolean} isInterrupted <tt>true</tt> if local connection is
* currently in the interrupted state or <tt>false</tt> if the connection
* is fine.
*/
showLocalConnectionInterrupted (isInterrupted) {
localVideoThumbnail.connectionIndicator
.updateConnectionStatusIndicator(!isInterrupted);
},
/**
* Resizes thumbnails.
*/
@ -618,6 +644,35 @@ var VideoLayout = {
}
},
/**
* Shows/hides warning about remote user's connectivity issues.
*
* @param {string} id the ID of the remote participant(MUC nickname)
* @param {boolean} isActive true if the connection is ok or false when
* the user is having connectivity issues.
*/
onParticipantConnectionStatusChanged (id, isActive) {
// Show/hide warning on the large video
if (this.isCurrentlyOnLarge(id)) {
if (largeVideo) {
// We have to trigger full large video update to transition from
// avatar to video on connectivity restored.
this.updateLargeVideo(id, true /* force update */);
}
}
// Show/hide warning on the thumbnail
let remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Updating only connection status indicator is not enough, because
// when we the connection is restored while the avatar was displayed
// (due to 'muted while disconnected' condition) we may want to show
// the video stream again and in order to do that the display mode
// must be updated.
//remoteVideo.updateConnectionStatusIndicator(isActive);
remoteVideo.updateView();
}
},
/**
* On last N change event.
*