Browse Source

ref(stats): process stats through one pub/sub

Instead of passing stats through UI then VideoLayout then the
SmallVideo, pass stats directly to what uses it--ConnectionIndicator.
This also bypasses adding the stats to the store, as they do not
seem to be something that needs to be shared or stored app-wide
just yet.
master
Leonard Kim 7 years ago
parent
commit
44bbd26c96

+ 2
- 12
conference.js View File

@@ -48,6 +48,7 @@ import {
48 48
     trackAdded,
49 49
     trackRemoved
50 50
 } from './react/features/base/tracks';
51
+import { statsEmitter } from './react/features/connection-indicator';
51 52
 import { showDesktopPicker } from  './react/features/desktop-picker';
52 53
 import {
53 54
     mediaPermissionPromptVisibilityChanged,
@@ -64,8 +65,6 @@ const ConferenceErrors = JitsiMeetJS.errors.conference;
64 65
 const TrackEvents = JitsiMeetJS.events.track;
65 66
 const TrackErrors = JitsiMeetJS.errors.track;
66 67
 
67
-const ConnectionQualityEvents = JitsiMeetJS.events.connectionQuality;
68
-
69 68
 const eventEmitter = new EventEmitter();
70 69
 
71 70
 let room;
@@ -1726,16 +1725,7 @@ export default {
1726 1725
             }
1727 1726
         });
1728 1727
 
1729
-        room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED,
1730
-            (stats) => {
1731
-                APP.UI.updateLocalStats(stats.connectionQuality, stats);
1732
-
1733
-        });
1734
-
1735
-        room.on(ConnectionQualityEvents.REMOTE_STATS_UPDATED,
1736
-            (id, stats) => {
1737
-                APP.UI.updateRemoteStats(id, stats.connectionQuality, stats);
1738
-        });
1728
+        statsEmitter.startListeningForStats(room);
1739 1729
 
1740 1730
         room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => {
1741 1731
             APP.UI.initEtherpad(value);

+ 0
- 19
modules/UI/UI.js View File

@@ -972,25 +972,6 @@ UI.hideStats = function () {
972 972
     VideoLayout.hideStats();
973 973
 };
974 974
 
975
-/**
976
- * Update local connection quality statistics.
977
- * @param {number} percent
978
- * @param {object} stats
979
- */
980
-UI.updateLocalStats = function (percent, stats) {
981
-    VideoLayout.updateLocalConnectionStats(percent, stats);
982
-};
983
-
984
-/**
985
- * Update connection quality statistics for remote user.
986
- * @param {string} id user id
987
- * @param {number} percent
988
- * @param {object} stats
989
- */
990
-UI.updateRemoteStats = function (id, percent, stats) {
991
-    VideoLayout.updateConnectionStats(id, percent, stats);
992
-};
993
-
994 975
 /**
995 976
  * Mark video as interrupted or not.
996 977
  * @param {boolean} interrupted if video is interrupted

+ 1
- 1
modules/UI/videolayout/LocalVideo.js View File

@@ -37,7 +37,7 @@ function LocalVideo(VideoLayout, emitter) {
37 37
     this.setDisplayName();
38 38
 
39 39
     this.addAudioLevelIndicator();
40
-    this.updateConnectionIndicator();
40
+    this.updateIndicators();
41 41
 }
42 42
 
43 43
 LocalVideo.prototype = Object.create(SmallVideo.prototype);

+ 1
- 13
modules/UI/videolayout/RemoteVideo.js View File

@@ -48,7 +48,7 @@ function RemoteVideo(user, VideoLayout, emitter) {
48 48
     this.hasRemoteVideoMenu = false;
49 49
     this._supportsRemoteControl = false;
50 50
     this.addRemoteVideoContainer();
51
-    this.updateConnectionIndicator();
51
+    this.updateIndicators();
52 52
     this.setDisplayName();
53 53
     this.bindHoverHandler();
54 54
     this.flipX = false;
@@ -632,18 +632,6 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
632 632
     }
633 633
 };
634 634
 
635
-RemoteVideo.prototype.updateResolution = function (resolution) {
636
-    this.updateConnectionIndicator({ resolution });
637
-};
638
-
639
-/**
640
- * Updates this video framerate indication.
641
- * @param framerate the value to update
642
- */
643
-RemoteVideo.prototype.updateFramerate = function (framerate) {
644
-    this.updateConnectionIndicator({ framerate });
645
-};
646
-
647 635
 /**
648 636
  * Sets the display name for the given video span id.
649 637
  *

+ 9
- 33
modules/UI/videolayout/SmallVideo.js View File

@@ -84,13 +84,14 @@ function SmallVideo(VideoLayout) {
84 84
     this.disableUpdateView = false;
85 85
 
86 86
     /**
87
-     * Statistics to display within the connection indicator. With new updates,
88
-     * only changed values are updated through assignment to a new reference.
87
+     * The current state of the user's bridge connection. The value should be
88
+     * a string as enumerated in the library's participantConnectionStatus
89
+     * constants.
89 90
      *
90 91
      * @private
91
-     * @type {object}
92
+     * @type {string|null}
92 93
      */
93
-    this._cachedConnectionStats = {};
94
+    this._connectionStatus = null;
94 95
 
95 96
     /**
96 97
      * Whether or not the ConnectionIndicator's popover is hovered. Modifies
@@ -260,18 +261,6 @@ SmallVideo.prototype.bindHoverHandler = function () {
260 261
     );
261 262
 };
262 263
 
263
-/**
264
- * Updates the data for the indicator
265
- * @param id the id of the indicator
266
- * @param percent the percent for connection quality
267
- * @param object the data
268
- */
269
-SmallVideo.prototype.updateConnectionStats = function (percent, object) {
270
-    const newStats = Object.assign({}, object, { percent });
271
-
272
-    this.updateConnectionIndicator(newStats);
273
-};
274
-
275 264
 /**
276 265
  * Unmounts the ConnectionIndicator component.
277 266
 
@@ -289,7 +278,8 @@ SmallVideo.prototype.removeConnectionIndicator = function () {
289 278
  * @returns {void}
290 279
  */
291 280
 SmallVideo.prototype.updateConnectionStatus = function (connectionStatus) {
292
-    this.updateConnectionIndicator({ connectionStatus });
281
+    this._connectionStatus = connectionStatus;
282
+    this.updateIndicators();
293 283
 };
294 284
 
295 285
 /**
@@ -741,21 +731,6 @@ SmallVideo.prototype.initBrowserSpecificProperties = function() {
741 731
     }
742 732
 };
743 733
 
744
-/**
745
- * Creates or updates the connection indicator. Updates the previously known
746
- * statistics about the participant's connection.
747
- *
748
- * @param {Object} newStats - New statistics to merge with previously known
749
- * statistics about the participant's connection.
750
- * @returns {void}
751
- */
752
-SmallVideo.prototype.updateConnectionIndicator = function (newStats = {}) {
753
-    this._cachedConnectionStats
754
-        = Object.assign({}, this._cachedConnectionStats, newStats);
755
-
756
-    this.updateIndicators();
757
-};
758
-
759 734
 /**
760 735
  * Updates the React element responsible for showing connection status, dominant
761 736
  * speaker, and raised hand icons. Uses instance variables to get the necessary
@@ -775,11 +750,12 @@ SmallVideo.prototype.updateIndicators = function () {
775 750
         <div>
776 751
             { this._showConnectionIndicator
777 752
                 ? <ConnectionIndicator
753
+                    connectionStatus = { this._connectionStatus }
778 754
                     iconSize = { iconSize }
779 755
                     isLocalVideo = { this.isLocal }
780 756
                     onHover = { this._onPopoverHover }
781 757
                     showMoreLink = { this.isLocal }
782
-                    stats = { this._cachedConnectionStats } />
758
+                    userID = { this.id } />
783 759
                 : null }
784 760
             { this._showRaisedHand
785 761
                 ? <RaisedHandIndicator iconSize = { iconSize } /> : null }

+ 5
- 54
modules/UI/videolayout/VideoLayout.js View File

@@ -211,6 +211,11 @@ var VideoLayout = {
211 211
         if (largeVideo && !largeVideo.id) {
212 212
             this.updateLargeVideo(APP.conference.getMyUserId(), true);
213 213
         }
214
+
215
+        // FIXME: replace this call with a generic update call once SmallVideo
216
+        // only contains a ReactElement. Then remove this call once the
217
+        // Filmstrip is fully in React.
218
+        localVideoThumbnail.updateIndicators();
214 219
     },
215 220
 
216 221
     /**
@@ -766,60 +771,6 @@ var VideoLayout = {
766 771
         }
767 772
     },
768 773
 
769
-    /**
770
-     * Updates local stats
771
-     * @param percent
772
-     * @param object
773
-     */
774
-    updateLocalConnectionStats (percent, object) {
775
-        const { framerate, resolution } = object;
776
-
777
-        // FIXME overwrites 'lib-jitsi-meet' internal object
778
-        // Why library internal objects are passed as event's args ?
779
-        object.resolution = resolution[APP.conference.getMyUserId()];
780
-        object.framerate = framerate[APP.conference.getMyUserId()];
781
-        localVideoThumbnail.updateConnectionStats(percent, object);
782
-
783
-        Object.keys(resolution).forEach(function (id) {
784
-            if (APP.conference.isLocalId(id)) {
785
-                return;
786
-            }
787
-
788
-            let resolutionValue = resolution[id];
789
-            let remoteVideo = remoteVideos[id];
790
-
791
-            if (resolutionValue && remoteVideo) {
792
-                remoteVideo.updateResolution(resolutionValue);
793
-            }
794
-        });
795
-
796
-        Object.keys(framerate).forEach(function (id) {
797
-            if (APP.conference.isLocalId(id)) {
798
-                return;
799
-            }
800
-
801
-            const framerateValue = framerate[id];
802
-            const remoteVideo = remoteVideos[id];
803
-
804
-            if (framerateValue && remoteVideo) {
805
-                remoteVideo.updateFramerate(framerateValue);
806
-            }
807
-        });
808
-    },
809
-
810
-    /**
811
-     * Updates remote stats.
812
-     * @param id the id associated with the stats
813
-     * @param percent the connection quality percent
814
-     * @param object the stats data
815
-     */
816
-    updateConnectionStats (id, percent, object) {
817
-        let remoteVideo = remoteVideos[id];
818
-        if (remoteVideo) {
819
-            remoteVideo.updateConnectionStats(percent, object);
820
-        }
821
-    },
822
-
823 774
     /**
824 775
      * Hides the connection indicator
825 776
      * @param id

+ 66
- 20
react/features/connection-indicator/components/ConnectionIndicator.js View File

@@ -5,6 +5,8 @@ import JitsiPopover from '../../../../modules/UI/util/JitsiPopover';
5 5
 import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
6 6
 import { ConnectionStatsTable } from '../../connection-stats';
7 7
 
8
+import statsEmitter from '../statsEmitter';
9
+
8 10
 declare var $: Object;
9 11
 declare var interfaceConfig: Object;
10 12
 
@@ -57,6 +59,14 @@ class ConnectionIndicator extends Component {
57 59
      * @static
58 60
      */
59 61
     static propTypes = {
62
+        /**
63
+         * The current condition of the user's connection, matching one of the
64
+         * enumerated values in the library.
65
+         *
66
+         * @type {JitsiParticipantConnectionStatus}
67
+         */
68
+        connectionStatus: React.PropTypes.string,
69
+
60 70
         /**
61 71
          * Whether or not the displays stats are for local video.
62 72
          */
@@ -74,25 +84,15 @@ class ConnectionIndicator extends Component {
74 84
         showMoreLink: React.PropTypes.bool,
75 85
 
76 86
         /**
77
-         * An object that contains statistics related to connection quality.
78
-         *
79
-         * {
80
-         *     bandwidth: Object,
81
-         *     bitrate: Object,
82
-         *     connectionStatus: String,
83
-         *     framerate: Object,
84
-         *     packetLoss: Object,
85
-         *     percent: Number,
86
-         *     resolution: Object,
87
-         *     transport:  Array
88
-         * }
87
+         * Invoked to obtain translated strings.
89 88
          */
90
-        stats: React.PropTypes.object,
89
+        t: React.PropTypes.func,
91 90
 
92 91
         /**
93
-         * Invoked to obtain translated strings.
92
+         * The user ID associated with the displayed connection indication and
93
+         * stats.
94 94
          */
95
-        t: React.PropTypes.func
95
+        userID: React.PropTypes.string
96 96
     };
97 97
 
98 98
     /**
@@ -121,10 +121,19 @@ class ConnectionIndicator extends Component {
121 121
              *
122 122
              * @type {boolean}
123 123
              */
124
-            showMoreStats: false
124
+            showMoreStats: false,
125
+
126
+            /**
127
+             * Cache of the stats received from subscribing to stats emitting.
128
+             * The keys should be the name of the stat. With each stat update,
129
+             * updates stats are mixed in with cached stats and a new stats
130
+             * object is set in state.
131
+             */
132
+            stats: {}
125 133
         };
126 134
 
127 135
         // Bind event handlers so they are only bound once for every instance.
136
+        this._onStatsUpdated = this._onStatsUpdated.bind(this);
128 137
         this._onToggleShowMore = this._onToggleShowMore.bind(this);
129 138
         this._setRootElement = this._setRootElement.bind(this);
130 139
     }
