Просмотр исходного кода

feat(ParticipantConnectionStatus): take advantage of RTC mute/unmute

'onmute'/'onunmute' event of MediaStreamTrack can be used to detect that
the remote user is having connectivity issues, because no video data is
received. We check if those are in sync with the signalling and if not
trigger connection interrupted updates with small delay.
dev1
paweldomas 9 лет назад
Родитель
Сommit
436c4e87c4
2 измененных файлов: 236 добавлений и 5 удалений
  1. 10
    0
      modules/RTC/RTCBrowserType.js
  2. 226
    5
      modules/connectivity/ParticipantConnectionStatus.js

+ 10
- 0
modules/RTC/RTCBrowserType.js Просмотреть файл

@@ -106,6 +106,16 @@ var RTCBrowserType = {
106 106
         return RTCBrowserType.isIExplorer() || RTCBrowserType.isSafari();
107 107
     },
108 108
 
109
+    /**
110
+     * Checks if the current browser triggers 'onmute'/'onunmute' events when
111
+     * user's connection is interrupted and the video stops playback.
112
+     * @returns {*|boolean} 'true' if the event is supported or 'false'
113
+     * otherwise.
114
+     */
115
+    isVideoMuteOnConnInterruptedSupported: function () {
116
+        return RTCBrowserType.isChrome();
117
+    },
118
+
109 119
     /**
110 120
      * Returns Firefox version.
111 121
      * @returns {number|null}

+ 226
- 5
modules/connectivity/ParticipantConnectionStatus.js Просмотреть файл

@@ -1,7 +1,20 @@
1 1
 /* global __filename, module, require */
2 2
 var logger = require("jitsi-meet-logger").getLogger(__filename);
3
+var MediaType = require("../../service/RTC/MediaType");
4
+var RTCBrowserType = require("../RTC/RTCBrowserType");
3 5
 var RTCEvents = require("../../service/RTC/RTCEvents");
6
+
4 7
 import * as JitsiConferenceEvents from "../../JitsiConferenceEvents";
8
+import * as JitsiTrackEvents from "../../JitsiTrackEvents";
9
+
10
+/**
11
+ * How long we're going to wait after the RTC video track muted event for
12
+ * the corresponding signalling mute event, before the connection interrupted
13
+ * is fired.
14
+ *
15
+ * @type {number} amount of time in milliseconds
16
+ */
17
+const RTC_MUTE_TIMEOUT = 1000;
5 18
 
6 19
 /**
7 20
  * Class is responsible for emitting
@@ -14,6 +27,13 @@ import * as JitsiConferenceEvents from "../../JitsiConferenceEvents";
14 27
 function ParticipantConnectionStatus(rtc, conference) {
15 28
     this.rtc = rtc;
16 29
     this.conference = conference;
30
+    /**
31
+     * A map of the "endpoint ID"(which corresponds to the resource part of MUC
32
+     * JID(nickname)) to the timeout callback IDs scheduled using
33
+     * window.setTimeout.
34
+     * @type {Object.<string, number>}
35
+     */
36
+    this.trackTimers = {};
17 37
 }
18 38
 
