Merge pull request #938 from jitsi/participant_conn_status
Adds participant connection status notifications
This commit is contained in:
commit
2a8700bca3
|
@ -691,6 +691,51 @@ export default {
|
||||||
isConnectionInterrupted () {
|
isConnectionInterrupted () {
|
||||||
return connectionIsInterrupted;
|
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 () {
|
getMyUserId () {
|
||||||
return this._room
|
return this._room
|
||||||
&& this._room.myUserId();
|
&& this._room.myUserId();
|
||||||
|
@ -1085,7 +1130,7 @@ export default {
|
||||||
|
|
||||||
console.log('USER %s connnected', id, user);
|
console.log('USER %s connnected', id, user);
|
||||||
APP.API.notifyUserJoined(id);
|
APP.API.notifyUserJoined(id);
|
||||||
APP.UI.addUser(id, user.getDisplayName());
|
APP.UI.addUser(user);
|
||||||
|
|
||||||
// check the roles for the new user and reflect them
|
// check the roles for the new user and reflect them
|
||||||
APP.UI.updateUserRole(user);
|
APP.UI.updateUserRole(user);
|
||||||
|
@ -1174,6 +1219,10 @@ export default {
|
||||||
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
|
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
|
||||||
APP.UI.handleLastNEndpoints(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) => {
|
room.on(ConferenceEvents.DOMINANT_SPEAKER_CHANGED, (id) => {
|
||||||
if (this.isLocalId(id)) {
|
if (this.isLocalId(id)) {
|
||||||
this.isDominantSpeaker = true;
|
this.isDominantSpeaker = true;
|
||||||
|
@ -1205,10 +1254,12 @@ export default {
|
||||||
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
room.on(ConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||||
connectionIsInterrupted = true;
|
connectionIsInterrupted = true;
|
||||||
ConnectionQuality.updateLocalConnectionQuality(0);
|
ConnectionQuality.updateLocalConnectionQuality(0);
|
||||||
|
APP.UI.showLocalConnectionInterrupted(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
|
room.on(ConferenceEvents.CONNECTION_RESTORED, () => {
|
||||||
connectionIsInterrupted = false;
|
connectionIsInterrupted = false;
|
||||||
|
APP.UI.showLocalConnectionInterrupted(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
|
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
|
||||||
|
|
|
@ -233,6 +233,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connection.connection_lost
|
||||||
|
{
|
||||||
|
color: #8B8B8B;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
.connection.connection_full
|
.connection.connection_full
|
||||||
{
|
{
|
||||||
color: #FFFFFF;/*#15A1ED*/
|
color: #FFFFFF;/*#15A1ED*/
|
||||||
|
@ -456,12 +462,44 @@
|
||||||
filter: grayscale(.5) opacity(0.8);
|
filter: grayscale(.5) opacity(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remoteVideoProblemFilter {
|
||||||
|
-webkit-filter: grayscale(100%);
|
||||||
|
filter: grayscale(100%);
|
||||||
|
}
|
||||||
|
|
||||||
.videoProblemFilter {
|
.videoProblemFilter {
|
||||||
-webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
|
-webkit-filter: blur(10px) grayscale(.5) opacity(0.8);
|
||||||
filter: blur(10px) grayscale(.5) opacity(0.8);
|
filter: blur(10px) grayscale(.5) opacity(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
#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;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -228,10 +228,11 @@
|
||||||
<img id="dominantSpeakerAvatar" src=""/>
|
<img id="dominantSpeakerAvatar" src=""/>
|
||||||
<canvas id="dominantSpeakerAudioLevel"></canvas>
|
<canvas id="dominantSpeakerAudioLevel"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="remoteConnectionMessage"></span>
|
||||||
<div id="largeVideoWrapper">
|
<div id="largeVideoWrapper">
|
||||||
<video id="largeVideo" muted="true" autoplay></video>
|
<video id="largeVideo" muted="true" autoplay></video>
|
||||||
</div>
|
</div>
|
||||||
<span id="videoConnectionMessage"></span>
|
<span id="localConnectionMessage"></span>
|
||||||
<span id="videoResolutionLabel">HD</span>
|
<span id="videoResolutionLabel">HD</span>
|
||||||
<span id="recordingLabel" class="centeredVideoLabel">
|
<span id="recordingLabel" class="centeredVideoLabel">
|
||||||
<span id="recordingLabelText"></span>
|
<span id="recordingLabelText"></span>
|
||||||
|
|
|
@ -324,7 +324,8 @@
|
||||||
"ATTACHED": "Attached",
|
"ATTACHED": "Attached",
|
||||||
"FETCH_SESSION_ID": "Obtaining session-id...",
|
"FETCH_SESSION_ID": "Obtaining session-id...",
|
||||||
"GOT_SESSION_ID": "Obtaining session-id... Done",
|
"GOT_SESSION_ID": "Obtaining session-id... Done",
|
||||||
"GET_SESSION_ID_ERROR": "Get session-id error: "
|
"GET_SESSION_ID_ERROR": "Get session-id error: ",
|
||||||
|
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
|
||||||
},
|
},
|
||||||
"recording":
|
"recording":
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.
|
* Sets the "raised hand" status for a participant.
|
||||||
*/
|
*/
|
||||||
|
@ -602,10 +613,11 @@ UI.getSharedDocumentManager = function () {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show user on UI.
|
* Show user on UI.
|
||||||
* @param {string} id user id
|
* @param {JitsiParticipant} user
|
||||||
* @param {string} displayName user nickname
|
|
||||||
*/
|
*/
|
||||||
UI.addUser = function (id, displayName) {
|
UI.addUser = function (user) {
|
||||||
|
var id = user.getId();
|
||||||
|
var displayName = user.getDisplayName();
|
||||||
UI.hideRingOverLay();
|
UI.hideRingOverLay();
|
||||||
ContactList.addContact(id);
|
ContactList.addContact(id);
|
||||||
|
|
||||||
|
@ -618,7 +630,7 @@ UI.addUser = function (id, displayName) {
|
||||||
UIUtil.playSoundNotification('userJoined');
|
UIUtil.playSoundNotification('userJoined');
|
||||||
|
|
||||||
// Add Peer's container
|
// Add Peer's container
|
||||||
VideoLayout.addParticipantContainer(id);
|
VideoLayout.addParticipantContainer(user);
|
||||||
|
|
||||||
// Configure avatar
|
// Configure avatar
|
||||||
UI.setUserEmail(id);
|
UI.setUserEmail(id);
|
||||||
|
@ -983,6 +995,17 @@ UI.handleLastNEndpoints = function (ids, enteringIds) {
|
||||||
VideoLayout.onLastNEndpointsChanged(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.
|
* Update audio level visualization for specified user.
|
||||||
* @param {string} id user id
|
* @param {string} id user id
|
||||||
|
|
|
@ -243,7 +243,7 @@ export default class SharedVideoManager {
|
||||||
|
|
||||||
let thumb = new SharedVideoThumb(self.url);
|
let thumb = new SharedVideoThumb(self.url);
|
||||||
thumb.setDisplayName(player.getVideoData().title);
|
thumb.setDisplayName(player.getVideoData().title);
|
||||||
VideoLayout.addParticipantContainer(self.url, thumb);
|
VideoLayout.addRemoteVideoContainer(self.url, thumb);
|
||||||
|
|
||||||
let iframe = player.getIframe();
|
let iframe = player.getIframe();
|
||||||
self.sharedVideo = new SharedVideoContainer(
|
self.sharedVideo = new SharedVideoContainer(
|
||||||
|
|
|
@ -245,13 +245,13 @@ ConnectionIndicator.prototype.showMore = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function createIcon(classes) {
|
function createIcon(classes, iconClass) {
|
||||||
var icon = document.createElement("span");
|
var icon = document.createElement("span");
|
||||||
for(var i in classes) {
|
for(var i in classes) {
|
||||||
icon.classList.add(classes[i]);
|
icon.classList.add(classes[i]);
|
||||||
}
|
}
|
||||||
icon.appendChild(
|
icon.appendChild(
|
||||||
document.createElement("i")).classList.add("icon-connection");
|
document.createElement("i")).classList.add(iconClass);
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,9 +282,12 @@ ConnectionIndicator.prototype.create = function () {
|
||||||
}.bind(this);
|
}.bind(this);
|
||||||
|
|
||||||
this.emptyIcon = this.connectionIndicatorContainer.appendChild(
|
this.emptyIcon = this.connectionIndicatorContainer.appendChild(
|
||||||
createIcon(["connection", "connection_empty"]));
|
createIcon(["connection", "connection_empty"], "icon-connection"));
|
||||||
this.fullIcon = this.connectionIndicatorContainer.appendChild(
|
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();
|
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
|
* Updates the data of the indicator
|
||||||
* @param percent the percent of connection quality
|
* @param percent the percent of connection quality
|
||||||
|
@ -314,12 +338,14 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
||||||
this.connectionIndicatorContainer.style.display = "block";
|
this.connectionIndicatorContainer.style.display = "block";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.bandwidth = object.bandwidth;
|
if (object) {
|
||||||
this.bitrate = object.bitrate;
|
this.bandwidth = object.bandwidth;
|
||||||
this.packetLoss = object.packetLoss;
|
this.bitrate = object.bitrate;
|
||||||
this.transport = object.transport;
|
this.packetLoss = object.packetLoss;
|
||||||
if (object.resolution) {
|
this.transport = object.transport;
|
||||||
this.resolution = object.resolution;
|
if (object.resolution) {
|
||||||
|
this.resolution = object.resolution;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (var quality in ConnectionIndicator.connectionQualityValues) {
|
for (var quality in ConnectionIndicator.connectionQualityValues) {
|
||||||
if (percent >= quality) {
|
if (percent >= quality) {
|
||||||
|
@ -327,7 +353,7 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
||||||
ConnectionIndicator.connectionQualityValues[quality];
|
ConnectionIndicator.connectionQualityValues[quality];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (object.isResolutionHD) {
|
if (object && typeof object.isResolutionHD === 'boolean') {
|
||||||
this.isResolutionHD = object.isResolutionHD;
|
this.isResolutionHD = object.isResolutionHD;
|
||||||
}
|
}
|
||||||
this.updateResolutionIndicator();
|
this.updateResolutionIndicator();
|
||||||
|
|
|
@ -5,12 +5,18 @@ import Avatar from "../avatar/Avatar";
|
||||||
import {createDeferred} from '../../util/helpers';
|
import {createDeferred} from '../../util/helpers';
|
||||||
import UIUtil from "../util/UIUtil";
|
import UIUtil from "../util/UIUtil";
|
||||||
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
|
import {VideoContainer, VIDEO_CONTAINER_TYPE} from "./VideoContainer";
|
||||||
|
import LargeContainer from "./LargeContainer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager for all Large containers.
|
* Manager for all Large containers.
|
||||||
*/
|
*/
|
||||||
export default class LargeVideoManager {
|
export default class LargeVideoManager {
|
||||||
constructor (emitter) {
|
constructor (emitter) {
|
||||||
|
/**
|
||||||
|
* The map of <tt>LargeContainer</tt>s where the key is the video
|
||||||
|
* container type.
|
||||||
|
* @type {Object.<string, LargeContainer>}
|
||||||
|
*/
|
||||||
this.containers = {};
|
this.containers = {};
|
||||||
|
|
||||||
this.state = VIDEO_CONTAINER_TYPE;
|
this.state = VIDEO_CONTAINER_TYPE;
|
||||||
|
@ -85,21 +91,18 @@ export default class LargeVideoManager {
|
||||||
* Called when the media connection has been interrupted.
|
* Called when the media connection has been interrupted.
|
||||||
*/
|
*/
|
||||||
onVideoInterrupted () {
|
onVideoInterrupted () {
|
||||||
this.enableVideoProblemFilter(true);
|
this.enableLocalConnectionProblemFilter(true);
|
||||||
let reconnectingKey = "connection.RECONNECTING";
|
this._setLocalConnectionMessage("connection.RECONNECTING")
|
||||||
$('#videoConnectionMessage')
|
|
||||||
.attr("data-i18n", reconnectingKey)
|
|
||||||
.text(APP.translation.translateString(reconnectingKey));
|
|
||||||
// Show the message only if the video is currently being displayed
|
// 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.
|
* Called when the media connection has been restored.
|
||||||
*/
|
*/
|
||||||
onVideoRestored () {
|
onVideoRestored () {
|
||||||
this.enableVideoProblemFilter(false);
|
this.enableLocalConnectionProblemFilter(false);
|
||||||
this.showVideoConnectionMessage(false);
|
this.showLocalConnectionMessage(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
get id () {
|
get id () {
|
||||||
|
@ -118,7 +121,8 @@ export default class LargeVideoManager {
|
||||||
|
|
||||||
// Include hide()/fadeOut only if we're switching between users
|
// Include hide()/fadeOut only if we're switching between users
|
||||||
let preUpdate;
|
let preUpdate;
|
||||||
if (this.newStreamData.id != this.id) {
|
let isUserSwitch = this.newStreamData.id != this.id;
|
||||||
|
if (isUserSwitch) {
|
||||||
preUpdate = container.hide();
|
preUpdate = container.hide();
|
||||||
} else {
|
} else {
|
||||||
preUpdate = Promise.resolve();
|
preUpdate = Promise.resolve();
|
||||||
|
@ -136,27 +140,53 @@ export default class LargeVideoManager {
|
||||||
// change the avatar url on large
|
// change the avatar url on large
|
||||||
this.updateAvatar(Avatar.getAvatarUrl(id));
|
this.updateAvatar(Avatar.getAvatarUrl(id));
|
||||||
|
|
||||||
|
// FIXME that does not really make sense, because the videoType
|
||||||
|
// (camera or desktop) is a completely different thing than
|
||||||
|
// the video container type (Etherpad, SharedVideo, VideoContainer).
|
||||||
|
// ----------------------------------------------------------------
|
||||||
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check
|
// If we the continer is VIDEO_CONTAINER_TYPE, we need to check
|
||||||
// its stream whether exist and is muted to set isVideoMuted
|
// its stream whether exist and is muted to set isVideoMuted
|
||||||
// in rest of the cases it is false
|
// in rest of the cases it is false
|
||||||
let isVideoMuted = false;
|
let showAvatar = false;
|
||||||
if (videoType == VIDEO_CONTAINER_TYPE)
|
if (videoType == VIDEO_CONTAINER_TYPE)
|
||||||
isVideoMuted = stream ? stream.isMuted() : true;
|
showAvatar = stream ? stream.isMuted() : true;
|
||||||
|
|
||||||
// show the avatar on large if needed
|
// If the user's connection is disrupted then the avatar will be
|
||||||
container.showAvatar(isVideoMuted);
|
// displayed in case we have no video image cached. That is if
|
||||||
|
// there was a user switch(image is lost on stream detach) or if
|
||||||
|
// the video was not rendered, before the connection has failed.
|
||||||
|
let isHavingConnectivityIssues
|
||||||
|
= APP.conference.isParticipantConnectionActive(id) === false;
|
||||||
|
if (isHavingConnectivityIssues
|
||||||
|
&& (isUserSwitch | !container.wasVideoRendered)) {
|
||||||
|
showAvatar = true;
|
||||||
|
}
|
||||||
|
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
// do not show stream if video is muted
|
// do not show stream if video is muted
|
||||||
// but we still should show watermark
|
// but we still should show watermark
|
||||||
if (isVideoMuted) {
|
if (showAvatar) {
|
||||||
this.showWatermark(true);
|
this.showWatermark(true);
|
||||||
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 {
|
} else {
|
||||||
promise = container.show();
|
promise = container.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// show the avatar on large if needed
|
||||||
|
container.showAvatar(showAvatar);
|
||||||
|
|
||||||
|
// Make sure no notification about remote failure is shown as
|
||||||
|
// it's UI conflicts with the one for local connection interrupted.
|
||||||
|
if (APP.conference.isConnectionInterrupted()) {
|
||||||
|
this.updateParticipantConnStatusIndication(id, true);
|
||||||
|
} else {
|
||||||
|
this.updateParticipantConnStatusIndication(
|
||||||
|
id, !isHavingConnectivityIssues);
|
||||||
|
}
|
||||||
|
|
||||||
// resolve updateLargeVideo promise after everything is done
|
// resolve updateLargeVideo promise after everything is done
|
||||||
promise.then(resolve);
|
promise.then(resolve);
|
||||||
|
|
||||||
|
@ -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.
|
* Update large video.
|
||||||
* Switches to large video even if previously other container was visible.
|
* Switches to large video even if previously other container was visible.
|
||||||
|
@ -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
|
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
|
||||||
*/
|
*/
|
||||||
enableVideoProblemFilter (enable) {
|
enableLocalConnectionProblemFilter (enable) {
|
||||||
this.videoContainer.enableVideoProblemFilter(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
|
* @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
|
* displayed or not. If missing the condition will be based on the value
|
||||||
* obtained from {@link APP.conference.isConnectionInterrupted}.
|
* obtained from {@link APP.conference.isConnectionInterrupted}.
|
||||||
*/
|
*/
|
||||||
showVideoConnectionMessage (show) {
|
showLocalConnectionMessage (show) {
|
||||||
if (typeof show !== 'boolean') {
|
if (typeof show !== 'boolean') {
|
||||||
show = APP.conference.isConnectionInterrupted();
|
show = APP.conference.isConnectionInterrupted();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
$('#videoConnectionMessage').css({display: "block"});
|
$('#localConnectionMessage').css({display: "block"});
|
||||||
|
// Avatar message conflicts with 'videoConnectionMessage',
|
||||||
|
// so it must be hidden
|
||||||
|
this.showRemoteConnectionMessage(false);
|
||||||
} else {
|
} 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.
|
* Add container of specified type.
|
||||||
* @param {string} type container 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
|
// be taking care of it by itself, but that is a bigger refactoring
|
||||||
if (this.state === VIDEO_CONTAINER_TYPE) {
|
if (this.state === VIDEO_CONTAINER_TYPE) {
|
||||||
this.showWatermark(false);
|
this.showWatermark(false);
|
||||||
this.showVideoConnectionMessage(false);
|
this.showLocalConnectionMessage(false);
|
||||||
|
this.showRemoteConnectionMessage(false);
|
||||||
}
|
}
|
||||||
oldContainer.hide();
|
oldContainer.hide();
|
||||||
|
|
||||||
|
@ -342,7 +470,11 @@ export default class LargeVideoManager {
|
||||||
// the container would be taking care of it by itself, but that
|
// the container would be taking care of it by itself, but that
|
||||||
// is a bigger refactoring
|
// is a bigger refactoring
|
||||||
this.showWatermark(true);
|
this.showWatermark(true);
|
||||||
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 */);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,7 +201,7 @@ LocalVideo.prototype.changeVideo = function (stream) {
|
||||||
localVideoContainer.removeChild(localVideo);
|
localVideoContainer.removeChild(localVideo);
|
||||||
// when removing only the video element and we are on stage
|
// when removing only the video element and we are on stage
|
||||||
// update the stage
|
// update the stage
|
||||||
if(this.VideoLayout.isCurrentlyOnLarge(this.id))
|
if(this.isCurrentlyOnLargeVideo())
|
||||||
this.VideoLayout.updateLargeVideo(this.id);
|
this.VideoLayout.updateLargeVideo(this.id);
|
||||||
stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
stream.off(TrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,17 +8,45 @@ import UIUtils from "../util/UIUtil";
|
||||||
import UIEvents from '../../../service/UI/UIEvents';
|
import UIEvents from '../../../service/UI/UIEvents';
|
||||||
import JitsiPopover from "../util/JitsiPopover";
|
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.emitter = emitter;
|
||||||
this.videoSpanId = `participant_${id}`;
|
this.videoSpanId = `participant_${this.id}`;
|
||||||
SmallVideo.call(this, VideoLayout);
|
SmallVideo.call(this, VideoLayout);
|
||||||
this.hasRemoteVideoMenu = false;
|
this.hasRemoteVideoMenu = false;
|
||||||
this.addRemoteVideoContainer();
|
this.addRemoteVideoContainer();
|
||||||
this.connectionIndicator = new ConnectionIndicator(this, id);
|
this.connectionIndicator = new ConnectionIndicator(this, this.id);
|
||||||
this.setDisplayName();
|
this.setDisplayName();
|
||||||
this.flipX = false;
|
this.flipX = false;
|
||||||
this.isLocal = 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);
|
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
|
* Adds the remote video menu element for the given <tt>id</tt> in the
|
||||||
* given <tt>parentElement</tt>.
|
* given <tt>parentElement</tt>.
|
||||||
|
@ -209,13 +264,88 @@ RemoteVideo.prototype.removeRemoteStreamElement = function (stream) {
|
||||||
var select = $('#' + elementID);
|
var select = $('#' + elementID);
|
||||||
select.remove();
|
select.remove();
|
||||||
|
|
||||||
|
if (isVideo) {
|
||||||
|
this.wasVideoPlayed = false;
|
||||||
|
}
|
||||||
|
|
||||||
console.info((isVideo ? "Video" : "Audio") +
|
console.info((isVideo ? "Video" : "Audio") +
|
||||||
" removed " + this.id, select);
|
" removed " + this.id, select);
|
||||||
|
|
||||||
// when removing only the video element and we are on stage
|
// when removing only the video element and we are on stage
|
||||||
// update the stage
|
// update the stage
|
||||||
if (isVideo && this.VideoLayout.isCurrentlyOnLarge(this.id))
|
if (isVideo && this.isCurrentlyOnLargeVideo())
|
||||||
this.VideoLayout.updateLargeVideo(this.id);
|
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
|
// Register 'onplaying' listener to trigger 'videoactive' on VideoLayout
|
||||||
// when video playback starts
|
// when video playback starts
|
||||||
var onPlayingHandler = function () {
|
var onPlayingHandler = function () {
|
||||||
|
self.wasVideoPlayed = true;
|
||||||
self.VideoLayout.videoactive(streamElement, self.id);
|
self.VideoLayout.videoactive(streamElement, self.id);
|
||||||
streamElement.onplaying = null;
|
streamElement.onplaying = null;
|
||||||
|
// Refresh to show the video
|
||||||
|
self.updateView();
|
||||||
};
|
};
|
||||||
streamElement.onplaying = onPlayingHandler;
|
streamElement.onplaying = onPlayingHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether or not video stream exists and has started for this
|
* Checks whether the video stream has started for this RemoteVideo instance.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* @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 () {
|
RemoteVideo.prototype.hasVideoStarted = function () {
|
||||||
var videoSelector = this.selectVideoElement();
|
return this.wasVideoPlayed;
|
||||||
return videoSelector.length && videoSelector[0].currentTime > 0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
||||||
|
|
|
@ -5,6 +5,27 @@ import UIEvents from "../../../service/UI/UIEvents";
|
||||||
|
|
||||||
const RTCUIHelper = JitsiMeetJS.util.RTCUIHelper;
|
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) {
|
function SmallVideo(VideoLayout) {
|
||||||
this.isAudioMuted = false;
|
this.isAudioMuted = false;
|
||||||
this.hasAvatar = false;
|
this.hasAvatar = false;
|
||||||
|
@ -337,6 +358,16 @@ SmallVideo.prototype.selectVideoElement = function () {
|
||||||
return $(RTCUIHelper.findVideoElement($('#' + this.videoSpanId)[0]));
|
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
|
* Enables / disables the css responsible for focusing/pinning a video
|
||||||
* thumbnail.
|
* thumbnail.
|
||||||
|
@ -359,6 +390,47 @@ SmallVideo.prototype.hasVideo = function () {
|
||||||
return this.selectVideoElement().length !== 0;
|
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.
|
* Hides or shows the user's avatar.
|
||||||
* This update assumes that large video had been updated and we will
|
* This update assumes that large video had been updated and we will
|
||||||
|
@ -378,46 +450,28 @@ SmallVideo.prototype.updateView = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let video = this.selectVideoElement();
|
// Determine whether video, avatar or blackness should be displayed
|
||||||
|
let displayMode = this.selectDisplayMode();
|
||||||
let avatar = $('#' + this.videoSpanId + ' .userAvatar');
|
// Show/hide video
|
||||||
|
setVisibility(this.selectVideoElement(), displayMode === DISPLAY_VIDEO);
|
||||||
var isCurrentlyOnLarge = this.VideoLayout.isCurrentlyOnLarge(this.id);
|
// Show/hide the avatar
|
||||||
|
setVisibility(this.$avatar(), displayMode === DISPLAY_AVATAR);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
|
SmallVideo.prototype.avatarChanged = function (avatarUrl) {
|
||||||
var thumbnail = $('#' + this.videoSpanId);
|
var thumbnail = $('#' + this.videoSpanId);
|
||||||
var avatar = $('#' + this.videoSpanId + ' .userAvatar');
|
var avatarSel = this.$avatar();
|
||||||
this.hasAvatar = true;
|
this.hasAvatar = true;
|
||||||
|
|
||||||
// set the avatar in the thumbnail
|
// set the avatar in the thumbnail
|
||||||
if (avatar && avatar.length > 0) {
|
if (avatarSel && avatarSel.length > 0) {
|
||||||
avatar[0].src = avatarUrl;
|
avatarSel[0].src = avatarUrl;
|
||||||
} else {
|
} else {
|
||||||
if (thumbnail && thumbnail.length > 0) {
|
if (thumbnail && thumbnail.length > 0) {
|
||||||
avatar = document.createElement('img');
|
var avatarElement = document.createElement('img');
|
||||||
avatar.className = 'userAvatar';
|
avatarElement.className = 'userAvatar';
|
||||||
avatar.src = avatarUrl;
|
avatarElement.src = avatarUrl;
|
||||||
thumbnail.append(avatar);
|
thumbnail.append(avatarElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -173,25 +173,51 @@ export class VideoContainer extends LargeContainer {
|
||||||
|
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag indicates whether or not the avatar is currently displayed.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
this.avatarDisplayed = false;
|
||||||
this.$avatar = $('#dominantSpeaker');
|
this.$avatar = $('#dominantSpeaker');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A jQuery selector of the remote connection message.
|
||||||
|
* @type {jQuery|HTMLElement}
|
||||||
|
*/
|
||||||
|
this.$remoteConnectionMessage = $('#remoteConnectionMessage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether or not the video stream attached to the video
|
||||||
|
* 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.$wrapper = $('#largeVideoWrapper');
|
||||||
|
|
||||||
this.avatarHeight = $("#dominantSpeakerAvatar").height();
|
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
|
// This does not work with Temasys plugin - has to be a property to be
|
||||||
// copied between new <object> elements
|
// copied between new <object> elements
|
||||||
//this.$video.on('play', onPlay);
|
//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
|
* 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
|
* @param {boolean} enable <tt>true</tt> if the filter is to be enabled or
|
||||||
* <tt>false</tt> otherwise.
|
* <tt>false</tt> otherwise.
|
||||||
*/
|
*/
|
||||||
enableVideoProblemFilter (enable) {
|
enableLocalConnectionProblemFilter (enable) {
|
||||||
this.$video.toggleClass("videoProblemFilter", 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) {
|
resize (containerWidth, containerHeight, animate = false) {
|
||||||
let [width, height]
|
let [width, height]
|
||||||
= this.getVideoSize(containerWidth, containerHeight);
|
= this.getVideoSize(containerWidth, containerHeight);
|
||||||
|
@ -263,6 +313,8 @@ export class VideoContainer extends LargeContainer {
|
||||||
|
|
||||||
this.$avatar.css('top', top);
|
this.$avatar.css('top', top);
|
||||||
|
|
||||||
|
this.positionRemoteConnectionMessage();
|
||||||
|
|
||||||
this.$wrapper.animate({
|
this.$wrapper.animate({
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
@ -284,6 +336,14 @@ export class VideoContainer extends LargeContainer {
|
||||||
* @param {string} videoType video type
|
* @param {string} videoType video type
|
||||||
*/
|
*/
|
||||||
setStream (stream, videoType) {
|
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
|
// detach old stream
|
||||||
if (this.stream) {
|
if (this.stream) {
|
||||||
this.stream.detach(this.$video[0]);
|
this.stream.detach(this.$video[0]);
|
||||||
|
@ -339,10 +399,23 @@ export class VideoContainer extends LargeContainer {
|
||||||
(show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
|
(show) ? interfaceConfig.DEFAULT_BACKGROUND : "#000");
|
||||||
|
|
||||||
this.$avatar.css("visibility", show ? "visible" : "hidden");
|
this.$avatar.css("visibility", show ? "visible" : "hidden");
|
||||||
|
this.avatarDisplayed = show;
|
||||||
|
|
||||||
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
|
this.emitter.emit(UIEvents.LARGE_VIDEO_AVATAR_DISPLAYED, show);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the remote user who is currently displayed by this video
|
||||||
|
* container is having connectivity issues.
|
||||||
|
*
|
||||||
|
* @param {boolean} show <tt>true</tt> to show or <tt>false</tt> to hide
|
||||||
|
* the indication.
|
||||||
|
*/
|
||||||
|
showRemoteConnectionProblemIndicator (show) {
|
||||||
|
this.$video.toggleClass("remoteVideoProblemFilter", show);
|
||||||
|
this.$avatar.toggleClass("remoteVideoProblemFilter", show);
|
||||||
|
}
|
||||||
|
|
||||||
// We are doing fadeOut/fadeIn animations on parent div which wraps
|
// We are doing fadeOut/fadeIn animations on parent div which wraps
|
||||||
// largeVideo, because when Temasys plugin is in use it replaces
|
// largeVideo, because when Temasys plugin is in use it replaces
|
||||||
// <video> elements with plugin <object> tag. In Safari jQuery is
|
// <video> elements with plugin <object> tag. In Safari jQuery is
|
||||||
|
|
|
@ -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
|
* @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;
|
let remoteVideo;
|
||||||
if(smallVideo)
|
if(smallVideo)
|
||||||
remoteVideo = smallVideo;
|
remoteVideo = smallVideo;
|
||||||
else
|
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;
|
remoteVideos[id] = remoteVideo;
|
||||||
|
|
||||||
let videoType = VideoLayout.getRemoteVideoType(id);
|
let videoType = VideoLayout.getRemoteVideoType(id);
|
||||||
|
@ -413,6 +425,8 @@ var VideoLayout = {
|
||||||
} else {
|
} else {
|
||||||
VideoLayout.resizeThumbnails(false, true);
|
VideoLayout.resizeThumbnails(false, true);
|
||||||
}
|
}
|
||||||
|
// Initialize the view
|
||||||
|
remoteVideo.updateView();
|
||||||
},
|
},
|
||||||
|
|
||||||
videoactive (videoelem, resourceJid) {
|
videoactive (videoelem, resourceJid) {
|
||||||
|
@ -487,6 +501,18 @@ var VideoLayout = {
|
||||||
localVideoThumbnail.showAudioIndicator(isMuted);
|
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.
|
* 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.
|
* On last N change event.
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue