Browse Source

fix(SignalingLayer) Update SSRC owners on leave. (#2184)

* fix(SignalingLayer) Update SSRC owners on leave.
Update the SSRC owners in the following cases:
1. When a remote endpoint leaves the call.
2. When a source-remove is received.
3. When a source is remapped (with ssrc-rewriting enabled).
Create the remote track even if presence is not yet received. The ssrc owner check prevents the client from creating a dummy track when the call switches over from p2p to jvb when the last remote endpoint leaves the call.

* ref(SignalingLayer) alpha sort methods.
Clean up unused methods, _findEndpointSourceInfoForMediaType is not used anymore.

* squash: Address review comments.
tags/v0.0.2
Jaya Allamsetty 1 year ago
parent
commit
4d6873c2f4
No account linked to committer's email address

+ 3
- 0
JitsiConference.js View File

@@ -1910,6 +1910,9 @@ JitsiConference.prototype.onMemberLeft = function(jid, reason) {
1910 1910
 
1911 1911
             remoteTracks && (tracksToBeRemoved = [ ...tracksToBeRemoved, ...remoteTracks ]);
1912 1912
 
1913
+            // Update the SSRC owners list.
1914
+            session._signalingLayer.updateSsrcOwnersOnLeave(id);
1915
+
1913 1916
             // Remove the ssrcs from the remote description and renegotiate.
1914 1917
             session.removeRemoteStreamsOnLeave(id);
1915 1918
         }

+ 17
- 13
modules/RTC/TraceablePeerConnection.js View File

@@ -958,7 +958,9 @@ TraceablePeerConnection.prototype._remoteTrackAdded = function(stream, track, tr
958 958
                 + 'track creation failed!'));
959 959
 
960 960
         return;
961
-    } else if (!ownerEndpointId) {
961
+    }
962
+
963
+    if (!ownerEndpointId) {
962 964
         GlobalOnErrorHandler.callErrorHandler(
963 965
             new Error(`No SSRC owner known for remote stream[ssrc=${trackSsrc},id=${streamId},type=${mediaType}]`
964 966
             + 'track creation failed!'));
@@ -969,16 +971,18 @@ TraceablePeerConnection.prototype._remoteTrackAdded = function(stream, track, tr
969 971
     const sourceName = this.signalingLayer.getTrackSourceName(trackSsrc);
970 972
     const peerMediaInfo = this.signalingLayer.getPeerMediaInfo(ownerEndpointId, mediaType, sourceName);
971 973
 
972
-    if (!peerMediaInfo) {
973
-        GlobalOnErrorHandler.callErrorHandler(
974
-            new Error(`${this}: no sourceInfo available for ${ownerEndpointId}:${sourceName} track creation failed!`));
974
+    // Assume default presence state for remote source. Presence can be received after source signaling. This shouldn't
975
+    // prevent the endpoint from creating a remote track for the source.
976
+    let muted = true;
977
+    let videoType = VideoType.CAMERA;
975 978
 
976
-        return;
979
+    if (peerMediaInfo) {
980
+        muted = peerMediaInfo.muted;
981
+        videoType = peerMediaInfo.videoType; // can be undefined
982
+    } else {
983
+        logger.info(`${this}: no source-info available for ${ownerEndpointId}:${sourceName}, assuming default state`);
977 984
     }
978 985
 
979
-    const muted = peerMediaInfo.muted;
980
-    const videoType = peerMediaInfo.videoType; // can be undefined
981
-
982 986
     this._createRemoteTrack(ownerEndpointId, stream, track, mediaType, videoType, trackSsrc, muted, sourceName);
983 987
 };
984 988
 
@@ -1082,9 +1086,8 @@ TraceablePeerConnection.prototype._remoteTrackRemoved = function(stream, track)
1082 1086
     const streamId = stream.id;
1083 1087
     const trackId = track?.id;
1084 1088
 
1089
+    // Ignore stream removed events for JVB "mixed" sources (used for RTCP termination).
1085 1090
     if (!RTC.isUserStreamById(streamId)) {
1086
-        logger.info(`${this} ignored remote 'stream removed' event for non-user stream[id=${streamId}]`);
1087
-
1088 1091
         return;
1089 1092
     }
1090 1093
 
@@ -1101,8 +1104,7 @@ TraceablePeerConnection.prototype._remoteTrackRemoved = function(stream, track)
1101 1104
     }
1102 1105
 
1103 1106
     const toBeRemoved = this.getRemoteTracks().find(
1104
-        remoteTrack => remoteTrack.getStreamId() === streamId
1105
-        && remoteTrack.getTrackId() === trackId);
1107
+        remoteTrack => remoteTrack.getStreamId() === streamId && remoteTrack.getTrackId() === trackId);
1106 1108
 