19 39
 /**
@@ -28,6 +48,33 @@ ParticipantConnectionStatus.prototype.init = function() {
28 48
     this.rtc.addListener(
29 49
         RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
30 50
         this._onEndpointConnStatusChanged);
51
+
52
+    // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
53
+    // events for video type tracks when they stop receiving data which is
54
+    // often a sign that remote user is having connectivity issues
55
+    if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
56
+
57
+        this._onTrackRtcMuted = this.onTrackRtcMuted.bind(this);
58
+        this.rtc.addListener(
59
+            RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);
60
+
61
+        this._onTrackRtcUnmuted = this.onTrackRtcUnmuted.bind(this);
62
+        this.rtc.addListener(
63
+            RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);
64
+
65
+        // Track added/removed listeners are used to bind "mute"/"unmute"
66
+        // event handlers
67
+        this._onRemoteTrackAdded = this.onRemoteTrackAdded.bind(this);
68
+        this.conference.on(
69
+            JitsiConferenceEvents.TRACK_ADDED, this._onRemoteTrackAdded);
70
+        this._onRemoteTrackRemoved = this.onRemoteTrackRemoved.bind(this);
71
+        this.conference.on(
72
+            JitsiConferenceEvents.TRACK_REMOVED, this._onRemoteTrackRemoved);
73
+
74
+        // Listened which will be bound to JitsiRemoteTrack to listen for
75
+        // signalling mute/unmute events.
76
+        this._onSignallingMuteChanged = this.onSignallingMuteChanged.bind(this);
77
+    }
31 78
 };
32 79
 
33 80
 /**
@@ -39,6 +86,38 @@ ParticipantConnectionStatus.prototype.dispose = function () {
39 86
     this.rtc.removeListener(
40 87
         RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
41 88
         this._onEndpointConnStatusChanged);
89
+
90
+    if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
91
+        this.rtc.removeListener(
92
+            RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);
93
+        this.rtc.removeListener(
94
+            RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);
95
+        this.conference.off(
96
+            JitsiConferenceEvents.TRACK_ADDED, this._onRemoteTrackAdded);
97
+        this.conference.off(
98
+            JitsiConferenceEvents.TRACK_REMOVED, this._onRemoteTrackRemoved);
99
+    }
100
+
101
+    Object.keys(this.trackTimers).forEach(function (participantId) {
102
+        this.clearTimeout(participantId);
103
+    }.bind(this));
104
+};
105
+
106
+/**
107
+ * Checks whether given <tt>JitsiParticipant</tt> has any muted video
108
+ * <tt>MediaStreamTrack</tt>s.
109
+ *
110
+ * @param {JitsiParticipant} participant to be checked for muted video tracks
111
+ *
112
+ * @return {boolean} <tt>true</tt> if given <tt>participant</tt> contains any
113
+ * video <tt>MediaStreamTrack</tt>s muted according to their 'muted' field.
114
+ */
115
+var hasRtcMutedVideoTrack = function (participant) {
116
+    return participant.getTracks().some(function(jitsiTrack) {
117
+        var rtcTrack = jitsiTrack.getTrack();
118
+        return jitsiTrack.getType() === MediaType.VIDEO
119
+            && rtcTrack && rtcTrack.muted === true;
120
+    });
42 121
 };
43 122
 
44 123
 /**
@@ -46,16 +125,30 @@ ParticipantConnectionStatus.prototype.dispose = function () {
46 125
  * notification over the data channel from the bridge about endpoint's
47 126
  * connection status update.
48 127
  * @param endpointId {string} the endpoint ID(MUC nickname/resource JID)
49
- * @param status {boolean} true if the connection is OK or false otherwise
128
+ * @param isActive {boolean} true if the connection is OK or false otherwise
50 129
  */
51 130
 ParticipantConnectionStatus.prototype.onEndpointConnStatusChanged
52
-= function(endpointId, status) {
131
+= function(endpointId, isActive) {
132
+
53 133
     logger.debug(
54
-        'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED(' + Date.now() +'): '
55
-            + endpointId +": " + status);
134
+        'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED('
135
+            + Date.now() +'): ' + endpointId + ": " + isActive);
136
+
56 137
     // Filter out events for the local JID for now
57 138
     if (endpointId !== this.conference.myUserId()) {
58
-        this._changeConnectionStatus(endpointId, status);
139
+        var participant = this.conference.getParticipantById(endpointId);
140
+        // Delay the 'active' event until the video track gets RTC unmuted event
141
+        if (isActive
142
+                && RTCBrowserType.isVideoMuteOnConnInterruptedSupported()
143
+                && participant
144
+                && hasRtcMutedVideoTrack(participant)
145
+                && !participant.isVideoMuted()) {
146
+            logger.debug(
147
+                "Ignoring RTCEvents.ENDPOINT_CONN_STATUS_CHANGED -"
148
+                    + " will wait for unmute event");
149
+        } else {
150
+            this._changeConnectionStatus(endpointId, isActive);
151
+        }
59 152
     }
60 153
 };
61 154
 
@@ -82,4 +175,132 @@ ParticipantConnectionStatus.prototype._changeConnectionStatus
82 175
     }
83 176
 };
84 177
 