@@ -136,6 +145,9 @@ class ConnectionIndicator extends Component {
136 145
      * returns {void}
137 146
      */
138 147
     componentDidMount() {
148
+        statsEmitter.subscribeToClientStats(
149
+            this.props.userID, this._onStatsUpdated);
150
+
139 151
         this.popover = new JitsiPopover($(this._rootElement), {
140 152
             content: this._renderStatisticsTable(),
141 153
             skin: 'black',
@@ -153,7 +165,14 @@ class ConnectionIndicator extends Component {
153 165
      * @inheritdoc
154 166
      * returns {void}
155 167
      */
156
-    componentDidUpdate() {
168
+    componentDidUpdate(prevProps) {
169
+        if (prevProps.userID !== this.props.userID) {
170
+            statsEmitter.unsubscribeToClientStats(
171
+                this.props.userID, this._onStatsUpdated);
172
+            statsEmitter.subscribeToClientStats(
173
+                this.props.userID, this._onStatsUpdated);
174
+        }
175
+
157 176
         this.popover.updateContent(this._renderStatisticsTable());
158 177
     }
159 178
 
@@ -164,6 +183,9 @@ class ConnectionIndicator extends Component {
164 183
      * returns {void}
165 184
      */
166 185
     componentWillUnmount() {
186
+        statsEmitter.unsubscribeToClientStats(
187
+            this.props.userID, this._onStatsUpdated);
188
+
167 189
         this.popover.forceHide();
168 190
         this.popover.remove();
169 191
     }
@@ -186,6 +208,30 @@ class ConnectionIndicator extends Component {
186 208
         );
187 209
     }
188 210
 
211
+    /**
212
+     * Callback invoked when new connection stats associated with the passed in
213
+     * user ID are available. Will update the component's display of current
214
+     * statistics.
215
+     *
216
+     * @param {Object} stats - Connection stats from the library.
217
+     * @private
218
+     * @returns {void}
219
+     */
220
+    _onStatsUpdated(stats = {}) {
221
+        const { connectionQuality } = stats;
222
+        const newPercentageState = typeof connectionQuality === 'undefined'
223
+            ? {} : { percent: connectionQuality };
224
+        const newStats = Object.assign(
225
+            {},
226
+            this.state.stats,
227
+            stats,
228
+            newPercentageState);
229
+
230
+        this.setState({
231
+            stats: newStats
232
+        });
233
+    }
234
+
189 235
     /**
190 236
      * Callback to invoke when the show more link in the popover content is
191 237
      * clicked. Sets the state which will determine if the popover should show
@@ -204,7 +250,7 @@ class ConnectionIndicator extends Component {
204 250
      * @returns {ReactElement}
205 251
      */
206 252
     _renderIcon() {
207
-        switch (this.props.stats.connectionStatus) {
253
+        switch (this.props.connectionStatus) {
208 254
         case JitsiParticipantConnectionStatus.INTERRUPTED:
209 255
             return (
210 256
                 <span className = 'connection_lost'>
@@ -218,7 +264,7 @@ class ConnectionIndicator extends Component {
218 264
                 </span>
219 265
             );
220 266
         default: {
221
-            const { percent } = this.props.stats;
267
+            const { percent } = this.state.stats;
222 268
             const width = QUALITY_TO_WIDTH.find(x => percent >= x.percent);
223 269
             const iconWidth = width && width.width
224 270
                 ? { width: width && width.width } : {};
@@ -253,7 +299,7 @@ class ConnectionIndicator extends Component {
253 299
             packetLoss,
254 300
             resolution,
255 301
             transport
256
-        } = this.props.stats;
302
+        } = this.state.stats;
257 303
 
258 304
         return (
259 305
             <ConnectionStatsTable

+ 2
- 0
react/features/connection-indicator/index.js View File

@@ -1 +1,3 @@
1 1
 export * from './components';
2
+
3
+export { default as statsEmitter } from './statsEmitter';

+ 147
- 0
react/features/connection-indicator/statsEmitter.js View File

@@ -0,0 +1,147 @@
1
+import JitsiMeetJS from '../base/lib-jitsi-meet';
2
+
3
+declare var APP: Object;
4
+
5
+/**
6
+ * Contains all the callbacks to be notified when stats are updated.
7
+ *
8
+ * {
9
+ *     userId: Function[]
10
+ * }
11
+ */
12
+const subscribers = {};
13
+
14
+/**
15
+ * A singleton that acts as a pub/sub service for connection stat updates.
16
+ */
17
+const statsEmitter = {
18
+    /**
19
+     * Have {@code statsEmitter} subscribe to stat updates from a given
20
+     * conference.
21
+     *
22
+     * @param {JitsiConference} conference - The conference for which
23
+     * {@code statsEmitter} should subscribe for stat updates.
24
+     * @returns {void}
25
+     */
26
+    startListeningForStats(conference) {
27
+        const { connectionQuality } = JitsiMeetJS.events;
28
+
29
+        conference.on(connectionQuality.LOCAL_STATS_UPDATED,
30
+            stats => this._onStatsUpdated(stats));
31
+
32
+        conference.on(connectionQuality.REMOTE_STATS_UPDATED,
33
+            (id, stats) => this._emitStatsUpdate(id, stats));
34
+    },
35
+
36
+    /**
37
+     * Add a subscriber to be notified when stats are updated for a specified
38
+     * user id.
39
+     *
40
+     * @param {string} id - The user id whose stats updates are of interest.
41
+     * @param {Function} callback - The function to invoke when stats for the
42
+     * user have been updated.
43
+     * @returns {void}
44
+     */
45
+    subscribeToClientStats(id, callback) {
46
+        if (!id) {
47
+            return;
48
+        }
49
+
50
+        if (!subscribers[id]) {
51
+            subscribers[id] = [];
52
+        }
53
+
54
+        subscribers[id].push(callback);
55
+    },
56
+
57
+    /**
58
+     * Remove a subscriber that is listening for stats updates for a specified
59
+     * user id.
60
+     *
61
+     * @param {string} id - The user id whose stats updates are no longer of
62
+     * interest.
63
+     * @param {Function} callback - The function that is currently subscribed to
64
+     * stat updates for the specified user id.
65
+     * @returns {void}
66
+     */
67
+    unsubscribeToClientStats(id, callback) {
68
+        if (!subscribers[id]) {
69
+            return;
70
+        }
71
+
72
+        const filteredSubscribers = subscribers[id].filter(
73
+            subscriber => subscriber !== callback);
74
+
75
+        if (filteredSubscribers.length) {
76
+            subscribers[id] = filteredSubscribers;
77
+        } else {
78
+            delete subscribers[id];
79
+        }
80
+    },
81
+
82
+    /**
83
+     * Emit a stat update to all those listening for a specific user's
84
+     * connection stats.
85
+     *
86
+     * @param {string} id - The user id the stats are associated with.
87
+     * @param {Object} stats - New connection stats for the user.
88
+     * @returns {void}
89
+     */
90
+    _emitStatsUpdate(id, stats = {}) {
91
+        const callbacks = subscribers[id] || [];
92
+
93
+        callbacks.forEach(callback => {
94
+            callback(stats);
95
+        });
96
+    },
97
+
98
+    /**
99
+     * Emit a stat update to all those listening for local stat updates. Will
100
+     * also update listeners of remote user stats of changes related to their
101
+     * stats.
102
+     *
103
+     * @param {Object} stats - Connection stats for the local user as provided
104
+     * by the library.
105
+     * @returns {void}
106
+     */
107
+    _onStatsUpdated(stats) {
108
+        const allUserFramerates = stats.framerate;
109
+        const allUserResolutions = stats.resolution;
110
+
111
+        const currentUserId = APP.conference.getMyUserId();
112
+        const currentUserFramerate = allUserFramerates[currentUserId];
113
+        const currentUserResolution = allUserResolutions[currentUserId];
114
+
115
+        // FIXME resolution and framerate are hashes keyed off of user ids with
116
+        // stat values. Receivers of stats expect resolution and framerate to
117
+        // be primatives, not hashes, so overwrites the 'lib-jitsi-meet' stats
118
+        // objects.
119
+        stats.framerate = currentUserFramerate;
120
+        stats.resolution = currentUserResolution;
121
+
122
+        this._emitStatsUpdate(currentUserId, stats);
123
+
124
+        Object.keys(allUserFramerates)
125
+            .filter(id => id !== currentUserId)
126
+            .forEach(id => {
127
+                const framerate = allUserFramerates[id];
128
+
129
+                if (framerate) {
130
+                    this._emitStatsUpdate(id, { framerate });
131
+                }
132
+            });
133
+
134
+        Object.keys(allUserResolutions)
135
+            .filter(id => id !== currentUserId)
136
+            .forEach(id => {
137
+                const resolution = allUserResolutions[id];
138
+
139
+                if (resolution) {
140
+                    this._emitStatsUpdate(id, { resolution });
141
+                }
142
+            });
143
+
144
+    }
145
+};
146
+
147
+export default statsEmitter;

Loading…
Cancel
Save