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 8 years ago
parent
commit
44bbd26c96

+ 2
- 12
conference.js View File

48
     trackAdded,
48
     trackAdded,
49
     trackRemoved
49
     trackRemoved
50
 } from './react/features/base/tracks';
50
 } from './react/features/base/tracks';
51
+import { statsEmitter } from './react/features/connection-indicator';
51
 import { showDesktopPicker } from  './react/features/desktop-picker';
52
 import { showDesktopPicker } from  './react/features/desktop-picker';
52
 import {
53
 import {
53
     mediaPermissionPromptVisibilityChanged,
54
     mediaPermissionPromptVisibilityChanged,
64
 const TrackEvents = JitsiMeetJS.events.track;
65
 const TrackEvents = JitsiMeetJS.events.track;
65
 const TrackErrors = JitsiMeetJS.errors.track;
66
 const TrackErrors = JitsiMeetJS.errors.track;
66
 
67
 
67
-const ConnectionQualityEvents = JitsiMeetJS.events.connectionQuality;
68
-
69
 const eventEmitter = new EventEmitter();
68
 const eventEmitter = new EventEmitter();
70
 
69
 
71
 let room;
70
 let room;
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
         room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => {
1730
         room.addCommandListener(this.commands.defaults.ETHERPAD, ({value}) => {
1741
             APP.UI.initEtherpad(value);
1731
             APP.UI.initEtherpad(value);

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

972
     VideoLayout.hideStats();
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
  * Mark video as interrupted or not.
976
  * Mark video as interrupted or not.
996
  * @param {boolean} interrupted if video is interrupted
977
  * @param {boolean} interrupted if video is interrupted

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

37
     this.setDisplayName();
37
     this.setDisplayName();
38
 
38
 
39
     this.addAudioLevelIndicator();
39
     this.addAudioLevelIndicator();
40
-    this.updateConnectionIndicator();
40
+    this.updateIndicators();
41
 }
41
 }
42
 
42
 
43
 LocalVideo.prototype = Object.create(SmallVideo.prototype);
43
 LocalVideo.prototype = Object.create(SmallVideo.prototype);

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

48
     this.hasRemoteVideoMenu = false;
48
     this.hasRemoteVideoMenu = false;
49
     this._supportsRemoteControl = false;
49
     this._supportsRemoteControl = false;
50
     this.addRemoteVideoContainer();
50
     this.addRemoteVideoContainer();
51
-    this.updateConnectionIndicator();
51
+    this.updateIndicators();
52
     this.setDisplayName();
52
     this.setDisplayName();
53
     this.bindHoverHandler();
53
     this.bindHoverHandler();
54
     this.flipX = false;
54
     this.flipX = false;
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
  * Sets the display name for the given video span id.
636
  * Sets the display name for the given video span id.
649
  *
637
  *

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

84
     this.disableUpdateView = false;
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
      * @private
91
      * @private
91
-     * @type {object}
92
+     * @type {string|null}
92
      */
93
      */
93
-    this._cachedConnectionStats = {};
94
+    this._connectionStatus = null;
94
 
95
 
95
     /**
96
     /**
96
      * Whether or not the ConnectionIndicator's popover is hovered. Modifies
97
      * Whether or not the ConnectionIndicator's popover is hovered. Modifies
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
  * Unmounts the ConnectionIndicator component.
265
  * Unmounts the ConnectionIndicator component.
277
 
266
 
289
  * @returns {void}
278
  * @returns {void}
290
  */
279
  */
291
 SmallVideo.prototype.updateConnectionStatus = function (connectionStatus) {
280
 SmallVideo.prototype.updateConnectionStatus = function (connectionStatus) {
292
-    this.updateConnectionIndicator({ connectionStatus });
281
+    this._connectionStatus = connectionStatus;
282
+    this.updateIndicators();
293
 };
283
 };
294
 
284
 
295
 /**
285
 /**
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
  * Updates the React element responsible for showing connection status, dominant
735
  * Updates the React element responsible for showing connection status, dominant
761
  * speaker, and raised hand icons. Uses instance variables to get the necessary
736
  * speaker, and raised hand icons. Uses instance variables to get the necessary
775
         <div>
750
         <div>
776
             { this._showConnectionIndicator
751
             { this._showConnectionIndicator
777
                 ? <ConnectionIndicator
752
                 ? <ConnectionIndicator
753
+                    connectionStatus = { this._connectionStatus }
778
                     iconSize = { iconSize }
754
                     iconSize = { iconSize }
779
                     isLocalVideo = { this.isLocal }
755
                     isLocalVideo = { this.isLocal }
780
                     onHover = { this._onPopoverHover }
756
                     onHover = { this._onPopoverHover }
781
                     showMoreLink = { this.isLocal }
757
                     showMoreLink = { this.isLocal }
782
-                    stats = { this._cachedConnectionStats } />
758
+                    userID = { this.id } />
783
                 : null }
759
                 : null }
784
             { this._showRaisedHand
760
             { this._showRaisedHand
785
                 ? <RaisedHandIndicator iconSize = { iconSize } /> : null }
761
                 ? <RaisedHandIndicator iconSize = { iconSize } /> : null }

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

211
         if (largeVideo && !largeVideo.id) {
211
         if (largeVideo && !largeVideo.id) {
212
             this.updateLargeVideo(APP.conference.getMyUserId(), true);
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
         }
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
      * Hides the connection indicator
775
      * Hides the connection indicator
825
      * @param id
776
      * @param id

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

5
 import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
5
 import { JitsiParticipantConnectionStatus } from '../../base/lib-jitsi-meet';
6
 import { ConnectionStatsTable } from '../../connection-stats';
6
 import { ConnectionStatsTable } from '../../connection-stats';
7
 
7
 
8
+import statsEmitter from '../statsEmitter';
9
+
8
 declare var $: Object;
10
 declare var $: Object;
9
 declare var interfaceConfig: Object;
11
 declare var interfaceConfig: Object;
10
 
12
 
57
      * @static
59
      * @static
58
      */
60
      */
59
     static propTypes = {
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
          * Whether or not the displays stats are for local video.
71
          * Whether or not the displays stats are for local video.
62
          */
72
          */
74
         showMoreLink: React.PropTypes.bool,
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
              *
121
              *
122
              * @type {boolean}
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
         // Bind event handlers so they are only bound once for every instance.
135
         // Bind event handlers so they are only bound once for every instance.
136
+        this._onStatsUpdated = this._onStatsUpdated.bind(this);
128
         this._onToggleShowMore = this._onToggleShowMore.bind(this);
137
         this._onToggleShowMore = this._onToggleShowMore.bind(this);
129
         this._setRootElement = this._setRootElement.bind(this);
138
         this._setRootElement = this._setRootElement.bind(this);
130
     }
139
     }
136
      * returns {void}
145
      * returns {void}
137
      */
146
      */
138
     componentDidMount() {
147
     componentDidMount() {
148
+        statsEmitter.subscribeToClientStats(
149
+            this.props.userID, this._onStatsUpdated);
150
+
139
         this.popover = new JitsiPopover($(this._rootElement), {
151
         this.popover = new JitsiPopover($(this._rootElement), {
140
             content: this._renderStatisticsTable(),
152
             content: this._renderStatisticsTable(),
141
             skin: 'black',
153
             skin: 'black',
153
      * @inheritdoc
165
      * @inheritdoc
154
      * returns {void}
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
         this.popover.updateContent(this._renderStatisticsTable());
176
         this.popover.updateContent(this._renderStatisticsTable());
158
     }
177
     }
159
 
178
 
164
      * returns {void}
183
      * returns {void}
165
      */
184
      */
166
     componentWillUnmount() {
185
     componentWillUnmount() {
186
+        statsEmitter.unsubscribeToClientStats(
187
+            this.props.userID, this._onStatsUpdated);
188
+
167
         this.popover.forceHide();
189
         this.popover.forceHide();
168
         this.popover.remove();
190
         this.popover.remove();
169
     }
191
     }
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
      * Callback to invoke when the show more link in the popover content is
236
      * Callback to invoke when the show more link in the popover content is
191
      * clicked. Sets the state which will determine if the popover should show
237
      * clicked. Sets the state which will determine if the popover should show
204
      * @returns {ReactElement}
250
      * @returns {ReactElement}
205
      */
251
      */
206
     _renderIcon() {
252
     _renderIcon() {
207
-        switch (this.props.stats.connectionStatus) {
253
+        switch (this.props.connectionStatus) {
208
         case JitsiParticipantConnectionStatus.INTERRUPTED:
254
         case JitsiParticipantConnectionStatus.INTERRUPTED:
209
             return (
255
             return (
210
                 <span className = 'connection_lost'>
256
                 <span className = 'connection_lost'>
218
                 </span>
264
                 </span>
219
             );
265
             );
220
         default: {
266
         default: {
221
-            const { percent } = this.props.stats;
267
+            const { percent } = this.state.stats;
222
             const width = QUALITY_TO_WIDTH.find(x => percent >= x.percent);
268
             const width = QUALITY_TO_WIDTH.find(x => percent >= x.percent);
223
             const iconWidth = width && width.width
269
             const iconWidth = width && width.width
224
                 ? { width: width && width.width } : {};
270
                 ? { width: width && width.width } : {};
253
             packetLoss,
299
             packetLoss,
254
             resolution,
300
             resolution,
255
             transport
301
             transport
256
-        } = this.props.stats;
302
+        } = this.state.stats;
257
 
303
 
258
         return (
304
         return (
259
             <ConnectionStatsTable
305
             <ConnectionStatsTable

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

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

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

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