178
+/**
179
+ * Reset the postponed "connection interrupted" event which was previously
180
+ * scheduled as a timeout on RTC 'onmute' event.
181
+ *
182
+ * @param participantId the participant for which the "connection interrupted"
183
+ * timeout was scheduled
184
+ */
185
+ParticipantConnectionStatus.prototype.clearTimeout = function (participantId) {
186
+    if (this.trackTimers[participantId]) {
187
+        window.clearTimeout(this.trackTimers[participantId]);
188
+        this.trackTimers[participantId] = null;
189
+    }
190
+};
191
+
192
+/**
193
+ * Bind signalling mute event listeners for video {JitsiRemoteTrack} when
194
+ * a new one is added to the conference.
195
+ *
196
+ * @param {JitsiTrack} remoteTrack the {JitsiTrack} which is being added to
197
+ * the conference.
198
+ */
199
+ParticipantConnectionStatus.prototype.onRemoteTrackAdded
200
+= function(remoteTrack) {
201
+    if (!remoteTrack.isLocal() && remoteTrack.getType() === MediaType.VIDEO) {
202
+
203
+        logger.debug(
204
+            "Detector on remote track added: ", remoteTrack.getParticipantId());
205
+
206
+        remoteTrack.on(
207
+            JitsiTrackEvents.TRACK_MUTE_CHANGED,
208
+            this._onSignallingMuteChanged);
209
+    }
210
+};
211
+
212
+/**
213
+ * Removes all event listeners bound to the remote video track and clears any
214
+ * related timeouts.
215
+ *
216
+ * @param {JitsiRemoteTrack} remoteTrack the remote track which is being removed
217
+ * from the conference.
218
+ */
219
+ParticipantConnectionStatus.prototype.onRemoteTrackRemoved
220
+= function(remoteTrack) {
221
+    if (!remoteTrack.isLocal() && remoteTrack.getType() === MediaType.VIDEO) {
222
+        logger.debug(
223
+            "Detector on remote track removed: ",
224
+            remoteTrack.getParticipantId());
225
+        remoteTrack.off(
226
+            JitsiTrackEvents.TRACK_MUTE_CHANGED,
227
+            this._onSignallingMuteChanged);
228
+        this.clearTimeout(remoteTrack.getParticipantId());
229
+    }
230
+};
231
+
232
+/**
233
+ * Handles RTC 'onmute' event for the video track.
234
+ *
235
+ * @param track {JitsiRemoteTrack} the video track for which 'onmute' event will
236
+ * be processed.
237
+ */
238
+ParticipantConnectionStatus.prototype.onTrackRtcMuted = function(track) {
239
+    var participantId = track.getParticipantId();
240
+    var participant = this.conference.getParticipantById(participantId);
241
+    logger.debug("Detector track RTC muted: ", participantId);
242
+    if (!participant) {
243
+        logger.error("No participant for id: " + participantId);
244
+        return;
245
+    }
246
+    if (!participant.isVideoMuted()) {
247
+        // If the user is not muted according to the signalling we'll give it
248
+        // some time, before the connection interrupted event is triggered.
249
+        this.trackTimers[participantId] = window.setTimeout(function () {
250
+            if (!track.isMuted() && participant.isConnectionActive()) {
251
+                logger.info(
252
+                    "Connection interrupted through the RTC mute: "
253
+                        + participantId, Date.now());
254
+                this._changeConnectionStatus(participantId, false);
255
+            }
256
+            this.clearTimeout(participantId);
257
+        }.bind(this), RTC_MUTE_TIMEOUT);
258
+    }
259
+};
260
+
261
+/**
262
+ * Handles RTC 'onunmute' event for the video track.
263
+ *
264
+ * @param track {JitsiRemoteTrack} the video track for which 'onunmute' event
265
+ * will be processed.
266
+ */
267
+ParticipantConnectionStatus.prototype.onTrackRtcUnmuted = function(track) {
268
+    logger.debug("Detector track RTC unmuted: ", track);
269
+    var participantId = track.getParticipantId();
270
+    if (!track.isMuted() &&
271
+        !this.conference.getParticipantById(participantId)
272
+            .isConnectionActive()) {
273
+        logger.info(
274
+            "Detector connection restored through the RTC unmute: "
275
+                + participantId, Date.now());
276
+        this._changeConnectionStatus(participantId, true);
277
+    }
278
+    this.clearTimeout(participantId);
279
+};
280
+
281
+/**
282
+ * Here the signalling "mute"/"unmute" events are processed.
283
+ *
284
+ * @param track {JitsiRemoteTrack} the remote video track for which
285
+ * the signalling mute/unmute event will be processed.
286
+ */
287
+ParticipantConnectionStatus.prototype.onSignallingMuteChanged
288
+= function (track) {
289
+    logger.debug(
290
+        "Detector on track signalling mute changed: ", track, track.isMuted());
291
+    var isMuted = track.isMuted();
292
+    var participantId = track.getParticipantId();
293
+    var participant = this.conference.getParticipantById(participantId);
294
+    if (!participant) {
295
+        logger.error("No participant for id: " + participantId);
296
+        return;
297
+    }
298
+    var isConnectionActive = participant.isConnectionActive();
299
+    if (isMuted && isConnectionActive && this.trackTimers[participantId]) {
300
+        logger.debug(
301
+            "Signalling got in sync - cancelling task for: " + participantId);
302
+        this.clearTimeout(participantId);
303
+    }
304
+};
305
+
85 306
 module.exports = ParticipantConnectionStatus;

Загрузка…
Отмена
Сохранить