Browse Source

feat: Measures end-to-end RTT with ping over the data channel. (#795)

* feat: Adds the user's region to presence. Fixes typos.

* feat: Measures end-to-end RTT with ping over the data channel.
dev1
bgrozev 6 years ago
parent
commit
e097a1189e
No account linked to committer's email address

+ 36
- 0
JitsiConference.js View File

@@ -15,6 +15,7 @@ import {
15 15
 import AvgRTPStatsReporter from './modules/statistics/AvgRTPStatsReporter';
16 16
 import ComponentsVersions from './modules/version/ComponentsVersions';
17 17
 import ConnectionQuality from './modules/connectivity/ConnectionQuality';
18
+import E2ePing from './modules/e2eping/e2eping';
18 19
 import { getLogger } from 'jitsi-meet-logger';
19 20
 import GlobalOnErrorHandler from './modules/util/GlobalOnErrorHandler';
20 21
 import EventEmitter from 'events';
@@ -244,6 +245,33 @@ JitsiConference.prototype._init = function(options = {}) {
244 245
 
245 246
     this.room.updateDeviceAvailability(RTC.getDeviceAvailability());
246 247
 
248
+    this.e2eping = new E2ePing(
249
+        this.eventEmitter,
250
+        config,
251
+        (message, to) => {
252
+            try {
253
+                this.sendMessage(
254
+                    message, to, true /* sendThroughVideobridge */);
255
+            } catch (error) {
256
+                logger.warn('Failed to send a ping request or response.');
257
+            }
258
+        });
259
+    this.on(
260
+        JitsiConferenceEvents.USER_JOINED,
261
+        (id, participant) => this.e2eping.participantJoined(participant));
262
+    this.on(
263
+        JitsiConferenceEvents.USER_LEFT,
264
+        (id, participant) => this.e2eping.participantLeft(participant));
265
+    this.on(
266
+        JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
267
+        (participant, payload) => {
268
+            this.e2eping.messageReceived(participant, payload);
269
+        });
270
+    this.on(
271
+        JitsiConferenceEvents.DATA_CHANNEL_OPENED,
272
+        this.e2eping.dataChannelOpened);
273
+
274
+
247 275
     if (!this.rtc) {
248 276
         this.rtc = new RTC(this, options);
249 277
         this.eventManager.setupRTCListeners();
@@ -320,6 +348,11 @@ JitsiConference.prototype._init = function(options = {}) {
320 348
 
321 349
     // creates dominant speaker detection that works only in p2p mode
322 350
     this.p2pDominantSpeakerDetection = new P2PDominantSpeakerDetection(this);
351
+
352
+    if (config && config.deploymentInfo && config.deploymentInfo.userRegion) {
353
+        this.setLocalParticipantProperty(
354
+            'region', config.deploymentInfo.userRegion);
355
+    }
323 356
 };
324 357
 
325 358
 /**
@@ -621,7 +654,10 @@ JitsiConference.prototype.sendCommand = function(name, values) {
621 654
     if (this.room) {
622 655
         this.room.addToPresence(name, values);
623 656
         this.room.sendPresence();
657
+    } else {
658
+        logger.warn('Not sending a command, room not initialized.');
624 659
     }
660
+
625 661
 };
626 662
 
627 663
 /**

+ 2
- 2
JitsiConferenceEvents.js View File

@@ -266,7 +266,7 @@ export const TRANSCRIPTION_STATUS_CHANGED
266 266
 
267 267
 
268 268
 /**
269
- * A new user joinned the conference.
269
+ * A new user joined the conference.
270 270
  */
271 271
 export const USER_JOINED = 'conference.userJoined';
272 272
 
@@ -286,6 +286,6 @@ export const USER_ROLE_CHANGED = 'conference.roleChanged';
286 286
 export const USER_STATUS_CHANGED = 'conference.statusChanged';
287 287
 
288 288
 /**
289
- * Event indicates that the bot participant type cahnged.
289
+ * Event indicates that the bot participant type changed.
290 290
  */
291 291
 export const BOT_TYPE_CHANGED = 'conference.bot_type_changed';

+ 3
- 1
JitsiMeetJS.js View File

@@ -4,6 +4,7 @@ import { createGetUserMediaEvent } from './service/statistics/AnalyticsEvents';
4 4
 import AuthUtil from './modules/util/AuthUtil';
5 5
 import * as ConnectionQualityEvents
6 6
     from './service/connectivity/ConnectionQualityEvents';
7
+import * as E2ePingEvents from './service/e2eping/E2ePingEvents';
7 8
 import GlobalOnErrorHandler from './modules/util/GlobalOnErrorHandler';
8 9
 import * as JitsiConferenceErrors from './JitsiConferenceErrors';
9 10
 import * as JitsiConferenceEvents from './JitsiConferenceEvents';
@@ -139,7 +140,8 @@ export default _mergeNamespaceAndModule({
139 140
         connection: JitsiConnectionEvents,
140 141
         track: JitsiTrackEvents,
141 142
         mediaDevices: JitsiMediaDevicesEvents,
142
-        connectionQuality: ConnectionQualityEvents
143
+        connectionQuality: ConnectionQualityEvents,
144
+        e2eping: E2ePingEvents
143 145
     },
144 146
     errors: {
145 147
         conference: JitsiConferenceErrors,

+ 334
- 0
modules/e2eping/e2eping.js View File

@@ -0,0 +1,334 @@
1
+/* global __filename */
2
+import { getLogger } from 'jitsi-meet-logger';
3
+import { createE2eRttEvent } from '../../service/statistics/AnalyticsEvents';
4
+import * as E2ePingEvents
5
+    from '../../service/e2eping/E2ePingEvents';
6
+import Statistics from '../statistics/statistics';
7
+
8
+const logger = getLogger(__filename);
9
+
10
+/**
11
+ * The 'type' of a message which designates an e2e ping request.
12
+ * @type {string}
13
+ */
14
+const E2E_PING_REQUEST = 'e2e-ping-request';
15
+
16
+/**
17
+ * The 'type' of a message which designates an e2e ping response.
18
+ * @type {string}
19
+ */
20
+const E2E_PING_RESPONSE = 'e2e-ping-response';
21
+
22
+/**
23
+ * Saves e2e ping related state for a single JitsiParticipant.
24
+ */
25
+class ParticipantWrapper {
26
+    /**
27
+     * Creates a ParticipantWrapper
28
+     * @param {JitsiParticipant} participant - The remote participant that this
29
+     * object wraps.
30
+     * @param {E2ePing} e2eping
31
+     */
32
+    constructor(participant, e2eping) {
33
+        // The JitsiParticipant
34
+        this.participant = participant;
35
+
36
+        // The E2ePing
37
+        this.e2eping = e2eping;
38
+
39
+        // Caches the ID
40
+        this.id = participant.getId();
41
+
42
+        // Recently sent requests
43
+        this.requests = {};
44
+
45
+        // The ID of the last sent request. We just increment it for each new
46
+        // request. Start at 1 so we can consider only thruthy values valid.
47
+        this.lastRequestId = 1;
48
+
49
+        this.clearIntervals = this.clearIntervals.bind(this);
50
+        this.sendRequest = this.sendRequest.bind(this);
51
+        this.handleResponse = this.handleResponse.bind(this);
52
+        this.maybeSendAnalytics = this.maybeSendAnalytics.bind(this);
53
+        this.sendAnalytics = this.sendAnalytics.bind(this);
54
+
55
+        // If the data channel was already open (this is likely a participant
56
+        // joining an existing conference) send a request immediately.
57
+        if (e2eping.isDataChannelOpen) {
58
+            this.sendRequest();
59
+        }
60
+
61
+        this.pingInterval = window.setInterval(
62
+            this.sendRequest, e2eping.pingIntervalMs);
63
+        this.analyticsInterval = window.setTimeout(
64
+            this.maybeSendAnalytics, this.e2eping.analyticsIntervalMs);
65
+    }
66
+
67
+    /**
68
+     * Clears the interval which sends pings.
69
+     * @type {*}
70
+     */
71
+    clearIntervals() {
72
+        if (this.pingInterval) {
73
+            window.clearInterval(this.pingInterval);
74
+        }
75
+        if (this.analyticsInterval) {
76
+            window.clearInterval(this.analyticsInterval);
77
+        }
78
+    }
79
+
80
+    /**
81
+     * Sends the next ping request.
82
+     * @type {*}
83
+     */
84
+    sendRequest() {
85
+        const requestId = this.lastRequestId++;
86
+        const requestMessage = {
87
+            type: E2E_PING_REQUEST,
88
+            id: requestId
89
+        };
90
+
91
+        this.e2eping.sendMessage(requestMessage, this.id);
92
+        this.requests[requestId] = {
93
+            id: requestId,
94
+            timeSent: window.performance.now()
95
+        };
96
+    }
97
+
98
+    /**
99
+     * Handles a response from this participant.
100
+     * @type {*}
101
+     */
102
+    handleResponse(response) {
103
+        const request = this.requests[response.id];
104
+
105
+        if (request) {
106
+            request.rtt = window.performance.now() - request.timeSent;
107
+            this.e2eping.eventEmitter.emit(
108
+                E2ePingEvents.E2E_RTT_CHANGED,
109
+                this.participant,
110
+                request.rtt);
111
+        }
112
+
113
+        this.maybeSendAnalytics();
114
+    }
115
+
116
+    /**
117
+     * Goes over the requests, clearing ones which we don't need anymore, and
118
+     * if it finds at least one request with a valid RTT in the last
119
+     * 'analyticsIntervalMs' then sends an analytics event.
120
+     * @type {*}
121
+     */
122
+    maybeSendAnalytics() {
123
+        const now = window.performance.now();
124
+
125
+        // The RTT we'll report is the minimum RTT measured in the last
126
+        // analyticsInterval
127
+        let rtt = Infinity;
128
+        let request, requestId;
129
+
130
+        // It's time to send analytics. Clean up all requests and find the
131
+        for (requestId in this.requests) {
132
+            if (this.requests.hasOwnProperty(requestId)) {
133
+                request = this.requests[requestId];
134
+
135
+                if (request.timeSent < now - this.e2eping.analyticsIntervalMs) {
136
+                    // An old request. We don't care about it anymore.
137
+                    delete this.requests[requestId];
138
+                } else if (request.rtt) {
139
+                    rtt = Math.min(rtt, request.rtt);
140
+                }
141
+            }
142
+        }
143
+
144
+        if (rtt < Infinity) {
145
+            this.sendAnalytics(rtt);
146
+        }
147
+    }
148
+
149
+    /**
150
+     * Sends an analytics event for this participant with the given RTT.
151
+     * @type {*}
152
+     */
153
+    sendAnalytics(rtt) {
154
+        Statistics.sendAnalytics(createE2eRttEvent(
155
+            this.id,
156
+            this.participant.getProperty('region'),
157
+            rtt));
158
+    }
159
+}
160
+
161
+/**
162
+ * Implements end-to-end ping (from one conference participant to another) via
163
+ * the jitsi-videobridge channel (either WebRTC data channel or web socket).
164
+ *
165
+ * TODO: use a broadcast message instead of individual pings to each remote
166
+ * participant.
167
+ *
168
+ * This class:
169
+ * 1. Sends periodic ping requests to all other participants in the
170
+ * conference.
171
+ * 2. Responds to ping requests from other participants.
172
+ * 3. Fires events with the end-to-end RTT to each participant whenever a
173
+ * response is received.
174
+ * 4. Fires analytics events with the end-to-end RTT periodically.
175
+ */
176
+export default class E2ePing {
177
+    /**
178
+     * @param {EventEmitter} eventEmitter - The object to use to emit events.
179
+     * @param {Function} sendMessage - The function to use to send a message.
180
+     * @param {Object} options
181
+     */
182
+    constructor(eventEmitter, options, sendMessage) {
183
+        this.eventEmitter = eventEmitter;
184
+        this.sendMessage = sendMessage;
185
+
186
+        // The interval at which pings will be sent (<= 0 disables sending).
187
+        this.pingIntervalMs = 10000;
188
+
189
+        // The interval at which analytics events will be sent.
190
+        this.analyticsIntervalMs = 60000;
191
+
192
+        // Maps a participant ID to its ParticipantWrapper
193
+        this.participants = {};
194
+
195
+        // Whether the WebRTC channel has been opened or not.
196
+        this.isDataChannelOpen = false;
197
+
198
+        if (options && options.e2eping) {
199
+            if (typeof options.e2eping.pingInterval === 'number') {
200
+                this.pingIntervalMs = options.e2eping.pingInterval;
201
+            }
202
+            if (typeof options.e2eping.analyticsInterval === 'number') {
203
+                this.analyticsIntervalMs = options.e2eping.analyticsInterval;
204
+            }
205
+
206
+            // We want to report at most once a ping interval.
207
+            if (this.analyticsIntervalMs > 0 && this.analyticsIntervalMs
208
+                < this.pingIntervalMs) {
209
+                this.analyticsIntervalMs = this.pingIntervalMs;
210
+            }
211
+        }
212
+        logger.info(
213
+            `Initializing e2e ping; pingInterval=${
214
+                this.pingIntervalMs}, analyticsInterval=${
215
+                this.analyticsIntervalMs}.`);
216
+    }
217
+
218
+    /**
219
+     * Notifies this instance that the communications channel has been opened
220
+     * and it can now send messages via sendMessage.
221
+     */
222
+    dataChannelOpened() {
223
+        this.isDataChannelOpen = true;
224
+
225
+        // We don't want to wait the whole interval before sending the first
226
+        // request, but we can't send it immediately after the participant joins
227
+        // either, because our data channel might not have initialized.
228
+        // So once the data channel initializes, send requests to everyone.
229
+        // Wait an additional 200ms to give a chance to the remote side (if it
230
+        // also just connected as is the case for the first 2 participants in a
231
+        // conference) to open its data channel.
232
+        for (const id in this.participants) {
233
+            if (this.participants.hasOwnProperty(id)) {
234
+                const participantWrapper = this.participants[id];
235
+
236
+                window.setTimeout(participantWrapper.sendRequest, 200);
237
+            }
238
+        }
239
+    }
240
+
241
+    /**
242
+     * Handles a message that was received.
243
+     *
244
+     * @param participant - The message sender.
245
+     * @param payload - The payload of the message.
246
+     */
247
+    messageReceived(participant, payload) {
248
+        // Listen to E2E PING requests and responses from other participants
249
+        // in the conference.
250
+        if (payload.type === E2E_PING_REQUEST) {
251
+            this.handleRequest(participant.getId(), payload);
252
+        } else if (payload.type === E2E_PING_RESPONSE) {
253
+            this.handleResponse(participant.getId(), payload);
254
+        }
255
+    }
256
+
257
+    /**
258
+     * Handles a participant joining the conference. Starts to send ping
259
+     * requests to the participant.
260
+     *
261
+     * @param {JitsiParticipant} participant - The participant that joined.
262
+     */
263
+    participantJoined(participant) {
264
+        const id = participant.getId();
265
+
266
+        if (this.pingIntervalMs <= 0) {
267
+            return;
268
+        }
269
+
270
+        if (this.participants[id]) {
271
+            logger.info(
272
+                `Participant wrapper already exists for ${id}. Clearing.`);
273
+            this.participants[id].clearIntervals();
274
+            delete this.participants[id];
275
+        }
276
+
277
+        this.participants[id] = new ParticipantWrapper(participant, this);
278
+    }
279
+
280
+    /**
281
+     * Handles a participant leaving the conference. Stops sending requests.
282
+     *
283
+     * @param {JitsiParticipant} participant - The participant that left.
284
+     */
285
+    participantLeft(participant) {
286
+        const id = participant.getId();
287
+
288
+        if (this.pingIntervalMs <= 0) {
289
+            return;
290
+        }
291
+
292
+        if (this.participants[id]) {
293
+            this.participants[id].clearIntervals();
294
+            delete this.participants[id];
295
+        }
296
+    }
297
+
298
+    /**
299
+     * Handles a ping request coming from another participant.
300
+     *
301
+     * @param {string} participantId - The ID of the participant who sent the
302
+     * request.
303
+     * @param {Object} request - The request.
304
+     */
305
+    handleRequest(participantId, request) {
306
+        // If it's a valid request, just send a response.
307
+        if (request && request.id) {
308
+            const response = {
309
+                type: E2E_PING_RESPONSE,
310
+                id: request.id
311
+            };
312
+
313
+            this.sendMessage(response, participantId);
314
+        } else {
315
+            logger.info(
316
+                `Received an invalid e2e ping request from ${participantId}.`);
317
+        }
318
+    }
319
+
320
+    /**
321
+     * Handles a ping response coming from another participant
322
+     * @param {string} participantId - The ID of the participant who sent the
323
+     * response.
324
+     * @param {Object} response - The response.
325
+     */
326
+    handleResponse(participantId, response) {
327
+        const participantWrapper = this.participants[participantId];
328
+
329
+        if (participantWrapper) {
330
+            participantWrapper.handleResponse(response);
331
+        }
332
+    }
333
+}
334
+

+ 2
- 1
modules/xmpp/ChatRoom.js View File

@@ -570,7 +570,8 @@ export default class ChatRoom extends Listenable {
570 570
                 // seems there is some period of time in prosody that the
571 571
                 // configuration form is received but not applied. And if any
572 572
                 // participant joins during that period of time the first
573
-                // presence from the focus won't conain <item jid="focus..." />.
573
+                // presence from the focus won't contain
574
+                // <item jid="focus..." />.
574 575
                 memberOfThis.isFocus = true;
575 576
                 this._initFocus(from, jid);
576 577
             }

+ 4
- 0
service/e2eping/E2ePingEvents.js View File

@@ -0,0 +1,4 @@
1
+/**
2
+ * Indicates that the end-to-end round-trip-time for a participant has changed.
3
+ */
4
+export const E2E_RTT_CHANGED = 'e2eping.e2e_rtt_changed';

+ 22
- 1
service/statistics/AnalyticsEvents.js View File

@@ -17,7 +17,7 @@
17 17
  * constant. Otherwise use a factory function.
18 18
  *
19 19
  * Note that the AnalyticsAdapter uses the events passed to its functions for
20
- * its own purposes, and might modify them. Because of this factory functions
20
+ * its own purposes, and might modify them. Because of this, factory functions
21 21
  * should create new objects.
22 22
  *
23 23
  */
@@ -264,6 +264,27 @@ export const createConnectionStageReachedEvent = function(stage, attributes) {
264 264
     };
265 265
 };
266 266
 
267
+/**
268
+ * Creates an operational event for the end-to-end round trip time to a
269
+ * specific remote participant.
270
+ * @param participantId the ID of the remote participant.
271
+ * @param region the region of the remote participant
272
+ * @param rtt the rtt
273
+ */
274
+export const createE2eRttEvent = function(participantId, region, rtt) {
275
+    const attributes = {
276
+        'participant_id': participantId,
277
+        region,
278
+        rtt
279
+    };
280
+
281
+    return {
282
+        attributes,
283
+        name: 'e2e_rtt',
284
+        type: TYPE_OPERATIONAL
285
+    };
286
+};
287
+
267 288
 /**
268 289
  * Creates an event which indicates that the focus has left the MUC.
269 290
  */

Loading…
Cancel
Save