diff --git a/conference.js b/conference.js
index 25252db96..baa9d3bc6 100644
--- a/conference.js
+++ b/conference.js
@@ -48,6 +48,7 @@ import {
trackAdded,
trackRemoved
} from './react/features/base/tracks';
+import { statsEmitter } from './react/features/connection-indicator';
import { showDesktopPicker } from './react/features/desktop-picker';
import {
mediaPermissionPromptVisibilityChanged,
@@ -64,8 +65,6 @@ const ConferenceErrors = JitsiMeetJS.errors.conference;
const TrackEvents = JitsiMeetJS.events.track;
const TrackErrors = JitsiMeetJS.errors.track;
-const ConnectionQualityEvents = JitsiMeetJS.events.connectionQuality;
-
const eventEmitter = new EventEmitter();
let room;
@@ -1726,16 +1725,7 @@ export default {
}
});
- room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED,
- (stats) => {
- APP.UI.updateLocalStats(stats.connectionQuality, stats);
-
- });
-
- room.on(ConnectionQualityEvents.REMOTE_STATS_UPDATED,
- (id, stats) => {
- APP.UI.updateRemoteStats(id, stats.connectionQuality, stats);
- });
+ statsEmitter.startListeningForStats(room);
room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => {
APP.UI.initEtherpad(value);
diff --git a/modules/UI/UI.js b/modules/UI/UI.js
index 930f7315b..36c8801fd 100644
--- a/modules/UI/UI.js
+++ b/modules/UI/UI.js
@@ -972,25 +972,6 @@ UI.hideStats = function () {
VideoLayout.hideStats();
};
-/**
- * Update local connection quality statistics.
- * @param {number} percent
- * @param {object} stats
- */
-UI.updateLocalStats = function (percent, stats) {
- VideoLayout.updateLocalConnectionStats(percent, stats);
-};
-
-/**
- * Update connection quality statistics for remote user.
- * @param {string} id user id
- * @param {number} percent
- * @param {object} stats
- */
-UI.updateRemoteStats = function (id, percent, stats) {
- VideoLayout.updateConnectionStats(id, percent, stats);
-};
-
/**
* Mark video as interrupted or not.
* @param {boolean} interrupted if video is interrupted
diff --git a/modules/UI/videolayout/LocalVideo.js b/modules/UI/videolayout/LocalVideo.js
index ef639285e..3b41e79d3 100644
--- a/modules/UI/videolayout/LocalVideo.js
+++ b/modules/UI/videolayout/LocalVideo.js
@@ -37,7 +37,7 @@ function LocalVideo(VideoLayout, emitter) {
this.setDisplayName();
this.addAudioLevelIndicator();
- this.updateConnectionIndicator();
+ this.updateIndicators();
}
LocalVideo.prototype = Object.create(SmallVideo.prototype);
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index 54d8ccb2a..6127f332e 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -48,7 +48,7 @@ function RemoteVideo(user, VideoLayout, emitter) {
this.hasRemoteVideoMenu = false;
this._supportsRemoteControl = false;
this.addRemoteVideoContainer();
- this.updateConnectionIndicator();
+ this.updateIndicators();
this.setDisplayName();
this.bindHoverHandler();
this.flipX = false;
@@ -632,18 +632,6 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
}
};
-RemoteVideo.prototype.updateResolution = function (resolution) {
- this.updateConnectionIndicator({ resolution });
-};
-
-/**
- * Updates this video framerate indication.
- * @param framerate the value to update
- */
-RemoteVideo.prototype.updateFramerate = function (framerate) {
- this.updateConnectionIndicator({ framerate });
-};
-
/**
* Sets the display name for the given video span id.
*
diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js
index b6e72a7ee..6f9853839 100644
--- a/modules/UI/videolayout/SmallVideo.js
+++ b/modules/UI/videolayout/SmallVideo.js
@@ -84,13 +84,14 @@ function SmallVideo(VideoLayout) {
this.disableUpdateView = false;
/**
- * Statistics to display within the connection indicator. With new updates,
- * only changed values are updated through assignment to a new reference.
+ * The current state of the user's bridge connection. The value should be
+ * a string as enumerated in the library's participantConnectionStatus
+ * constants.
*
* @private
- * @type {object}
+ * @type {string|null}
*/
- this._cachedConnectionStats = {};
+ this._connectionStatus = null;
/**
* Whether or not the ConnectionIndicator's popover is hovered. Modifies
@@ -260,18 +261,6 @@ SmallVideo.prototype.bindHoverHandler = function () {
);
};
-/**
- * Updates the data for the indicator
- * @param id the id of the indicator
- * @param percent the percent for connection quality
- * @param object the data
- */
-SmallVideo.prototype.updateConnectionStats = function (percent, object) {
- const newStats = Object.assign({}, object, { percent });
-
- this.updateConnectionIndicator(newStats);
-};
-
/**
* Unmounts the ConnectionIndicator component.
@@ -289,7 +278,8 @@ SmallVideo.prototype.removeConnectionIndicator = function () {
* @returns {void}
*/
SmallVideo.prototype.updateConnectionStatus = function (connectionStatus) {
- this.updateConnectionIndicator({ connectionStatus });
+ this._connectionStatus = connectionStatus;
+ this.updateIndicators();
};
/**
@@ -741,21 +731,6 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
}
};
-/**
- * Creates or updates the connection indicator. Updates the previously known
- * statistics about the participant's connection.
- *
- * @param {Object} newStats - New statistics to merge with previously known
- * statistics about the participant's connection.
- * @returns {void}
- */
-SmallVideo.prototype.updateConnectionIndicator = function (newStats = {}) {
- this._cachedConnectionStats
- = Object.assign({}, this._cachedConnectionStats, newStats);
-
- this.updateIndicators();
-};
-
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
@@ -775,11 +750,12 @@ SmallVideo.prototype.updateIndicators = function () {
{ this._showConnectionIndicator
?
+ userID = { this.id } />
: null }
{ this._showRaisedHand
? : null }
diff --git a/modules/UI/videolayout/VideoLayout.js b/modules/UI/videolayout/VideoLayout.js
index be4cc2f5c..a040bfc1c 100644
--- a/modules/UI/videolayout/VideoLayout.js
+++ b/modules/UI/videolayout/VideoLayout.js
@@ -211,6 +211,11 @@ var VideoLayout = {
if (largeVideo && !largeVideo.id) {
this.updateLargeVideo(APP.conference.getMyUserId(), true);
}
+
+ // FIXME: replace this call with a generic update call once SmallVideo
+ // only contains a ReactElement. Then remove this call once the
+ // Filmstrip is fully in React.
+ localVideoThumbnail.updateIndicators();
},
/**
@@ -766,60 +771,6 @@ var VideoLayout = {
}
},
- /**
- * Updates local stats
- * @param percent
- * @param object
- */
- updateLocalConnectionStats (percent, object) {
- const { framerate, resolution } = object;
-
- // FIXME overwrites 'lib-jitsi-meet' internal object
- // Why library internal objects are passed as event's args ?
- object.resolution = resolution[APP.conference.getMyUserId()];
- object.framerate = framerate[APP.conference.getMyUserId()];
- localVideoThumbnail.updateConnectionStats(percent, object);
-
- Object.keys(resolution).forEach(function (id) {
- if (APP.conference.isLocalId(id)) {
- return;
- }
-
- let resolutionValue = resolution[id];
- let remoteVideo = remoteVideos[id];
-
- if (resolutionValue && remoteVideo) {
- remoteVideo.updateResolution(resolutionValue);
- }
- });
-
- Object.keys(framerate).forEach(function (id) {
- if (APP.conference.isLocalId(id)) {
- return;
- }
-
- const framerateValue = framerate[id];
- const remoteVideo = remoteVideos[id];
-
- if (framerateValue && remoteVideo) {
- remoteVideo.updateFramerate(framerateValue);
- }
- });
- },
-
- /**
- * Updates remote stats.
- * @param id the id associated with the stats
- * @param percent the connection quality percent
- * @param object the stats data
- */
- updateConnectionStats (id, percent, object) {
- let remoteVideo = remoteVideos[id];
- if (remoteVideo) {
- remoteVideo.updateConnectionStats(percent, object);
- }
- },
-
/**
* Hides the connection indicator
* @param id
diff --git a/react/features/connection-indicator/components/ConnectionIndicator.js b/react/features/connection-indicator/components/ConnectionIndicator.js
index 37f3b261c..f67288d95 100644
--- a/react/features/connection-indicator/components/ConnectionIndicator.js
+++ b/react/features/connection-indicator/components/ConnectionIndicator.js
@@ -5,6 +5,8 @@ import JitsiPopover from '../../../../modules/UI/util/JitsiPopover';
import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
import { ConnectionStatsTable } from '../../connection-stats';
+import statsEmitter from '../statsEmitter';
+
declare var $: Object;
declare var interfaceConfig: Object;
@@ -57,6 +59,14 @@ class ConnectionIndicator extends Component {
* @static
*/
static propTypes = {
+ /**
+ * The current condition of the user's connection, matching one of the
+ * enumerated values in the library.
+ *
+ * @type {JitsiParticipantConnectionStatus}
+ */
+ connectionStatus: React.PropTypes.string,
+
/**
* Whether or not the displays stats are for local video.
*/
@@ -73,26 +83,16 @@ class ConnectionIndicator extends Component {
*/
showMoreLink: React.PropTypes.bool,
- /**
- * An object that contains statistics related to connection quality.
- *
- * {
- * bandwidth: Object,
- * bitrate: Object,
- * connectionStatus: String,
- * framerate: Object,
- * packetLoss: Object,
- * percent: Number,
- * resolution: Object,
- * transport: Array
- * }
- */
- stats: React.PropTypes.object,
-
/**
* Invoked to obtain translated strings.
*/
- t: React.PropTypes.func
+ t: React.PropTypes.func,
+
+ /**
+ * The user ID associated with the displayed connection indication and
+ * stats.
+ */
+ userID: React.PropTypes.string
};
/**
@@ -121,10 +121,19 @@ class ConnectionIndicator extends Component {
*
* @type {boolean}
*/
- showMoreStats: false
+ showMoreStats: false,
+
+ /**
+ * Cache of the stats received from subscribing to stats emitting.
+ * The keys should be the name of the stat. With each stat update,
+ * updates stats are mixed in with cached stats and a new stats
+ * object is set in state.
+ */
+ stats: {}
};
// Bind event handlers so they are only bound once for every instance.
+ this._onStatsUpdated = this._onStatsUpdated.bind(this);
this._onToggleShowMore = this._onToggleShowMore.bind(this);
this._setRootElement = this._setRootElement.bind(this);
}
@@ -136,6 +145,9 @@ class ConnectionIndicator extends Component {
* returns {void}
*/
componentDidMount() {
+ statsEmitter.subscribeToClientStats(
+ this.props.userID, this._onStatsUpdated);
+
this.popover = new JitsiPopover($(this._rootElement), {
content: this._renderStatisticsTable(),
skin: 'black',
@@ -153,7 +165,14 @@ class ConnectionIndicator extends Component {
* @inheritdoc
* returns {void}
*/
- componentDidUpdate() {
+ componentDidUpdate(prevProps) {
+ if (prevProps.userID !== this.props.userID) {
+ statsEmitter.unsubscribeToClientStats(
+ prevProps.userID, this._onStatsUpdated);
+ statsEmitter.subscribeToClientStats(
+ this.props.userID, this._onStatsUpdated);
+ }
+
this.popover.updateContent(this._renderStatisticsTable());
}
@@ -164,6 +183,9 @@ class ConnectionIndicator extends Component {
* returns {void}
*/
componentWillUnmount() {
+ statsEmitter.unsubscribeToClientStats(
+ this.props.userID, this._onStatsUpdated);
+
this.popover.forceHide();
this.popover.remove();
}
@@ -186,6 +208,30 @@ class ConnectionIndicator extends Component {
);
}
+ /**
+ * Callback invoked when new connection stats associated with the passed in
+ * user ID are available. Will update the component's display of current
+ * statistics.
+ *
+ * @param {Object} stats - Connection stats from the library.
+ * @private
+ * @returns {void}
+ */
+ _onStatsUpdated(stats = {}) {
+ const { connectionQuality } = stats;
+ const newPercentageState = typeof connectionQuality === 'undefined'
+ ? {} : { percent: connectionQuality };
+ const newStats = Object.assign(
+ {},
+ this.state.stats,
+ stats,
+ newPercentageState);
+
+ this.setState({
+ stats: newStats
+ });
+ }
+
/**
* Callback to invoke when the show more link in the popover content is
* clicked. Sets the state which will determine if the popover should show
@@ -204,7 +250,7 @@ class ConnectionIndicator extends Component {
* @returns {ReactElement}
*/
_renderIcon() {
- switch (this.props.stats.connectionStatus) {
+ switch (this.props.connectionStatus) {
case JitsiParticipantConnectionStatus.INTERRUPTED:
return (
@@ -218,7 +264,7 @@ class ConnectionIndicator extends Component {
);
default: {
- const { percent } = this.props.stats;
+ const { percent } = this.state.stats;
const width = QUALITY_TO_WIDTH.find(x => percent >= x.percent);
const iconWidth = width && width.width
? { width: width && width.width } : {};
@@ -253,7 +299,7 @@ class ConnectionIndicator extends Component {
packetLoss,
resolution,
transport
- } = this.props.stats;
+ } = this.state.stats;
return (
this._onStatsUpdated(conference.myUserId(), stats));
+
+ conference.on(connectionQuality.REMOTE_STATS_UPDATED,
+ (id, stats) => this._emitStatsUpdate(id, stats));
+ },
+
+ /**
+ * Add a subscriber to be notified when stats are updated for a specified
+ * user id.
+ *
+ * @param {string} id - The user id whose stats updates are of interest.
+ * @param {Function} callback - The function to invoke when stats for the
+ * user have been updated.
+ * @returns {void}
+ */
+ subscribeToClientStats(id, callback) {
+ if (!id) {
+ return;
+ }
+
+ if (!subscribers[id]) {
+ subscribers[id] = [];
+ }
+
+ subscribers[id].push(callback);
+ },
+
+ /**
+ * Remove a subscriber that is listening for stats updates for a specified
+ * user id.
+ *
+ * @param {string} id - The user id whose stats updates are no longer of
+ * interest.
+ * @param {Function} callback - The function that is currently subscribed to
+ * stat updates for the specified user id.
+ * @returns {void}
+ */
+ unsubscribeToClientStats(id, callback) {
+ if (!subscribers[id]) {
+ return;
+ }
+
+ const filteredSubscribers = subscribers[id].filter(
+ subscriber => subscriber !== callback);
+
+ if (filteredSubscribers.length) {
+ subscribers[id] = filteredSubscribers;
+ } else {
+ delete subscribers[id];
+ }
+ },
+
+ /**
+ * Emit a stat update to all those listening for a specific user's
+ * connection stats.
+ *
+ * @param {string} id - The user id the stats are associated with.
+ * @param {Object} stats - New connection stats for the user.
+ * @returns {void}
+ */
+ _emitStatsUpdate(id, stats = {}) {
+ const callbacks = subscribers[id] || [];
+
+ callbacks.forEach(callback => {
+ callback(stats);
+ });
+ },
+
+ /**
+ * Emit a stat update to all those listening for local stat updates. Will
+ * also update listeners of remote user stats of changes related to their
+ * stats.
+ *
+ * @param {string} currentUserId - The user id for the local user.
+ * @param {Object} stats - Connection stats for the local user as provided
+ * by the library.
+ * @returns {void}
+ */
+ _onStatsUpdated(currentUserId, stats) {
+ const allUserFramerates = stats.framerate;
+ const allUserResolutions = stats.resolution;
+
+ const currentUserFramerate = allUserFramerates[currentUserId];
+ const currentUserResolution = allUserResolutions[currentUserId];
+
+ // FIXME resolution and framerate are hashes keyed off of user ids with
+ // stat values. Receivers of stats expect resolution and framerate to
+ // be primatives, not hashes, so overwrites the 'lib-jitsi-meet' stats
+ // objects.
+ stats.framerate = currentUserFramerate;
+ stats.resolution = currentUserResolution;
+
+ this._emitStatsUpdate(currentUserId, stats);
+
+ // Get all the unique user ids from the framerate and resolution stats
+ // and update remote user stats as needed.
+ const framerateUserIds = Object.keys(allUserFramerates);
+ const resolutionUserIds = Object.keys(allUserResolutions);
+
+ _.union(framerateUserIds, resolutionUserIds)
+ .filter(id => id !== currentUserId)
+ .forEach(id => {
+ const remoteUserStats = {};
+
+ const framerate = allUserFramerates[id];
+
+ if (framerate) {
+ remoteUserStats.framerate = framerate;
+ }
+
+ const resolution = allUserResolutions[id];
+
+ if (resolution) {
+ remoteUserStats.resolution = resolution;
+ }
+
+ this._emitStatsUpdate(id, remoteUserStats);
+ });
+ }
+};
+
+export default statsEmitter;