From cf931f8a9fddc2c31a0f91e415f8b95046182309 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 16 Sep 2016 14:52:07 -0500 Subject: [PATCH 01/22] ref(ConnectionIndicator.js) pass icon class as an argument --- modules/UI/videolayout/ConnectionIndicator.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/UI/videolayout/ConnectionIndicator.js b/modules/UI/videolayout/ConnectionIndicator.js index ff5eb9b61..95273a5cb 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,9 @@ 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")); }; /** From e9445866a53f3a4fad586a51dc9318c1bbf6b69f Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 16 Sep 2016 14:57:56 -0500 Subject: [PATCH 02/22] ref(ConnectionIndicator.js): make CQ 'object' optional --- modules/UI/videolayout/ConnectionIndicator.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/modules/UI/videolayout/ConnectionIndicator.js b/modules/UI/videolayout/ConnectionIndicator.js index 95273a5cb..e2da230f8 100644 --- a/modules/UI/videolayout/ConnectionIndicator.js +++ b/modules/UI/videolayout/ConnectionIndicator.js @@ -314,12 +314,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 +329,7 @@ ConnectionIndicator.prototype.updateConnectionQuality = ConnectionIndicator.connectionQualityValues[quality]; } } - if (object.isResolutionHD) { + if (object && typeof object.isResolutionHD === 'boolean') { this.isResolutionHD = object.isResolutionHD; } this.updateResolutionIndicator(); From 3ef5dd20ef4734a16a1dede123969c8a9a16f764 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 16 Sep 2016 15:17:00 -0500 Subject: [PATCH 03/22] ref(RemoteVideo): store JitsiParticipant instead of id --- conference.js | 2 +- modules/UI/UI.js | 9 +++++---- modules/UI/shared_video/SharedVideo.js | 2 +- modules/UI/videolayout/RemoteVideo.js | 18 ++++++++++++++---- modules/UI/videolayout/VideoLayout.js | 22 +++++++++++++++++----- 5 files changed, 38 insertions(+), 15 deletions(-) diff --git a/conference.js b/conference.js index ed346eca2..a715ca552 100644 --- a/conference.js +++ b/conference.js @@ -1085,7 +1085,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); diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 22802db3d..558e4b2f6 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -602,10 +602,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 +619,7 @@ UI.addUser = function (id, displayName) { UIUtil.playSoundNotification('userJoined'); // Add Peer's container - VideoLayout.addParticipantContainer(id); + VideoLayout.addParticipantContainer(user); // Configure avatar UI.setUserEmail(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/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 6d577765a..382ed5c6c 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -8,14 +8,24 @@ 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; diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 0b3b68375..e20a122b2 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -382,18 +382,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 RemoteVideo 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 SmallVideo. + * + * @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); From 8a43699a89b5be53c5b972d4048c2d39b8771603 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 16 Sep 2016 15:33:22 -0500 Subject: [PATCH 04/22] feat(ConnectionIndicator): show disconnected GSM bars on local thumbnail --- conference.js | 2 ++ css/_videolayout_default.scss | 6 +++++ modules/UI/UI.js | 11 +++++++++ modules/UI/videolayout/ConnectionIndicator.js | 24 +++++++++++++++++++ modules/UI/videolayout/VideoLayout.js | 12 ++++++++++ 5 files changed, 55 insertions(+) diff --git a/conference.js b/conference.js index a715ca552..66865a188 100644 --- a/conference.js +++ b/conference.js @@ -1205,10 +1205,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 e53a4b55f..8af855ed1 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -221,6 +221,12 @@ overflow: hidden; } +.connection.connection_lost +{ + color: #8B8B8B; + overflow: visible; +} + .connection.connection_full { color: #FFFFFF;/*#15A1ED*/ diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 558e4b2f6..9b0e24da3 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. */ diff --git a/modules/UI/videolayout/ConnectionIndicator.js b/modules/UI/videolayout/ConnectionIndicator.js index e2da230f8..ddcd65d4f 100644 --- a/modules/UI/videolayout/ConnectionIndicator.js +++ b/modules/UI/videolayout/ConnectionIndicator.js @@ -285,6 +285,9 @@ ConnectionIndicator.prototype.create = function () { createIcon(["connection", "connection_empty"], "icon-connection")); this.fullIcon = this.connectionIndicatorContainer.appendChild( 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 diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index e20a122b2..582b25372 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -497,6 +497,18 @@ var VideoLayout = { localVideoThumbnail.showAudioIndicator(isMuted); }, + /** + * 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. + */ + showLocalConnectionInterrupted (isInterrupted) { + localVideoThumbnail.connectionIndicator + .updateConnectionStatusIndicator(!isInterrupted); + }, + /** * Resizes thumbnails. */ From 5daceaead723a49e4bbe5615db1a7c9b93308322 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 16 Sep 2016 15:40:24 -0500 Subject: [PATCH 05/22] feat(conference.js): add isParticipantConnectionActive --- conference.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/conference.js b/conference.js index 66865a188..9c18b06fc 100644 --- a/conference.js +++ b/conference.js @@ -691,6 +691,29 @@ 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; + }, getMyUserId () { return this._room && this._room.myUserId(); From 9d1364b6fb55caa74d5399b69e8a9525375a773f Mon Sep 17 00:00:00 2001 From: paweldomas Date: Fri, 16 Sep 2016 15:51:19 -0500 Subject: [PATCH 06/22] feat(RemoteVideo): show disconnected GSM bars for remotes --- conference.js | 4 +++ modules/UI/UI.js | 11 +++++++ modules/UI/videolayout/RemoteVideo.js | 43 +++++++++++++++++++++++++++ modules/UI/videolayout/VideoLayout.js | 17 +++++++++++ 4 files changed, 75 insertions(+) diff --git a/conference.js b/conference.js index 9c18b06fc..d12ff9c8d 100644 --- a/conference.js +++ b/conference.js @@ -1197,6 +1197,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; diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 9b0e24da3..ff0ea0fd6 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -995,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/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 382ed5c6c..52d3d6220 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -227,6 +227,49 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) { this.VideoLayout.updateLargeVideo(this.id); }; +/** + * 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(); +}; + +/** + * @inheritDoc + */ +RemoteVideo.prototype.updateView = function () { + SmallVideo.prototype.updateView.call(this); + this.updateConnectionStatusIndicator( + null /* will obtain the status from 'conference' */); +}; + +/** + * 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); + + if(this.connectionIndicator) + this.connectionIndicator.updateConnectionStatusIndicator(isActive); +}; + /** * Removes RemoteVideo from the page. */ diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 582b25372..86aafa775 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -423,6 +423,8 @@ var VideoLayout = { } else { VideoLayout.resizeThumbnails(false, true); } + // Initialize the view + remoteVideo.updateView(); }, videoactive (videoelem, resourceJid) { @@ -640,6 +642,21 @@ 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 thumbnail + let remoteVideo = remoteVideos[id]; + if (remoteVideo) { + remoteVideo.updateConnectionStatusIndicator(isActive); + } + }, + /** * On last N change event. * From 4722054c3ecea20a2eb6f992dc13fd079f2e12cc Mon Sep 17 00:00:00 2001 From: paweldomas Date: Mon, 19 Sep 2016 13:57:14 -0500 Subject: [PATCH 07/22] ref(SmallVideo): adds avatar selector --- modules/UI/videolayout/SmallVideo.js | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 7b7192fae..87bfe072b 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -337,6 +337,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. @@ -380,7 +390,7 @@ SmallVideo.prototype.updateView = function () { let video = this.selectVideoElement(); - let avatar = $('#' + this.videoSpanId + ' .userAvatar'); + let avatar = this.$avatar; var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id); @@ -406,18 +416,18 @@ SmallVideo.prototype.updateView = function () { 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); } } }; From fceb512a032a87d417e75d0b5016cf3d0e146055 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Mon, 19 Sep 2016 14:04:55 -0500 Subject: [PATCH 08/22] ref(SmallVideo): add 'isCurrentlyOnLargeVideo' --- modules/UI/videolayout/LocalVideo.js | 2 +- modules/UI/videolayout/RemoteVideo.js | 2 +- modules/UI/videolayout/SmallVideo.js | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) 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 52d3d6220..85df94bfe 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -223,7 +223,7 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) { // 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); }; diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 87bfe072b..5cc9769f1 100644 --- a/modules/UI/videolayout/SmallVideo.js +++ b/modules/UI/videolayout/SmallVideo.js @@ -369,6 +369,17 @@ 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); +}; + /** * Hides or shows the user's avatar. * This update assumes that large video had been updated and we will @@ -392,7 +403,7 @@ SmallVideo.prototype.updateView = function () { let avatar = this.$avatar; - var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id); + var isCurrentlyOnLarge = this.isCurrentlyOnLargeVideo(); var showVideo = !this.isVideoMuted && !isCurrentlyOnLarge; var showAvatar; From 30cb948dcf7ac548e8ab0e96a85e8338b2e014db Mon Sep 17 00:00:00 2001 From: paweldomas Date: Mon, 19 Sep 2016 14:18:52 -0500 Subject: [PATCH 09/22] feat(SmallVideo): make thumbnail grey The video or the avatar on a thumbnail will be displayed in greyscale when the user is having connectivity issues. --- css/_videolayout_default.scss | 5 +++++ modules/UI/videolayout/RemoteVideo.js | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index 8af855ed1..ee1d49629 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -455,6 +455,11 @@ filter: blur(10px) grayscale(.5) opacity(0.8); } +.videoThumbnailProblemFilter { + -webkit-filter: grayscale(100%); + filter: grayscale(100%); +} + #videoConnectionMessage { display: none; position: absolute; diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 85df94bfe..351326d57 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -268,6 +268,12 @@ RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) { if(this.connectionIndicator) this.connectionIndicator.updateConnectionStatusIndicator(isActive); + + // Toggle thumbnail video problem filter + this.selectVideoElement().toggleClass( + "videoThumbnailProblemFilter", !isActive); + this.$avatar().toggleClass( + "videoThumbnailProblemFilter", !isActive); }; /** @@ -300,6 +306,8 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) { var onPlayingHandler = function () { self.VideoLayout.videoactive(streamElement, self.id); streamElement.onplaying = null; + // Refresh to show the video + self.updateView(); }; streamElement.onplaying = onPlayingHandler; }; From 40f2c593a2061b0d1527dc36af8c3ae9e57e5c9d Mon Sep 17 00:00:00 2001 From: paweldomas Date: Mon, 19 Sep 2016 15:59:56 -0500 Subject: [PATCH 10/22] ref(SmallVideo): figure out what is to be displayed At any point of time we display one of the three: video, avatar or blackness. The purpose of this commit is to make that fact more clear in the code. --- modules/UI/videolayout/RemoteVideo.js | 12 ++++ modules/UI/videolayout/SmallVideo.js | 81 +++++++++++++++++++-------- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 351326d57..ae5952443 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -238,6 +238,18 @@ 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(); +}; + /** * @inheritDoc */ diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js index 5cc9769f1..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; @@ -380,6 +401,36 @@ 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 @@ -399,30 +450,12 @@ SmallVideo.prototype.updateView = function () { } } - let video = this.selectVideoElement(); - - let avatar = this.$avatar; - - var isCurrentlyOnLarge = this.isCurrentlyOnLargeVideo(); - - 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) { From 66bbc4d9fdf01ece8a14862ac83ca04f70ab50ba Mon Sep 17 00:00:00 2001 From: paweldomas Date: Thu, 22 Sep 2016 15:57:43 -0500 Subject: [PATCH 11/22] fix(RemoteVideo): change hasVideoStarted logic We used to rely on 'currentTime' of the video element, but we execute 'updateView' from the 'onplay' callback and on fast machines it may happen that the value is 0 even though the video has just started. --- modules/UI/videolayout/RemoteVideo.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index ae5952443..05750f045 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -29,6 +29,14 @@ function RemoteVideo(user, VideoLayout, emitter) { 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; } RemoteVideo.prototype = Object.create(SmallVideo.prototype); @@ -218,6 +226,10 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) { var select = $('#' + elementID); select.remove(); + if (isVideo) { + this.wasVideoPlayed = false; + } + console.info((isVideo ? "Video" : "Audio") + " removed " + this.id, select); @@ -316,6 +328,7 @@ 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 @@ -325,15 +338,13 @@ RemoteVideo.prototype.waitForPlayback = function (streamElement, stream) { }; /** - * 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) { From 46766ec239cac30ecca07807250ac8e23ee2cc6e Mon Sep 17 00:00:00 2001 From: paweldomas Date: Thu, 22 Sep 2016 16:21:01 -0500 Subject: [PATCH 12/22] fix(RemoteVideo): avoid black thumbnail When the user is having connectivity issues we use the image cached in the video element to show the preview in greyscale. It looks like this cached image gets invalided after prolonged periods of time the video element being hidden(and it is hidden when the video is muted). So we never show this image if the user gets muted during connectivity disruption in order to avoid blackness. --- modules/UI/videolayout/RemoteVideo.js | 51 +++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js index 05750f045..da9fc9693 100644 --- a/modules/UI/videolayout/RemoteVideo.js +++ b/modules/UI/videolayout/RemoteVideo.js @@ -37,6 +37,16 @@ function RemoteVideo(user, VideoLayout, emitter) { * @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); @@ -179,6 +189,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. @@ -237,6 +274,9 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) { // update the stage if (isVideo && this.isCurrentlyOnLargeVideo()) this.VideoLayout.updateLargeVideo(this.id); + else + // Missing video stream will affect display mode + this.updateView(); }; /** @@ -259,16 +299,20 @@ RemoteVideo.prototype.isConnectionActive = function() { */ RemoteVideo.prototype.isVideoPlayable = function () { return SmallVideo.prototype.isVideoPlayable.call(this) - && this.hasVideoStarted(); + && this.hasVideoStarted() && !this.mutedWhileDisconnected; }; /** * @inheritDoc */ RemoteVideo.prototype.updateView = function () { - SmallVideo.prototype.updateView.call(this); + 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); }; /** @@ -290,6 +334,9 @@ RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) { console.debug(this.id + " thumbnail is connection active ? " + isActive); + // Update 'mutedWhileDisconnected' flag + this._figureOutMutedWhileDisconnected(!isActive); + if(this.connectionIndicator) this.connectionIndicator.updateConnectionStatusIndicator(isActive); From 0aea799b5005f21daaa1106e96b6a6e5117cd0d7 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 10:11:57 -0500 Subject: [PATCH 13/22] doc(LargeVideoManager): fills missing JS doc --- modules/UI/videolayout/LargeVideoManager.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index e0934ee74..69166394e 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; @@ -136,6 +142,10 @@ 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 From b8937e0349ea253c2cb478026baecf177a88e5b5 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 10:18:17 -0500 Subject: [PATCH 14/22] fix(LargeVideoManager): hide video when avatar is displayed --- modules/UI/videolayout/LargeVideoManager.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 69166394e..53f2777ff 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -162,7 +162,8 @@ export default class LargeVideoManager { // but we still should show watermark if (isVideoMuted) { this.showWatermark(true); - promise = Promise.resolve(); + // If the avatar is to be displayed the video should be hidden + promise = container.hide(); } else { promise = container.show(); } From 352e784cad9bb933d7b28af7b612ed0ce893d993 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 10:36:24 -0500 Subject: [PATCH 15/22] fix(VideoLayout): show video when the connection is back --- modules/UI/videolayout/VideoLayout.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js index 86aafa775..4a7cc0fbc 100644 --- a/modules/UI/videolayout/VideoLayout.js +++ b/modules/UI/videolayout/VideoLayout.js @@ -653,7 +653,13 @@ var VideoLayout = { // Show/hide warning on the thumbnail let remoteVideo = remoteVideos[id]; if (remoteVideo) { - remoteVideo.updateConnectionStatusIndicator(isActive); + // 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(); } }, From 5843c6c5694509598e0eefe8e3e9ddf827b6cfe2 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 11:19:58 -0500 Subject: [PATCH 16/22] ref(LargeVideoManager): rename 'enableVideoProblemFilter' --- modules/UI/videolayout/LargeVideoManager.js | 11 ++++++----- modules/UI/videolayout/VideoContainer.js | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 53f2777ff..251d7e1e6 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -91,7 +91,7 @@ export default class LargeVideoManager { * Called when the media connection has been interrupted. */ onVideoInterrupted () { - this.enableVideoProblemFilter(true); + this.enableLocalConnectionProblemFilter(true); let reconnectingKey = "connection.RECONNECTING"; $('#videoConnectionMessage') .attr("data-i18n", reconnectingKey) @@ -104,7 +104,7 @@ export default class LargeVideoManager { * Called when the media connection has been restored. */ onVideoRestored () { - this.enableVideoProblemFilter(false); + this.enableLocalConnectionProblemFilter(false); this.showVideoConnectionMessage(false); } @@ -240,12 +240,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); } /** diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index 2043344c2..fda76f75f 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -186,12 +186,12 @@ export class VideoContainer extends LargeContainer { /** * 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 true if the filter is to be enabled or * false otherwise. */ - enableVideoProblemFilter (enable) { + enableLocalConnectionProblemFilter (enable) { this.$video.toggleClass("videoProblemFilter", enable); } From 62d2e3e2a4fb12d278a48c22171299f58e69134b Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 11:46:19 -0500 Subject: [PATCH 17/22] feat(conference.js): add 'getParticipantDisplayName' --- conference.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/conference.js b/conference.js index d12ff9c8d..c06c19181 100644 --- a/conference.js +++ b/conference.js @@ -714,6 +714,28 @@ export default { 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(); From 5952261e876c220d482856ea3458a367fcf78c52 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 11:52:38 -0500 Subject: [PATCH 18/22] ref(LargeVideoManager): introduce 'setVideoConnectionMessage' --- modules/UI/videolayout/LargeVideoManager.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index 251d7e1e6..ba5ee7264 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -92,10 +92,7 @@ export default class LargeVideoManager { */ onVideoInterrupted () { this.enableLocalConnectionProblemFilter(true); - let reconnectingKey = "connection.RECONNECTING"; - $('#videoConnectionMessage') - .attr("data-i18n", reconnectingKey) - .text(APP.translation.translateString(reconnectingKey)); + this._setVideoConnectionMessage("connection.RECONNECTING") // Show the message only if the video is currently being displayed this.showVideoConnectionMessage(this.state === VIDEO_CONTAINER_TYPE); } @@ -282,6 +279,21 @@ export default class LargeVideoManager { } } + /** + * Updated the text which is to be shown on the top of large video. + * + * @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 + */ + _setVideoConnectionMessage (msgKey, msgOptions) { + $('#videoConnectionMessage') + .attr("data-i18n", msgKey) + .text(APP.translation.translateString(msgKey, msgOptions)); + } + /** * Add container of specified type. * @param {string} type container type From 42fd3097de04056141097ffacfaabc1e8ff06201 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 13:19:52 -0500 Subject: [PATCH 19/22] feat(VideoContainer): add 'wasVideoRendered' flag The 'wasVideoRendered' flag will tell whether or not we have any video image rendered(even if stalled) on the large video element. --- modules/UI/videolayout/VideoContainer.js | 25 +++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index fda76f75f..e2a7bd3eb 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -174,14 +174,29 @@ export class VideoContainer extends LargeContainer { this.isVisible = false; this.$avatar = $('#dominantSpeaker'); + + /** + * 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 elements //this.$video.on('play', onPlay); - this.$video[0].onplay = onPlay; + this.$video[0].onplay = onPlayCallback; } /** @@ -284,6 +299,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]); From 661ea2cf45dac9fb7ad79ce9bacb6f78ec37f407 Mon Sep 17 00:00:00 2001 From: paweldomas Date: Sat, 24 Sep 2016 13:24:18 -0500 Subject: [PATCH 20/22] 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. --- css/_videolayout_default.scss | 27 +++++ index.html | 1 + lang/main.json | 3 +- modules/UI/videolayout/LargeVideoManager.js | 121 ++++++++++++++++++-- modules/UI/videolayout/VideoContainer.js | 50 ++++++++ modules/UI/videolayout/VideoLayout.js | 6 + 6 files changed, 200 insertions(+), 8 deletions(-) diff --git a/css/_videolayout_default.scss b/css/_videolayout_default.scss index ee1d49629..7dbc3528b 100644 --- a/css/_videolayout_default.scss +++ b/css/_videolayout_default.scss @@ -450,6 +450,11 @@ 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); @@ -460,6 +465,28 @@ 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 { display: none; position: absolute; diff --git a/index.html b/index.html index 48c8e1e3a..24a009220 100644 --- a/index.html +++ b/index.html @@ -228,6 +228,7 @@ +
diff --git a/lang/main.json b/lang/main.json index 0e5f69792..d891fb625 100644 --- a/lang/main.json +++ b/lang/main.json @@ -324,7 +324,8 @@ "ATTACHED": "Attached", "FETCH_SESSION_ID": "Obtaining session-id...", "GOT_SESSION_ID": "Obtaining session-id... Done", - "GET_SESSION_ID_ERROR": "Get session-id error: " + "GET_SESSION_ID_ERROR": "Get session-id error: ", + "USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..." }, "recording": { diff --git a/modules/UI/videolayout/LargeVideoManager.js b/modules/UI/videolayout/LargeVideoManager.js index ba5ee7264..56bb9ccdb 100644 --- a/modules/UI/videolayout/LargeVideoManager.js +++ b/modules/UI/videolayout/LargeVideoManager.js @@ -121,7 +121,8 @@ export default class LargeVideoManager { // Include hide()/fadeOut only if we're switching between users let preUpdate; - if (this.newStreamData.id != this.id) { + let isUserSwitch = this.newStreamData.id != this.id; + if (isUserSwitch) { preUpdate = container.hide(); } else { preUpdate = Promise.resolve(); @@ -146,25 +147,46 @@ export default class LargeVideoManager { // If we the continer is VIDEO_CONTAINER_TYPE, we need to check // its stream whether exist and is muted to set isVideoMuted // in rest of the cases it is false - let isVideoMuted = false; + let showAvatar = false; if (videoType == VIDEO_CONTAINER_TYPE) - isVideoMuted = stream ? stream.isMuted() : true; + showAvatar = stream ? stream.isMuted() : true; - // show the avatar on large if needed - container.showAvatar(isVideoMuted); + // If the user's connection is disrupted then the avatar will be + // displayed in case we have no video image cached. That is if + // there was a user switch(image is lost on stream detach) or if + // the video was not rendered, before the connection has failed. + let isHavingConnectivityIssues + = APP.conference.isParticipantConnectionActive(id) === false; + if (isHavingConnectivityIssues + && (isUserSwitch | !container.wasVideoRendered)) { + showAvatar = true; + } let promise; // do not show stream if video is muted // but we still should show watermark - if (isVideoMuted) { + if (showAvatar) { this.showWatermark(true); - // If the avatar is to be displayed the video should be hidden + // If the intention of this switch is to show the avatar + // we need to make sure that the video is hidden promise = container.hide(); } else { promise = container.show(); } + // show the avatar on large if needed + container.showAvatar(showAvatar); + + // Make sure no notification about remote failure is shown as + // it's UI conflicts with the one for local connection interrupted. + if (APP.conference.isConnectionInterrupted()) { + this.updateParticipantConnStatusIndication(id, true); + } else { + this.updateParticipantConnStatusIndication( + id, !isHavingConnectivityIssues); + } + // resolve updateLargeVideo promise after everything is done promise.then(resolve); @@ -177,6 +199,38 @@ export default class LargeVideoManager { }); } + /** + * Shows/hides notification about participant's connectivity issues to be + * shown on the large video area. + * + * @param {string} id the id of remote participant(MUC nickname) + * @param {boolean} isConnected true if the connection is active or false + * when the user is having connectivity issues. + * + * @private + */ + updateParticipantConnStatusIndication (id, isConnected) { + + // Apply grey filter on the large video + this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected); + + if (isConnected) { + // Hide the message + this.showRemoteConnectionMessage(false); + } else { + // Get user's display name + let displayName + = APP.conference.getParticipantDisplayName(id); + this._setRemoteConnectionMessage( + "connection.USER_CONNECTION_INTERRUPTED", + { displayName: displayName }); + + // Show it now only if the VideoContainer is on top + this.showRemoteConnectionMessage( + this.state === VIDEO_CONTAINER_TYPE); + } + } + /** * Update large video. * Switches to large video even if previously other container was visible. @@ -274,11 +328,59 @@ export default class LargeVideoManager { if (show) { $('#videoConnectionMessage').css({display: "block"}); + // Avatar message conflicts with 'videoConnectionMessage', + // so it must be hidden + this.showRemoteConnectionMessage(false); } else { $('#videoConnectionMessage').css({display: "none"}); } } + /** + * Shows hides the "avatar" message which is to be displayed either in + * the middle of the screen or below the avatar image. + * + * @param {null|boolean} show (optional) true to show the avatar + * message or false to hide it. If not provided then the connection + * status of the user currently on the large video will be obtained form + * "APP.conference" and the message will be displayed if the user's + * connection is interrupted. + */ + showRemoteConnectionMessage (show) { + if (typeof show !== 'boolean') { + show = APP.conference.isParticipantConnectionActive(this.id); + } + + if (show) { + $('#remoteConnectionMessage').css({display: "block"}); + // 'videoConnectionMessage' message conflicts with 'avatarMessage', + // so it must be hidden + this.showVideoConnectionMessage(false); + } else { + $('#remoteConnectionMessage').hide(); + } + } + + /** + * Updates the text which describes that the remote user is having + * connectivity issues. + * + * @param {string} msgKey the translation key which will be used to get + * the message text. + * @param {object} msgOptions translation options object. + * + * @private + */ + _setRemoteConnectionMessage (msgKey, msgOptions) { + if (msgKey) { + let text = APP.translation.translateString(msgKey, msgOptions); + $('#remoteConnectionMessage') + .attr("data-i18n", msgKey).text(text); + } + + this.videoContainer.positionRemoteConnectionMessage(); + } + /** * Updated the text which is to be shown on the top of large video. * @@ -353,6 +455,7 @@ export default class LargeVideoManager { if (this.state === VIDEO_CONTAINER_TYPE) { this.showWatermark(false); this.showVideoConnectionMessage(false); + this.showRemoteConnectionMessage(false); } oldContainer.hide(); @@ -366,6 +469,10 @@ export default class LargeVideoManager { // the container would be taking care of it by itself, but that // is a bigger refactoring this.showWatermark(true); + // "avatar" and "video connection" can not be displayed both + // at the same time, but the latter is of higher priority and it + // will hide the avatar one if will be displayed. + this.showRemoteConnectionMessage(/* fet the current state */); this.showVideoConnectionMessage(/* fetch the current state */); } }); diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index e2a7bd3eb..5ebfe979c 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -173,8 +173,19 @@ export class VideoContainer extends LargeContainer { this.isVisible = false; + /** + * Flag indicates whether or not the avatar is currently displayed. + * @type {boolean} + */ + this.avatarDisplayed = false; this.$avatar = $('#dominantSpeaker'); + /** + * A jQuery selector of the remote connection message. + * @type {jQuery|HTMLElement} + */ + this.$remoteConnectionMessage = $('#remoteConnectionMessage'); + /** * Indicates whether or not the video stream attached to the video * element has started(which means that there is any image rendered @@ -266,6 +277,30 @@ export class VideoContainer extends LargeContainer { } } + /** + * Update position of the remote connection message which describes that + * the remote user is having connectivity issues. + */ + positionRemoteConnectionMessage () { + + if (this.avatarDisplayed) { + let $avatarImage = $("#dominantSpeakerAvatar"); + this.$remoteConnectionMessage.css( + 'top', + $avatarImage.offset().top + $avatarImage.height() + 10); + } else { + let height = this.$remoteConnectionMessage.height(); + let parentHeight = this.$remoteConnectionMessage.parent().height(); + this.$remoteConnectionMessage.css( + 'top', (parentHeight/2) - (height/2)); + } + + let width = this.$remoteConnectionMessage.width(); + let parentWidth = this.$remoteConnectionMessage.parent().width(); + this.$remoteConnectionMessage.css( + 'left', ((parentWidth/2) - (width/2))); + } + resize (containerWidth, containerHeight, animate = false) { let [width, height] = this.getVideoSize(containerWidth, containerHeight); @@ -278,6 +313,8 @@ export class VideoContainer extends LargeContainer { this.$avatar.css('top', top); + this.positionRemoteConnectionMessage(); + this.$wrapper.animate({ width: width, height: height, @@ -362,10 +399,23 @@ export class VideoContainer extends LargeContainer { (show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000"); this.$avatar.css("visibility", show ? "visible" : "hidden"); + this.avatarDisplayed = show; this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show); } + /** + * Indicates that the remote user who is currently displayed by this video + * container is having connectivity issues. + * + * @param {boolean} show true to show or false to hide + * the indication. + */ + showRemoteConnectionProblemIndicator (show) { + this.$video.toggleClass("remoteVideoProblemFilter", show); + this.$avatar.toggleClass("remoteVideoProblemFilter", show); + } + // We are doing fadeOut/fadeIn animations on parent div which wraps // largeVideo, because when Temasys plugin is in use it replaces //