1107 1109
     if (!toBeRemoved) {
1108 1110
         GlobalOnErrorHandler.callErrorHandler(new Error(`${this} remote track removal failed - track not found`));
@@ -1110,7 +1112,6 @@ TraceablePeerConnection.prototype._remoteTrackRemoved = function(stream, track)
1110 1112
         return;
1111 1113
     }
1112 1114
 
1113
-    logger.info(`${this} remote track removed stream[id=${streamId},trackId=${trackId}]`);
1114 1115
     this._removeRemoteTrack(toBeRemoved);
1115 1116
 };
1116 1117
 
@@ -1141,6 +1142,9 @@ TraceablePeerConnection.prototype.removeRemoteTracks = function(owner) {
1141 1142
  * @returns {void}
1142 1143
  */
1143 1144
 TraceablePeerConnection.prototype._removeRemoteTrack = function(toBeRemoved) {
1145
+    logger.info(`${this} Removing remote track stream[id=${toBeRemoved.getStreamId()},`
1146
+        + `trackId=${toBeRemoved.getTrackId()}]`);
1147
+
1144 1148
     toBeRemoved.dispose();
1145 1149
     const participantId = toBeRemoved.getParticipantId();
1146 1150
     const userTracksByMediaType = this.remoteTracks.get(participantId);

+ 39
- 13
modules/proxyconnection/CustomSignalingLayer.js View File

@@ -2,10 +2,8 @@ import { getLogger } from '@jitsi/logger';
2 2
 
3 3
 import SignalingLayer from '../../service/RTC/SignalingLayer';
4 4
 
5
-
6 5
 const logger = getLogger(__filename);
7 6
 
8
-
9 7
 /**
10 8
  * Custom semi-mock implementation for the Proxy connection service.
11 9
  */
@@ -29,14 +27,6 @@ export default class CustomSignalingLayer extends SignalingLayer {
29 27
         this.chatRoom = null;
30 28
     }
31 29
 
32
-    /**
33
-     * Sets the <tt>ChatRoom</tt> instance used.
34
-     * @param {ChatRoom} room
35
-     */
36
-    setChatRoom(room) {
37
-        this.chatRoom = room;
38
-    }
39
-
40 30
     /**
41 31
      * @inheritDoc
42 32
      */
@@ -58,6 +48,34 @@ export default class CustomSignalingLayer extends SignalingLayer {
58 48
         return this.ssrcOwners.get(ssrc);
59 49
     }
60 50
 
51
+    /**
52
+     * @inheritDoc
53
+     */
54
+    getTrackSourceName(ssrc) { // eslint-disable-line no-unused-vars
55
+        return undefined;
56
+    }
57
+
58
+    /**
59
+     * @inheritDoc
60
+     */
61
+    removeSSRCOwners(ssrcList) {
62
+        if (!ssrcList?.length) {
63
+            return;
64
+        }
65
+
66
+        for (const ssrc of ssrcList) {
67
+            this.ssrcOwners.delete(ssrc);
68
+        }
69
+    }
70
+
71
+    /**
72
+     * Sets the <tt>ChatRoom</tt> instance used.
73
+     * @param {ChatRoom} room
74
+     */
75
+    setChatRoom(room) {
76
+        this.chatRoom = room;
77
+    }
78
+
61 79
     /**
62 80
      * @inheritDoc
63 81
      */
@@ -93,13 +111,21 @@ export default class CustomSignalingLayer extends SignalingLayer {
93 111
     /**
94 112
      * @inheritDoc
95 113
      */
96
-    getTrackSourceName(ssrc) { // eslint-disable-line no-unused-vars
97
-        return undefined;
114
+    setTrackSourceName(ssrc, sourceName) { // eslint-disable-line no-unused-vars
98 115
     }
99 116
 
100 117
     /**
101 118
      * @inheritDoc
102 119
      */
103
-    setTrackSourceName(ssrc, sourceName) { // eslint-disable-line no-unused-vars
120
+    updateSsrcOwnersOnLeave(id) {
121
+        const ssrcs = Array.from(this.ssrcOwners)
122
+            .filter(entry => entry[1] === id)
123
+            .map(entry => entry[0]);
124
+
125
+        if (!ssrcs?.length) {
126
+            return;
127
+        }
128
+
129
+        this.removeSSRCOwners(ssrcs);
104 130
     }
105 131
 }

+ 10
- 0
modules/xmpp/JingleSessionPC.js View File

@@ -1810,6 +1810,9 @@ export default class JingleSessionPC extends JingleSession {
1810 1810
                 if (track) {
1811 1811
                     logger.debug(`Existing SSRC ${s.ssrc}: new owner ${s.owner}. name=${s.source}`);
1812 1812
 
1813
+                    // Update the SSRC owner.
1814
+                    this._signalingLayer.setSSRCOwner(s.ssrc, s.owner);
1815
+
1813 1816
                     if (s.videoType === 'CAMERA') {
1814 1817
                         track._setVideoType('camera');
1815 1818
                     } else if (s.videoType === 'DESKTOP') {
@@ -2042,11 +2045,14 @@ export default class JingleSessionPC extends JingleSession {
2042 2045
         const remoteSdp = this.usesUnifiedPlan
2043 2046
             ? new SDP(this.peerconnection.peerconnection.remoteDescription.sdp)
2044 2047
             : new SDP(this.peerconnection.remoteDescription.sdp);
2048
+        let ssrcs;
2045 2049
 
2046 2050
         removeSsrcInfo.forEach((lines, idx) => {
2047 2051
             // eslint-disable-next-line no-param-reassign
2048 2052
             lines = lines.split('\r\n');
2049 2053
             lines.pop(); // remove empty last element;
2054
+            ssrcs = lines.map(line => Number(line.split('a=ssrc:')[1]?.split(' ')[0]));
2055
+
2050 2056
             if (this.usesUnifiedPlan) {
2051 2057
                 let mid;
2052 2058
 
@@ -2085,6 +2091,10 @@ export default class JingleSessionPC extends JingleSession {
2085 2091
                 });
2086 2092
             }
2087 2093
         });
2094
+
2095
+        // Update the ssrc owners list.
2096
+        ssrcs?.length && this._signalingLayer.removeSSRCOwners(ssrcs);
2097
+
2088 2098
         remoteSdp.raw = remoteSdp.session + remoteSdp.media.join('');
2089 2099
 
2090 2100
         return remoteSdp;

+ 95
- 76
modules/xmpp/SignalingLayerImpl.js View File

@@ -6,6 +6,7 @@ import * as SignalingEvents from '../../service/RTC/SignalingEvents';
6 6
 import SignalingLayer, { getMediaTypeFromSourceName } from '../../service/RTC/SignalingLayer';
7 7
 import { VideoType } from '../../service/RTC/VideoType';
8 8
 import { XMPPEvents } from '../../service/xmpp/XMPPEvents';
9
+import FeatureFlags from '../flags/FeatureFlags';
9 10
 
10 11
 import { filterNodeFromPresenceJSON } from './ChatRoom';
11 12
 
@@ -79,45 +80,6 @@ export default class SignalingLayerImpl extends SignalingLayer {
79 80
         return false;
80 81
     }
81 82
 
82
-    /**
83
-     * Check is given endpoint has advertised <SourceInfo/> in it's presence which means that the source name signaling
84
-     * is used by this endpoint.
85
-     *
86
-     * @param {EndpointId} endpointId
87
-     * @returns {boolean}
88
-     */
89
-    _doesEndpointSendNewSourceInfo(endpointId) {
90
-        const presence = this.chatRoom?.getLastPresence(endpointId);
91
-
92
-        return Boolean(presence && presence.find(node => node.tagName === SOURCE_INFO_PRESENCE_ELEMENT));
93
-    }
94
-
95
-    /**
96
-     * Sets the <tt>ChatRoom</tt> instance used and binds presence listeners.
97
-     * @param {ChatRoom} room
98
-     */
99
-    setChatRoom(room) {
100
-        const oldChatRoom = this.chatRoom;
101
-
102
-        this.chatRoom = room;
103
-        if (oldChatRoom) {
104
-            oldChatRoom.removePresenceListener(
105
-                'audiomuted', this._audioMuteHandler);
106
-            oldChatRoom.removePresenceListener(
107
-                'videomuted', this._videoMuteHandler);
108
-            oldChatRoom.removePresenceListener(
109
-                'videoType', this._videoTypeHandler);
110
-            this._sourceInfoHandler
111
-                && oldChatRoom.removePresenceListener(SOURCE_INFO_PRESENCE_ELEMENT, this._sourceInfoHandler);
112
-            this._memberLeftHandler
113
-                && oldChatRoom.removeEventListener(XMPPEvents.MUC_MEMBER_LEFT, this._memberLeftHandler);
114
-        }
115
-        if (room) {
116
-            this._bindChatRoomEventHandlers(room);
117
-            this._addLocalSourceInfoToPresence();
118
-        }
119
-    }
120
-
121 83
     /**
122 84
      * Binds event listeners to the chat room instance.
123 85
      * @param {ChatRoom} room
@@ -235,28 +197,31 @@ export default class SignalingLayerImpl extends SignalingLayer {
235 197
     }
236 198
 
237 199
     /**
238
-     * Finds the first source of given media type for the given endpoint.
239
-     * @param endpointId
240
-     * @param mediaType
241
-     * @returns {SourceInfo|null}
242
-     * @private
200
+     * Check is given endpoint has advertised <SourceInfo/> in it's presence which means that the source name signaling
201
+     * is used by this endpoint.
202
+     *
203
+     * @param {EndpointId} endpointId
204
+     * @returns {boolean}
243 205
      */
244
-    _findEndpointSourceInfoForMediaType(endpointId, mediaType) {
245
-        const remoteSourceState = this._remoteSourceState[endpointId];
246
-
247
-        if (!remoteSourceState) {
248
-            return null;
249
-        }
206
+    _doesEndpointSendNewSourceInfo(endpointId) {
207
+        const presence = this.chatRoom?.getLastPresence(endpointId);
250 208
 
251
-        for (const sourceInfo of Object.values(remoteSourceState)) {
252
-            const _mediaType = getMediaTypeFromSourceName(sourceInfo.sourceName);
209
+        return Boolean(presence && presence.find(node => node.tagName === SOURCE_INFO_PRESENCE_ELEMENT));
210
+    }
253 211
 
254
-            if (_mediaType === mediaType) {
255
-                return sourceInfo;
256
-            }
212
+    /**
213
+     * Logs a debug or error message to console depending on whether SSRC rewriting is enabled or not.
214
+     * Owner changes are permitted only when SSRC rewriting is enabled.
215
+     *
216
+     * @param {string} message - The message to be logged.
217
+     * @returns {void}
218
+     */
219
+    _logOwnerChangedMessage(message) {
220
+        if (FeatureFlags.isSsrcRewritingSupported()) {
221
+            logger.debug(message);
222
+        } else {
223
+            logger.error(message);
257 224
         }
258
-
259
-        return null;
260 225
     }
261 226
 
262 227
     /**
@@ -322,6 +287,52 @@ export default class SignalingLayerImpl extends SignalingLayer {
322 287
         return this.ssrcOwners.get(ssrc);
323 288
     }
324 289
 
290
+    /**
291
+     * @inheritDoc
292
+     */
293
+    getTrackSourceName(ssrc) {
294
+        return this._sourceNames.get(ssrc);
295
+    }
296
+
297
+    /**
298
+     * @inheritDoc
299
+     */
300
+    removeSSRCOwners(ssrcList) {
301
+        if (!ssrcList?.length) {
302
+            return;
303
+        }
304
+
305
+        for (const ssrc of ssrcList) {
306
+            this.ssrcOwners.delete(ssrc);
307
+        }
308
+    }
309
+
310
+    /**
311
+     * Sets the <tt>ChatRoom</tt> instance used and binds presence listeners.
312
+     * @param {ChatRoom} room
313
+     */
314
+    setChatRoom(room) {
315
+        const oldChatRoom = this.chatRoom;
316
+
317
+        this.chatRoom = room;
318
+        if (oldChatRoom) {
319
+            oldChatRoom.removePresenceListener(
320
+                'audiomuted', this._audioMuteHandler);
321
+            oldChatRoom.removePresenceListener(
322
+                'videomuted', this._videoMuteHandler);
323
+            oldChatRoom.removePresenceListener(
324
+                'videoType', this._videoTypeHandler);
325
+            this._sourceInfoHandler
326
+                && oldChatRoom.removePresenceListener(SOURCE_INFO_PRESENCE_ELEMENT, this._sourceInfoHandler);
327
+            this._memberLeftHandler
328
+                && oldChatRoom.removeEventListener(XMPPEvents.MUC_MEMBER_LEFT, this._memberLeftHandler);
329
+        }
330
+        if (room) {
331
+            this._bindChatRoomEventHandlers(room);
332
+            this._addLocalSourceInfoToPresence();
333
+        }
334
+    }
335
+
325 336
     /**
326 337
      * @inheritDoc
327 338
      */
@@ -335,7 +346,7 @@ export default class SignalingLayerImpl extends SignalingLayer {
335 346
         const existingOwner = this.ssrcOwners.get(ssrc);
336 347
 
337 348
         if (existingOwner && existingOwner !== endpointId) {
338
-            logger.error(`SSRC owner re-assigned from ${existingOwner} to ${endpointId}`);
349
+            this._logOwnerChangedMessage(`SSRC owner re-assigned from ${existingOwner} to ${endpointId}`);
339 350
         }
340 351
         this.ssrcOwners.set(ssrc, endpointId);
341 352
     }
@@ -357,6 +368,25 @@ export default class SignalingLayerImpl extends SignalingLayer {
357 368
         return false;
358 369
     }
359 370
 
371
+    /**
372
+     * @inheritDoc
373
+     */
374
+    setTrackSourceName(ssrc, sourceName) {
375
+        if (typeof ssrc !== 'number') {
376
+            throw new TypeError(`SSRC(${ssrc}) must be a number`);
377
+        }
378
+
379
+        // Now signaling layer instance is shared between different JingleSessionPC instances, so although very unlikely
380
+        // an SSRC conflict could potentially occur. Log a message to make debugging easier.
381
+        const existingName = this._sourceNames.get(ssrc);
382
+
383
+        if (existingName && existingName !== sourceName) {
384
+            this._logOwnerChangedMessage(`SSRC(${ssrc}) sourceName re-assigned from ${existingName} to ${sourceName}`);
385
+        }
386
+
387
+        this._sourceNames.set(ssrc, sourceName);
388
+    }
389
+
360 390
     /**
361 391
      * @inheritDoc
362 392
      */
@@ -378,27 +408,16 @@ export default class SignalingLayerImpl extends SignalingLayer {
378 408
     /**
379 409
      * @inheritDoc
380 410
      */
381
-    getTrackSourceName(ssrc) {
382
-        return this._sourceNames.get(ssrc);
383
-    }
384
-
385
-    /**
386
-     * @inheritDoc
387
-     */
388
-    setTrackSourceName(ssrc, sourceName) {
389
-        if (typeof ssrc !== 'number') {
390
-            throw new TypeError(`SSRC(${ssrc}) must be a number`);
391
-        }
392
-
393
-        // Now signaling layer instance is shared between different JingleSessionPC instances, so although very unlikely
394
-        // an SSRC conflict could potentially occur. Log a message to make debugging easier.
395
-        const existingName = this._sourceNames.get(ssrc);
411
+    updateSsrcOwnersOnLeave(id) {
412
+        const ssrcs = Array.from(this.ssrcOwners)
413
+            .filter(entry => entry[1] === id)
414
+            .map(entry => entry[0]);
396 415
 
397
-        if (existingName && existingName !== sourceName) {
398
-            logger.error(`SSRC(${ssrc}) sourceName re-assigned from ${existingName} to ${sourceName}`);
416
+        if (!ssrcs?.length) {
417
+            return;
399 418
         }
400 419
 
401
-        this._sourceNames.set(ssrc, sourceName);
420
+        this.removeSSRCOwners(ssrcs);
402 421
     }
403 422
 
404 423
 }

+ 26
- 11
service/RTC/SignalingLayer.js View File

@@ -88,16 +88,6 @@ export function getSourceIndexFromSourceName(sourceName) {
88 88
  * @interface SignalingLayer
89 89
  */
90 90
 export default class SignalingLayer extends Listenable {
91
-
92
-    /**
93
-     * Obtains the endpoint ID for given SSRC.
94
-     * @param {number} ssrc the SSRC number.
95
-     * @return {string|null} the endpoint ID for given media SSRC.
96
-     */
97
-    getSSRCOwner(ssrc) { // eslint-disable-line no-unused-vars
98
-        throw new Error('not implemented');
99
-    }
100
-
101 91
     /**
102 92
      * Obtains the info about given media advertised in the MUC presence of
103 93
      * the participant identified by the given MUC JID.
@@ -126,6 +116,15 @@ export default class SignalingLayer extends Listenable {
126 116
         throw new Error('not implemented');
127 117
     }
128 118
 
119
+    /**
120
+     * Obtains the endpoint ID for given SSRC.
121
+     * @param {number} ssrc the SSRC number.
122
+     * @return {string|null} the endpoint ID for given media SSRC.
123
+     */
124
+    getSSRCOwner(ssrc) { // eslint-disable-line no-unused-vars
125
+        throw new Error('not implemented');
126
+    }
127
+
129 128
     /**
130 129
      * Obtains the source name for given SSRC.
131 130
      * @param {number} ssrc the track's SSRC identifier.
@@ -135,6 +134,14 @@ export default class SignalingLayer extends Listenable {
135 134
         throw new Error('not implemented');
136 135
     }
137 136
 
137
+    /**
138
+     * Removes the association between a given SSRC and its current owner so that it can re-used when the SSRC gets
139
+     * remapped to another source from a different endpoint.
140
+     * @param {number} ssrc a list of SSRCs.
141
+     */
142
+    removeSSRCOwners(ssrcList) { // eslint-disable-line no-unused-vars
143
+    }
144
+
138 145
     /**
139 146
      * Set an SSRC owner.
140 147
      * @param {number} ssrc an SSRC to be owned
@@ -144,7 +151,6 @@ export default class SignalingLayer extends Listenable {
144 151
     setSSRCOwner(ssrc, endpointId) { // eslint-disable-line no-unused-vars
145 152
     }
146 153
 
147
-
148 154
     /**
149 155
      * Adjusts muted status of given track.
150 156
      *
@@ -172,4 +178,13 @@ export default class SignalingLayer extends Listenable {
172 178
      */
173 179
     setTrackVideoType(sourceName, videoType) { // eslint-disable-line no-unused-vars
174 180
     }
181
+
182
+    /**
183
+     * Removes the SSRCs associated with a given endpoint from the SSRC owners.
184
+     *
185
+     * @param {string} id endpoint id of the participant leaving the call.
186
+     * @returns {void}
187
+     */
188
+    updateSsrcOwnersOnLeave(id) { // eslint-disable-line no-unused-vars
189
+    }
175 190
 }

+ 2
- 0
types/hand-crafted/modules/xmpp/SignalingLayerImpl.d.ts View File

@@ -7,5 +7,7 @@ declare class SignalingLayerImpl extends SignalingLayer {
7 7
   setChatRoom: ( room: ChatRoom ) => void;
8 8
   getPeerMediaInfo: ( owner: string, mediaType: MediaType ) => PeerMediaInfo | null;
9 9
   getSSRCOwner: ( ssrc: number ) => string | null;
10
+  removeSSRCOwners: (ssrcList: Array<number> ) => void;
10 11
   setSSRCOwner: ( ssrc: number, endpointId: string ) => void;
12
+  updateSsrcOwnersOnLeave: ( id: string ) => void;
11 13
 }

Loading…
Cancel
Save