Browse Source

P2P ver2 (#413)

* ref(RTC): store remote tracks in peer TPC

In order to implement P2P <-> JVB connection switching we need to
be able to associate remote tracks with the TraceablePeerConnections.

* feat(ChatRoom): multiple presence handlers

Add support for more than 1 presence handler per tag name.

* feat(JitsiLocalTrack): update stored MSID

* ref(stats): add peer connection arg to BYTE_SENT_STATS

Required to store local SSRCs in TraceablePeerConnection.

* ref: change local SSRCs strategy

* fix: generate recvonly SSRC if 0 video tracks

Video SSRC has to be generated for the recvonly stream if there are no
video tracks in the PeerConnection.

* feat: add "attach" and "detach" methods

* feat(jitsi tracks): improve logging

Adds toString methods and improve log messages around local and remote
tracks.

* ref(modify SSRCs): optimisations + fixes

- adds _doRenegotiate to JingleSessionPC that wraps some of
  the duplicated logic
- fixes problems with attach/detach
- renames methods to reflect what that they really do (operate on
  JitsiTrack rather than streams)

* ref(JingleSessionPC): remove duplication

Extracts common code for the 'modificationQueue' execution

* ref(VideoMuteSdpHack): rename, add docs, fix minor

Renames, adds docs and moves 'modified' flag and media direction
modification.

* ref(TPC): add 'isSimulcastOn'

* ref(MungeLocalSdp): move to RTC module

* ref: move "ufrag" events to the RTC module

* ref(JitsiConference): use promises for mute

* feat(XMPPEvents): add CONNECTION_ESTABLISHED

* feat: add peer to peer

* fix(P2P): deal with everyone's a moderator + fixes

* feat(P2P): implement "backToP2PDelay"

* fix(TPC): crash on FF accessing LD/RD

Firefox return null/undefined for localDescription/remoteDescription
objects if they have not been set yet, while Chrome does return empty
object instead. This commit makes the behaviour consistent by making
sure that at least empty object is returned for all browsers.

* fix(TPC): replace isFirefox with feature

* fix(JSPC): fix renegotiate crash on FF

FF does not allow to call 'createAnswer' in 'have-local-offer' state

* fix(JSPC): fix addIceCandidate crash on FF

* doc(JitsiConference): fix outdated comment

To be squashed with "ref(ChatRoom): remove unnecessary JingleSessionPC dependency"

* style(JitsiConference): rename arg to "jingleSession"

* feat(stats): add 'p2p' to 'transport'

The new p2p field will inform whether the transport comes from the peer
to peer type of connection or not.

* doc(TPC): describe local maps

* fix(P2P): multiplex between JVB and P2P ICE status

Will make sure that when in P2P mode the conference will be updated
with the ICE state coming from P2P and when in the JVB mode will get
the JVB one.

* doc(TPC): fixes docs and adds FIXME

* ref: use 'doesVideoMuteByStreamRemove'

* feat(P2P): stop P2P when ICE enters FAILED

The conference will switch back to the JVB connection when P2P
connection breaks (ICE enters failed state).

* feat(P2P): "connectivity-error" for ICE failed

Will use "connectivity-error" reason element name when ending P2P
session due to ICE failure.

* feat(xmpp): make P2P Stun servers configurable

STUN servers used in the P2P connection can be configured through
"p2pStunServers" option.

* ref(JitsiConference): use 'getActivePeerConnection'

* fix(P2P): re-create 'dtmfManager'

* ref(P2P): deal with ICE "completed" state

* ref(RTC): rename "owner" to "ownerEndpointId"

* fix(MungeLocalSdp): fix directions

* ref(ParticipantConnectionStatus): use for..of and () =>

* remove double 'l'

* ref: fix ESLint errors

* fix(MungeLocalSdp): adopt to new SdpTransformerUtil

* ref(MungeLocalSdp): use for .. of

* ref(SdpTransformUtil): remove "forEachSSRCAttr"

* fix(SDPDiffer): fix invalid "arrayEquals" call

* doc(MungeLocalSdp): add fixme

* fix(P2PEnabledConference): JVB tracks not added

* ref(JitsiConference): doc + rename mute methods

* ref(JitsiConference): adjust log level

* fix(JitsiConference): remove invalid eslint comments

Some mistake during rebase merge

* doc(JitsiConference): add FIXME

* ref(JitsiConferenceEventManager): stick to "tpc"

* ref(JitsiLocalTrack): use Set for "peerConnections"

* ref(JitsiLocalTrack): simplify expression

* ref(MungeLocalSdp): style + doc fixes

* ref: rename MungeLocalSdp to LocalSdpMunger

* ref(ParticipantConn..Status): rename method

* ref(SignalingLayerImpl): use Map and =>

* fix(strophe.jingle.js): minor style fixes + rename

* doc(XMPPEvents): typo

* doc(P2PEnabledConference): typo and style

* ref(P2PEnabledConference): rename methods

* ref(P2PEnabledConference): do not use "window"

* fix(P2PEnabledConference): cleanup deferred task

* ref(TPC): make options the last arg

* ref(TPC): use Map

* ref(TPC): syntax and other fixes...

* doc(ChatRoom): remove comment

* ref: remove P2PEnabledConference

* fix: remove JSUtil.js

* ref(LocalSdpMunger): re-use 'RtxModifier'

Reuses RtxModifier for injecting local RTX SSRCs as part of
the LocalSdpMunger logic.

* doc(LocalSdpMunger): remove confusing FIXME

* fix(TPC): setLocalDescription for FF

* fix(LocalSdpMunger): crash on react-native

* fix(JingleSessionPC): no events when ended

The instance once terminated should not emit connection state events.

* fix(P2P): do not start P2P on react-native

* fix: log meaningful error

Prior to this change you would see something like:
JitsiConference <error>: null

* fix(JingleSessionPC): no IQs once ended

* fix(JingleSessionPC): Jingle error logging

* fix: arguments order

* fix: make audio SSRC consistent

Audio SSRC needs to stay consistent between detach and attach operations
in order to avoid source-remove/source-add.

* fix(P2P): disable P2P on FF

There are problems with going back from P2P to JVB in FireFox. Other
participants will not see FF video. Looks like something related to
detach/attach.

* fix(JitsiConference): attach local tracks

Local tracks should be attached back to the JVB connection only
if the P2P was established.

* ref(JitsiConference): PR review fixes

ref(JitsiConference): else if

ref(JitsiConference): use getter

doc(JitsiConference): add comment

style(JitsiConference): remove extra line

log(JitsiConference): misleading msg

ref(JitsiConference): rename method

* ref(RTC): del _iteratePeerConnections

* ref: move getUfrag to SDPUtil

* fix(LocalSdpMunger): docs and if check

* fix(TPC): docs and typo

* ref(JingleSessionPC): PR review fixes

ref(JingleSessionPC): rename 'candidates'

ref(JitsiConference): remove extra check

ref(JitsiConference): rename isP2PEstablished

ref(JitsiConference): rename field (typo)

* doc(JitsiConferenceEventManager): typo

* ref(JitsiLocalTrack): rename var

* ref(JitsiConference): PR review fixes

ref(JitsiConference): rename var

doc(JitsiConference): add comment

doc(JitsiConference): add comment

doc(JitsiConference): fix comment

ref(JitsiConference): rename listener

ref(JitsiConference): rename var

* doc(RTC): remove duplicated arg description

doc(RTC): fill docs

* doc(SignalingLayerImpl): remove fixed FIXME

* ref(strophe.jingle.js): remove comment and break line

* style(TPC): formatting

doc(TPC): add FIXME

ref(TPC): remove unused code

doc(TPC): add docs

* doc(JingleSessionPC): mark "send" methods private

style(JingleSessionPC): extra lines
master
Paweł Domas 8 years ago
parent
commit
81a1b0a31b

+ 929
- 243
JitsiConference.js
File diff suppressed because it is too large
View File


+ 69
- 71
JitsiConferenceEventManager.js View File

29
             conference.statistics.sendMuteEvent(track.isMuted(),
29
             conference.statistics.sendMuteEvent(track.isMuted(),
30
                 track.getType());
30
                 track.getType());
31
         });
31
         });
32
+    conference.on(
33
+        JitsiConferenceEvents.CONNECTION_INTERRUPTED,
34
+        Statistics.sendEventToAll.bind(Statistics, 'connection.interrupted'));
35
+    conference.on(
36
+        JitsiConferenceEvents.CONNECTION_RESTORED,
37
+        Statistics.sendEventToAll.bind(Statistics, 'connection.restored'));
32
 }
38
 }
33
 
39
 
34
 /**
40
 /**
111
     this.chatRoomForwarder = new EventEmitterForwarder(chatRoom,
117
     this.chatRoomForwarder = new EventEmitterForwarder(chatRoom,
112
         this.conference.eventEmitter);
118
         this.conference.eventEmitter);
113
 
119
 
114
-    chatRoom.addListener(XMPPEvents.ICE_RESTARTING, () => {
115
-        // All data channels have to be closed, before ICE restart
116
-        // otherwise Chrome will not trigger "opened" event for the channel
117
-        // established with the new bridge
118
-        conference.rtc.closeAllDataChannels();
120
+    chatRoom.addListener(XMPPEvents.ICE_RESTARTING, jingleSession => {
121
+        if (!jingleSession.isP2P) {
122
+            // All data channels have to be closed, before ICE restart
123
+            // otherwise Chrome will not trigger "opened" event for the channel
124
+            // established with the new bridge
125
+            conference.rtc.closeAllDataChannels();
126
+        }
127
+
128
+        // else: there are no DataChannels in P2P session (at least for now)
119
     });
129
     });
120
 
130
 
121
     chatRoom.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS,
131
     chatRoom.addListener(XMPPEvents.AUDIO_MUTED_BY_FOCUS,
140
     // send some analytics events
150
     // send some analytics events
141
     chatRoom.addListener(XMPPEvents.MUC_JOINED,
151
     chatRoom.addListener(XMPPEvents.MUC_JOINED,
142
         () => {
152
         () => {
143
-            this.conference.connectionIsInterrupted = false;
153
+            this.conference.isJvbConnectionInterrupted = false;
144
 
154
 
145
             Object.keys(chatRoom.connectionTimes).forEach(key => {
155
             Object.keys(chatRoom.connectionTimes).forEach(key => {
146
                 const value = chatRoom.connectionTimes[key];
156
                 const value = chatRoom.connectionTimes[key];
194
 
204
 
195
     chatRoom.addListener(XMPPEvents.JINGLE_FATAL_ERROR,
205
     chatRoom.addListener(XMPPEvents.JINGLE_FATAL_ERROR,
196
         (session, error) => {
206
         (session, error) => {
197
-            conference.eventEmitter.emit(
198
-                JitsiConferenceEvents.CONFERENCE_FAILED,
199
-                JitsiConferenceErrors.JINGLE_FATAL_ERROR, error);
207
+            if (!session.isP2P) {
208
+                conference.eventEmitter.emit(
209
+                    JitsiConferenceEvents.CONFERENCE_FAILED,
210
+                    JitsiConferenceErrors.JINGLE_FATAL_ERROR, error);
211
+            }
200
         });
212
         });
201
 
213
 
202
     chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
214
     chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
203
-        () => {
204
-            chatRoom.eventEmitter.emit(
205
-                XMPPEvents.CONFERENCE_SETUP_FAILED,
206
-                new Error('ICE fail'));
215
+        jingleSession => {
216
+            conference._onIceConnectionFailed(jingleSession);
207
         });
217
         });
208
 
218
 
209
     this.chatRoomForwarder.forward(XMPPEvents.MUC_DESTROYED,
219
     this.chatRoomForwarder.forward(XMPPEvents.MUC_DESTROYED,
230
         = reason => Statistics.sendEventToAll(`conference.error.${reason}`);
240
         = reason => Statistics.sendEventToAll(`conference.error.${reason}`);
231
 
241
 
232
     chatRoom.addListener(XMPPEvents.SESSION_ACCEPT_TIMEOUT,
242
     chatRoom.addListener(XMPPEvents.SESSION_ACCEPT_TIMEOUT,
233
-        eventLogHandler.bind(null, 'sessionAcceptTimeout'));
234
-
235
-    this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_INTERRUPTED,
236
-        JitsiConferenceEvents.CONNECTION_INTERRUPTED);
237
-    chatRoom.addListener(XMPPEvents.CONNECTION_INTERRUPTED,
238
-        () => {
239
-            Statistics.sendEventToAll('connection.interrupted');
240
-            this.conference.connectionIsInterrupted = true;
243
+        jingleSession => {
244
+            eventLogHandler(
245
+                jingleSession.isP2P
246
+                    ? 'p2pSessionAcceptTimeout' : 'sessionAcceptTimeout');
241
         });
247
         });
242
 
248
 
243
     this.chatRoomForwarder.forward(XMPPEvents.RECORDER_STATE_CHANGED,
249
     this.chatRoomForwarder.forward(XMPPEvents.RECORDER_STATE_CHANGED,
246
     this.chatRoomForwarder.forward(XMPPEvents.PHONE_NUMBER_CHANGED,
252
     this.chatRoomForwarder.forward(XMPPEvents.PHONE_NUMBER_CHANGED,
247
         JitsiConferenceEvents.PHONE_NUMBER_CHANGED);
253
         JitsiConferenceEvents.PHONE_NUMBER_CHANGED);
248
 
254
 
249
-    this.chatRoomForwarder.forward(XMPPEvents.CONNECTION_RESTORED,
250
-        JitsiConferenceEvents.CONNECTION_RESTORED);
251
-    chatRoom.addListener(XMPPEvents.CONNECTION_RESTORED,
252
-        () => {
253
-            Statistics.sendEventToAll('connection.restored');
254
-            this.conference.connectionIsInterrupted = false;
255
+    chatRoom.addListener(
256
+        XMPPEvents.CONFERENCE_SETUP_FAILED,
257
+        (jingleSession, error) => {
258
+            if (!jingleSession.isP2P) {
259
+                conference.eventEmitter.emit(
260
+                    JitsiConferenceEvents.CONFERENCE_FAILED,
261
+                    JitsiConferenceErrors.SETUP_FAILED,
262
+                    error);
263
+            }
255
         });
264
         });
256
 
265
 
257
-    this.chatRoomForwarder.forward(XMPPEvents.CONFERENCE_SETUP_FAILED,
258
-        JitsiConferenceEvents.CONFERENCE_FAILED,
259
-        JitsiConferenceErrors.SETUP_FAILED);
260
-
261
     chatRoom.setParticipantPropertyListener((node, from) => {
266
     chatRoom.setParticipantPropertyListener((node, from) => {
262
         const participant = conference.getParticipantById(from);
267
         const participant = conference.getParticipantById(from);
263
 
268
 
294
         conference.onDisplayNameChanged.bind(conference));
299
         conference.onDisplayNameChanged.bind(conference));
295
 
300
 
296
     chatRoom.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, role => {
301
     chatRoom.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, role => {
297
-        conference.eventEmitter.emit(JitsiConferenceEvents.USER_ROLE_CHANGED,
298
-            conference.myUserId(), role);
302
+        conference.onLocalRoleChanged(role);
299
 
303
 
300
         // log all events for the recorder operated by the moderator
304
         // log all events for the recorder operated by the moderator
301
         if (conference.statistics && conference.isModerator()) {
305
         if (conference.statistics && conference.isModerator()) {
351
                 JitsiConferenceEvents.USER_STATUS_CHANGED, id, status);
355
                 JitsiConferenceEvents.USER_STATUS_CHANGED, id, status);
352
         });
356
         });
353
 
357
 
354
-    conference.room.addListener(XMPPEvents.LOCAL_UFRAG_CHANGED,
355
-        ufrag => {
356
-            Statistics.sendLog(
357
-                JSON.stringify({ id: 'local_ufrag',
358
-                    value: ufrag }));
359
-        });
360
-    conference.room.addListener(XMPPEvents.REMOTE_UFRAG_CHANGED,
361
-        ufrag => {
362
-            Statistics.sendLog(
363
-                JSON.stringify({ id: 'remote_ufrag',
364
-                    value: ufrag }));
365
-        });
366
-
367
     chatRoom.addPresenceListener('startmuted', (data, from) => {
358
     chatRoom.addPresenceListener('startmuted', (data, from) => {
368
         let isModerator = false;
359
         let isModerator = false;
369
 
360
 
404
         }
395
         }
405
     });
396
     });
406
 
397
 
407
-    chatRoom.addPresenceListener('videomuted', (values, from) => {
408
-        conference.rtc.handleRemoteTrackMute(MediaType.VIDEO,
409
-            values.value === 'true', from);
410
-    });
411
-
412
-    chatRoom.addPresenceListener('audiomuted', (values, from) => {
413
-        conference.rtc.handleRemoteTrackMute(MediaType.AUDIO,
414
-            values.value === 'true', from);
415
-    });
416
-
417
-    chatRoom.addPresenceListener('videoType', (data, from) => {
418
-        conference.rtc.handleRemoteTrackVideoTypeChanged(data.value, from);
419
-    });
420
-
421
     chatRoom.addPresenceListener('devices', (data, from) => {
398
     chatRoom.addPresenceListener('devices', (data, from) => {
422
         let isAudioAvailable = false;
399
         let isAudioAvailable = false;
423
         let isVideoAvailable = false;
400
         let isVideoAvailable = false;
467
     if (conference.statistics) {
444
     if (conference.statistics) {
468
         // FIXME ICE related events should end up in RTCEvents eventually
445
         // FIXME ICE related events should end up in RTCEvents eventually
469
         chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
446
         chatRoom.addListener(XMPPEvents.CONNECTION_ICE_FAILED,
470
-            pc => {
447
+            (session, pc) => {
471
                 conference.statistics.sendIceConnectionFailedEvent(pc);
448
                 conference.statistics.sendIceConnectionFailedEvent(pc);
472
             });
449
             });
473
         chatRoom.addListener(XMPPEvents.ADD_ICE_CANDIDATE_FAILED,
450
         chatRoom.addListener(XMPPEvents.ADD_ICE_CANDIDATE_FAILED,
543
             }
520
             }
544
         });
521
         });
545
 
522
 
523
+    rtc.addListener(RTCEvents.LOCAL_UFRAG_CHANGED,
524
+        (tpc, ufrag) => {
525
+            if (!tpc.isP2P) {
526
+                Statistics.sendLog(
527
+                    JSON.stringify({
528
+                        id: 'local_ufrag',
529
+                        value: ufrag
530
+                    }));
531
+            }
532
+        });
533
+    rtc.addListener(RTCEvents.REMOTE_UFRAG_CHANGED,
534
+        (tpc, ufrag) => {
535
+            if (!tpc.isP2P) {
536
+                Statistics.sendLog(
537
+                    JSON.stringify({
538
+                        id: 'remote_ufrag',
539
+                        value: ufrag
540
+                    }));
541
+            }
542
+        });
543
+
546
     if (conference.statistics) {
544
     if (conference.statistics) {
547
         rtc.addListener(RTCEvents.CREATE_ANSWER_FAILED,
545
         rtc.addListener(RTCEvents.CREATE_ANSWER_FAILED,
548
             (e, pc) => {
546
             (e, pc) => {
587
     conference.xmpp.addListener(
585
     conference.xmpp.addListener(
588
         XMPPEvents.CALL_INCOMING,
586
         XMPPEvents.CALL_INCOMING,
589
         conference.onIncomingCall.bind(conference));
587
         conference.onIncomingCall.bind(conference));
588
+    conference.xmpp.addListener(
589
+        XMPPEvents.CALL_ACCEPTED,
590
+        conference.onCallAccepted.bind(conference));
591
+    conference.xmpp.addListener(
592
+        XMPPEvents.TRANSPORT_INFO,
593
+        conference.onTransportInfo.bind(conference));
590
     conference.xmpp.addListener(
594
     conference.xmpp.addListener(
591
         XMPPEvents.CALL_ENDED,
595
         XMPPEvents.CALL_ENDED,
592
         conference.onCallEnded.bind(conference));
596
         conference.onCallEnded.bind(conference));
624
     }
628
     }
625
 
629
 
626
     conference.statistics.addAudioLevelListener((ssrc, level) => {
630
     conference.statistics.addAudioLevelListener((ssrc, level) => {
627
-        const resource = conference.rtc.getResourceBySSRC(ssrc);
628
-
629
-        if (!resource) {
630
-            return;
631
-        }
632
-
633
-        conference.rtc.setAudioLevel(resource, level);
631
+        conference.rtc.setAudioLevel(ssrc, level);
634
     });
632
     });
635
 
633
 
636
     // Forward the "before stats disposed" event
634
     // Forward the "before stats disposed" event
647
             JitsiConferenceEvents.CONNECTION_STATS, stats);
645
             JitsiConferenceEvents.CONNECTION_STATS, stats);
648
     });
646
     });
649
 
647
 
650
-    conference.statistics.addByteSentStatsListener(stats => {
648
+    conference.statistics.addByteSentStatsListener((tpc, stats) => {
651
         conference.getLocalTracks(MediaType.AUDIO).forEach(track => {
649
         conference.getLocalTracks(MediaType.AUDIO).forEach(track => {
652
-            const ssrc = track.getSSRC();
650
+            const ssrc = tpc.getLocalSSRC(track);
653
 
651
 
654
             if (!ssrc || !stats.hasOwnProperty(ssrc)) {
652
             if (!ssrc || !stats.hasOwnProperty(ssrc)) {
655
                 return;
653
                 return;
656
             }
654
             }
657
 
655
 
658
-            track._setByteSent(stats[ssrc]);
656
+            track._setByteSent(tpc, stats[ssrc]);
659
         });
657
         });
660
     });
658
     });
661
 };
659
 };

+ 7
- 0
JitsiConferenceEvents.js View File

132
 export const PARTICIPANT_PROPERTY_CHANGED
132
 export const PARTICIPANT_PROPERTY_CHANGED
133
     = 'conference.participant_property_changed';
133
     = 'conference.participant_property_changed';
134
 
134
 
135
+/**
136
+ * Indicates that the conference has switched between JVB and P2P connections.
137
+ * The first argument of this event is a <tt>boolean</tt> which when set to
138
+ * <tt>true</tt> means that the conference is running on the P2P connection.
139
+ */
140
+export const P2P_STATUS = 'conference.p2pStatus';
141
+
135
 /**
142
 /**
136
  * Indicates that phone number changed.
143
  * Indicates that phone number changed.
137
  */
144
  */

+ 6
- 5
JitsiConnection.js View File

1
-const JitsiConference = require('./JitsiConference');
2
-
1
+import JitsiConference from './JitsiConference';
3
 import * as JitsiConnectionEvents from './JitsiConnectionEvents';
2
 import * as JitsiConnectionEvents from './JitsiConnectionEvents';
3
+import Statistics from './modules/statistics/statistics';
4
 import XMPP from './modules/xmpp/xmpp';
4
 import XMPP from './modules/xmpp/xmpp';
5
-const Statistics = require('./modules/statistics/statistics');
6
 
5
 
7
 /**
6
 /**
8
  * Creates new connection object for the Jitsi Meet server side video
7
  * Creates new connection object for the Jitsi Meet server side video
90
  * @returns {JitsiConference} returns the new conference object.
89
  * @returns {JitsiConference} returns the new conference object.
91
  */
90
  */
92
 JitsiConnection.prototype.initJitsiConference = function(name, options) {
91
 JitsiConnection.prototype.initJitsiConference = function(name, options) {
93
-    return new JitsiConference({ name,
92
+    return new JitsiConference({
93
+        name,
94
         config: options,
94
         config: options,
95
-        connection: this });
95
+        connection: this
96
+    });
96
 };
97
 };
97
 
98
 
98
 /**
99
 /**

+ 111
- 62
modules/RTC/JitsiLocalTrack.js View File

20
 /**
20
 /**
21
  * Represents a single media track(either audio or video).
21
  * Represents a single media track(either audio or video).
22
  * One <tt>JitsiLocalTrack</tt> corresponds to one WebRTC MediaStreamTrack.
22
  * One <tt>JitsiLocalTrack</tt> corresponds to one WebRTC MediaStreamTrack.
23
+ * @param {number} rtcId the ID assigned by the RTC module
23
  * @param stream WebRTC MediaStream, parent of the track
24
  * @param stream WebRTC MediaStream, parent of the track
24
  * @param track underlying WebRTC MediaStreamTrack for new JitsiRemoteTrack
25
  * @param track underlying WebRTC MediaStreamTrack for new JitsiRemoteTrack
25
  * @param mediaType the MediaType of the JitsiRemoteTrack
26
  * @param mediaType the MediaType of the JitsiRemoteTrack
30
  * @constructor
31
  * @constructor
31
  */
32
  */
32
 function JitsiLocalTrack(
33
 function JitsiLocalTrack(
34
+        rtcId,
33
         stream,
35
         stream,
34
         track,
36
         track,
35
         mediaType,
37
         mediaType,
37
         resolution,
39
         resolution,
38
         deviceId,
40
         deviceId,
39
         facingMode) {
41
         facingMode) {
42
+
43
+    /**
44
+     * The ID assigned by the RTC module on instance creation.
45
+     * @type {number}
46
+     */
47
+    this.rtcId = rtcId;
40
     JitsiTrack.call(
48
     JitsiTrack.call(
41
         this,
49
         this,
42
         null /* RTC */,
50
         null /* RTC */,
49
             this.dontFireRemoveEvent = false;
57
             this.dontFireRemoveEvent = false;
50
         } /* inactiveHandler */,
58
         } /* inactiveHandler */,
51
         mediaType,
59
         mediaType,
52
-        videoType,
53
-        null /* ssrc */);
60
+        videoType);
54
     this.dontFireRemoveEvent = false;
61
     this.dontFireRemoveEvent = false;
55
     this.resolution = resolution;
62
     this.resolution = resolution;
56
 
63
 
62
 
69
 
63
     this.deviceId = deviceId;
70
     this.deviceId = deviceId;
64
     this.startMuted = false;
71
     this.startMuted = false;
65
-    this.initialMSID = this.getMSID();
72
+    this.storedMSID = this.getMSID();
66
     this.inMuteOrUnmuteProgress = false;
73
     this.inMuteOrUnmuteProgress = false;
67
 
74
 
75
+    /**
76
+     * An array which stores the peer connection to which this local track is
77
+     * currently attached to. See {@link TraceablePeerConnection.attachTrack}.
78
+     * @type {Set<TraceablePeerConnection>}
79
+     */
80
+    this.peerConnections = new Set();
81
+
68
     /**
82
     /**
69
      * The facing mode of the camera from which this JitsiLocalTrack instance
83
      * The facing mode of the camera from which this JitsiLocalTrack instance
70
      * was obtained.
84
      * was obtained.
144
 JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
158
 JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
145
 JitsiLocalTrack.prototype.constructor = JitsiLocalTrack;
159
 JitsiLocalTrack.prototype.constructor = JitsiLocalTrack;
146
 
160
 
161
+JitsiLocalTrack.prototype._addPeerConnection = function(tpc) {
162
+    if (this._isAttachedToPC(tpc)) {
163
+        logger.error(`${tpc} has been associated with ${this} already !`);
164
+    } else {
165
+        this.peerConnections.add(tpc);
166
+    }
167
+};
168
+
169
+JitsiLocalTrack.prototype._removePeerConnection = function(tpc) {
170
+    if (this._isAttachedToPC(tpc)) {
171
+        this.peerConnections.delete(tpc);
172
+    } else {
173
+        logger.error(`${tpc} is not associated with ${this}`);
174
+    }
175
+};
176
+
177
+/**
178
+ * Checks whether or not this instance is attached to given
179
+ * <tt>TraceablePeerConnection</tt>. See
180
+ * {@link TraceablePeerConnection.attachTrack} for more info.
181
+ * @param {TraceablePeerConnection.attachTrack} tpc
182
+ * @return {boolean} <tt>true</tt> if this tracks is currently attached to given
183
+ * peer connection or <tt>false</tt> otherwise.
184
+ */
185
+JitsiLocalTrack.prototype._isAttachedToPC = function(tpc) {
186
+    return this.peerConnections.has(tpc);
187
+};
188
+
147
 /**
189
 /**
148
  * Returns if associated MediaStreamTrack is in the 'ended' state
190
  * Returns if associated MediaStreamTrack is in the 'ended' state
149
  * @returns {boolean}
191
  * @returns {boolean}
237
     }
279
     }
238
 };
280
 };
239
 
281
 
282
+/**
283
+ * Sets the stream property of JitsiLocalTrack object and sets all stored
284
+ * handlers to it.
285
+ * @param {MediaStream} stream the new stream.
286
+ */
287
+JitsiLocalTrack.prototype._setStream = function(stream) {
288
+    JitsiTrack.prototype._setStream.call(this, stream);
289
+
290
+    // Store the MSID for video mute/unmute purposes
291
+    if (stream) {
292
+        this.storedMSID = this.getMSID();
293
+        logger.debug(`Setting new MSID: ${this.storedMSID} on ${this}`);
294
+    } else {
295
+        logger.debug(`Setting 'null' stream on ${this}`);
296
+    }
297
+};
298
+
240
 /**
299
 /**
241
  * Mutes the track. Will reject the Promise if there is mute/unmute operation
300
  * Mutes the track. Will reject the Promise if there is mute/unmute operation
242
  * in progress.
301
  * in progress.
300
     // Local track can be used out of conference, so we need to handle that
359
     // Local track can be used out of conference, so we need to handle that
301
     // case and mark that track should start muted or not when added to
360
     // case and mark that track should start muted or not when added to
302
     // conference.
361
     // conference.
362
+    // Pawel: track's muted status should be taken into account when track is
363
+    // being added to the conference/JingleSessionPC/TraceablePeerConnection.
364
+    // There's no need to add such fields. It is logical that when muted track
365
+    // is being added to a conference it "starts muted"...
303
     if (!this.conference || !this.conference.room) {
366
     if (!this.conference || !this.conference.room) {
304
         this.startMuted = mute;
367
         this.startMuted = mute;
305
     }
368
     }
306
 
369
 
307
     this.dontFireRemoveEvent = false;
370
     this.dontFireRemoveEvent = false;
308
 
371
 
309
-    // FIXME FF does not support 'removeStream' method used to mute
372
+    // A function that will print info about muted status transition
373
+    const logMuteInfo = () => logger.info(`Mute ${this}: ${mute}`);
374
+
310
     if (this.isAudioTrack()
375
     if (this.isAudioTrack()
311
         || this.videoType === VideoType.DESKTOP
376
         || this.videoType === VideoType.DESKTOP
312
-        || RTCBrowserType.isFirefox()) {
377
+        || !RTCBrowserType.doesVideoMuteByStreamRemove()) {
378
+        logMuteInfo();
313
         if (this.track) {
379
         if (this.track) {
314
             this.track.enabled = !mute;
380
             this.track.enabled = !mute;
315
         }
381
         }
316
     } else if (mute) {
382
     } else if (mute) {
317
         this.dontFireRemoveEvent = true;
383
         this.dontFireRemoveEvent = true;
318
         promise = new Promise((resolve, reject) => {
384
         promise = new Promise((resolve, reject) => {
385
+            logMuteInfo();
319
             this._removeStreamFromConferenceAsMute(() => {
386
             this._removeStreamFromConferenceAsMute(() => {
320
                 // FIXME: Maybe here we should set the SRC for the containers
387
                 // FIXME: Maybe here we should set the SRC for the containers
321
                 // to something
388
                 // to something
327
             });
394
             });
328
         });
395
         });
329
     } else {
396
     } else {
397
+        logMuteInfo();
398
+
330
         // This path is only for camera.
399
         // This path is only for camera.
331
         const streamOptions = {
400
         const streamOptions = {
332
             cameraDeviceId: this.getDeviceId(),
401
             cameraDeviceId: this.getDeviceId(),
352
                     // unmute, but let's not crash here
421
                     // unmute, but let's not crash here
353
                     if (self.videoType !== streamInfo.videoType) {
422
                     if (self.videoType !== streamInfo.videoType) {
354
                         logger.warn(
423
                         logger.warn(
355
-                            'Video type has changed after unmute!',
424
+                            `${this}: video type has changed after unmute!`,
356
                             self.videoType, streamInfo.videoType);
425
                             self.videoType, streamInfo.videoType);
357
                         self.videoType = streamInfo.videoType;
426
                         self.videoType = streamInfo.videoType;
358
                     }
427
                     }
387
         return Promise.resolve();
456
         return Promise.resolve();
388
     }
457
     }
389
 
458
 
459
+    // FIXME it would be good to not included conference as part of this process
460
+    // Only TraceablePeerConnections to which the track is attached should care
461
+    // about this action. The TPCs to which the track is not attached can sync
462
+    // up when track is re-attached.
463
+    // A problem with that is that the "modify sources" queue is part of
464
+    // the JingleSessionPC and it would be excluded from the process. One
465
+    // solution would be to extract class between TPC and JingleSessionPC which
466
+    // would contain the queue and would notify the signaling layer when local
467
+    // SSRCs are changed. This would help to separate XMPP from the RTC module.
390
     return new Promise((resolve, reject) => {
468
     return new Promise((resolve, reject) => {
391
-        this.conference._addLocalStream(
392
-            this.stream,
393
-            resolve,
394
-            error => reject(new Error(error)),
395
-            {
396
-                mtype: this.type,
397
-                type: 'unmute',
398
-                ssrcs: this.ssrc && this.ssrc.ssrcs,
399
-                groups: this.ssrc && this.ssrc.groups,
400
-                msid: this.getMSID()
401
-            });
469
+        this.conference._addLocalTrackAsUnmute(this)
470
+            .then(resolve, error => reject(new Error(error)));
402
     });
471
     });
403
 };
472
 };
404
 
473
 
415
 
484
 
416
         return;
485
         return;
417
     }
486
     }
418
-
419
-    this.conference.removeLocalStream(
420
-        this.stream,
487
+    this.conference._removeLocalTrackAsMute(this).then(
421
         successCallback,
488
         successCallback,
422
-        error => errorCallback(new Error(error)),
423
-        {
424
-            mtype: this.type,
425
-            type: 'mute',
426
-            ssrcs: this.ssrc && this.ssrc.ssrcs,
427
-            groups: this.ssrc && this.ssrc.groups
428
-        });
489
+        error => errorCallback(new Error(error)));
429
 };
490
 };
430
 
491
 
431
 /**
492
 /**
502
 
563
 
503
 };
564
 };
504
 
565
 
505
-/**
506
- * Updates the SSRC associated with the MediaStream in JitsiLocalTrack object.
507
- * @ssrc the new ssrc
508
- */
509
-JitsiLocalTrack.prototype._setSSRC = function(ssrc) {
510
-    this.ssrc = ssrc;
511
-};
512
-
513
-
514
 /**
566
 /**
515
  * Sets the JitsiConference object associated with the track. This is temp
567
  * Sets the JitsiConference object associated with the track. This is temp
516
  * solution.
568
  * solution.
528
     }
580
     }
529
 };
581
 };
530
 
582
 
531
-/**
532
- * Gets the SSRC of this local track if it's available already or <tt>null</tt>
533
- * otherwise. That's because we don't know the SSRC until local description is
534
- * created.
535
- * In case of video and simulcast returns the the primarySSRC.
536
- * @returns {string} or {null}
537
- */
538
-JitsiLocalTrack.prototype.getSSRC = function() {
539
-    if (this.ssrc && this.ssrc.groups && this.ssrc.groups.length) {
540
-        return this.ssrc.groups[0].ssrcs[0];
541
-    } else if (this.ssrc && this.ssrc.ssrcs && this.ssrc.ssrcs.length) {
542
-        return this.ssrc.ssrcs[0];
543
-    }
544
-
545
-    return null;
546
-
547
-};
548
-
549
 /**
583
 /**
550
  * Returns <tt>true</tt>.
584
  * Returns <tt>true</tt>.
551
  * @returns {boolean} <tt>true</tt>
585
  * @returns {boolean} <tt>true</tt>
562
     return this._realDeviceId || this.deviceId;
596
     return this._realDeviceId || this.deviceId;
563
 };
597
 };
564
 
598
 
599
+/**
600
+ * Returns the participant id which owns the track.
601
+ * @returns {string} the id of the participants. It corresponds to the Colibri
602
+ * endpoint id/MUC nickname in case of Jitsi-meet.
603
+ */
604
+JitsiLocalTrack.prototype.getParticipantId = function() {
605
+    return this.conference && this.conference.myUserId();
606
+};
607
+
565
 /**
608
 /**
566
  * Sets the value of bytes sent statistic.
609
  * Sets the value of bytes sent statistic.
567
- * @param bytesSent {integer} the new value (FIXME: what is an integer in js?)
610
+ * @param {TraceablePeerConnection} tpc the source of the "bytes sent" stat
611
+ * @param {number} bytesSent the new value
568
  * NOTE: used only for audio tracks to detect audio issues.
612
  * NOTE: used only for audio tracks to detect audio issues.
569
  */
613
  */
570
-JitsiLocalTrack.prototype._setByteSent = function(bytesSent) {
614
+JitsiLocalTrack.prototype._setByteSent = function(tpc, bytesSent) {
571
     this._bytesSent = bytesSent;
615
     this._bytesSent = bytesSent;
572
-
573
-    // FIXME it's a shame that PeerConnection and ICE status does not belong
574
-    // to the RTC module and it has to be accessed through
575
-    // the conference(and through the XMPP chat room ???) instead
576
-    const iceConnectionState
577
-        = this.conference ? this.conference.getConnectionState() : null;
616
+    const iceConnectionState = tpc.getConnectionState();
578
 
617
 
579
     if (this._testByteSent && iceConnectionState === 'connected') {
618
     if (this._testByteSent && iceConnectionState === 'connected') {
580
         setTimeout(() => {
619
         setTimeout(() => {
581
             if (this._bytesSent <= 0) {
620
             if (this._bytesSent <= 0) {
621
+                logger.warn(`${this} 'bytes sent' <= 0: ${this._bytesSent}`);
622
+
582
                 // we are not receiving anything from the microphone
623
                 // we are not receiving anything from the microphone
583
                 this._fireNoDataFromSourceEvent();
624
                 this._fireNoDataFromSourceEvent();
584
             }
625
             }
676
             && (!('muted' in track) || track.muted !== true));
717
             && (!('muted' in track) || track.muted !== true));
677
 };
718
 };
678
 
719
 
720
+/**
721
+ * Creates a text representation of this local track instance.
722
+ * @return {string}
723
+ */
724
+JitsiLocalTrack.prototype.toString = function() {
725
+    return `LocalTrack[${this.rtcId},${this.getType()}]`;
726
+};
727
+
679
 module.exports = JitsiLocalTrack;
728
 module.exports = JitsiLocalTrack;

+ 16
- 3
modules/RTC/JitsiRemoteTrack.js View File

24
  * @param {VideoType} videoType the type of the video if applicable
24
  * @param {VideoType} videoType the type of the video if applicable
25
  * @param {string} ssrc the SSRC number of the Media Stream
25
  * @param {string} ssrc the SSRC number of the Media Stream
26
  * @param {boolean} muted the initial muted state
26
  * @param {boolean} muted the initial muted state
27
+ * @param {boolean} isP2P indicates whether or not this track belongs to a P2P
28
+ * session
27
  * @constructor
29
  * @constructor
28
  */
30
  */
29
 function JitsiRemoteTrack(
31
 function JitsiRemoteTrack(
35
         mediaType,
37
         mediaType,
36
         videoType,
38
         videoType,
37
         ssrc,
39
         ssrc,
38
-        muted) {
40
+        muted,
41
+        isP2P) {
39
     JitsiTrack.call(
42
     JitsiTrack.call(
40
         this,
43
         this,
41
         conference,
44
         conference,
45
             // Nothing to do if the track is inactive.
48
             // Nothing to do if the track is inactive.
46
         },
49
         },
47
         mediaType,
50
         mediaType,
48
-        videoType,
49
-        ssrc);
51
+        videoType);
50
     this.rtc = rtc;
52
     this.rtc = rtc;
53
+    this.ssrc = ssrc;
51
     this.ownerEndpointId = ownerEndpointId;
54
     this.ownerEndpointId = ownerEndpointId;
52
     this.muted = muted;
55
     this.muted = muted;
56
+    this.isP2P = isP2P;
53
 
57
 
54
     // we want to mark whether the track has been ever muted
58
     // we want to mark whether the track has been ever muted
55
     // to detect ttfm events for startmuted conferences, as it can significantly
59
     // to detect ttfm events for startmuted conferences, as it can significantly
218
     }
222
     }
219
 };
223
 };
220
 
224
 
225
+/**
226
+ * Creates a text representation of this remote track instance.
227
+ * @return {string}
228
+ */
229
+JitsiRemoteTrack.prototype.toString = function() {
230
+    return `RemoteTrack[${this.ownerEndpointId}, ${this.getType()
231
+            }, p2p: ${this.isP2P}]`;
232
+};
233
+
221
 module.exports = JitsiRemoteTrack;
234
 module.exports = JitsiRemoteTrack;

+ 6
- 9
modules/RTC/JitsiTrack.js View File

67
  *        onended/oninactive events of the stream.
67
  *        onended/oninactive events of the stream.
68
  * @param trackMediaType the media type of the JitsiTrack
68
  * @param trackMediaType the media type of the JitsiTrack
69
  * @param videoType the VideoType for this track if any
69
  * @param videoType the VideoType for this track if any
70
- * @param ssrc the SSRC of this track if known
71
  */
70
  */
72
 function JitsiTrack(
71
 function JitsiTrack(
73
-        conference,
74
-        stream,
75
-        track,
76
-        streamInactiveHandler,
77
-        trackMediaType,
78
-        videoType,
79
-        ssrc) {
72
+    conference,
73
+    stream,
74
+    track,
75
+    streamInactiveHandler,
76
+    trackMediaType,
77
+    videoType) {
80
     /**
78
     /**
81
      * Array with the HTML elements that are displaying the streams.
79
      * Array with the HTML elements that are displaying the streams.
82
      * @type {Array}
80
      * @type {Array}
84
     this.containers = [];
82
     this.containers = [];
85
     this.conference = conference;
83
     this.conference = conference;
86
     this.stream = stream;
84
     this.stream = stream;
87
-    this.ssrc = ssrc;
88
     this.eventEmitter = new EventEmitter();
85
     this.eventEmitter = new EventEmitter();
89
     this.audioLevel = -1;
86
     this.audioLevel = -1;
90
     this.type = trackMediaType;
87
     this.type = trackMediaType;

+ 298
- 0
modules/RTC/LocalSdpMunger.js View File

1
+/* global __filename */
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+import * as MediaType from '../../service/RTC/MediaType';
5
+import { SdpTransformWrap } from '../xmpp/SdpTransformUtil';
6
+
7
+const logger = getLogger(__filename);
8
+
9
+/**
10
+ * Fakes local SDP, so that it will reflect detached local tracks associated
11
+ * with the {@link TraceablePeerConnection} and make operations like
12
+ * attach/detach and video mute/unmute local operations. That means it prevents
13
+ * from SSRC updates being sent to Jicofo/remote peer, so that there is no
14
+ * sRD/sLD cycle on the remote side.
15
+ */
16
+export default class LocalSdpMunger {
17
+
18
+    /**
19
+     * Creates new <tt>LocalSdpMunger</tt> instance.
20
+     *
21
+     * @param {TraceablePeerConnection} tpc
22
+     */
23
+    constructor(tpc) {
24
+        this.tpc = tpc;
25
+    }
26
+
27
+    /**
28
+     * Makes sure that detached local audio tracks stored in the parent
29
+     * {@link TraceablePeerConnection} are described in the local SDP.
30
+     * It's done in order to prevent from sending 'source-remove'/'source-add'
31
+     * Jingle notifications when local audio track is detached from
32
+     * the {@link TraceablePeerConnection}.
33
+     * @param {SdpTransformWrap} transformer the transformer instance which will
34
+     * be used to process the SDP.
35
+     * @return {boolean} <tt>true</tt> if there were any modifications to
36
+     * the SDP wrapped by <tt>transformer</tt>.
37
+     * @private
38
+     */
39
+    _addDetachedLocalAudioTracksToSDP(transformer) {
40
+        const localAudio = this.tpc.getLocalTracks(MediaType.AUDIO);
41
+
42
+        if (!localAudio.length) {
43
+            return false;
44
+        }
45
+        const audioMLine = transformer.selectMedia('audio');
46
+
47
+        if (!audioMLine) {
48
+            logger.error(
49
+                'Unable to hack local audio track SDP - no "audio" media');
50
+
51
+            return false;
52
+        }
53
+
54
+        if (audioMLine.direction === 'inactive') {
55
+            logger.error(
56
+                'Not doing local audio transform for "inactive" direction');
57
+
58
+            return false;
59
+        }
60
+
61
+        let modified = false;
62
+
63
+        for (const audioTrack of localAudio) {
64
+            const isAttached = audioTrack._isAttachedToPC(this.tpc);
65
+            const shouldFake = !isAttached;
66
+
67
+            logger.debug(
68
+                `${audioTrack} isAttached: ${isAttached
69
+                    } => should fake audio SDP ?: ${shouldFake}`);
70
+
71
+            if (!shouldFake) {
72
+                // not using continue increases indentation
73
+                // eslint-disable-next-line no-continue
74
+                continue;
75
+            }
76
+
77
+            // Inject removed SSRCs
78
+            const audioSSRC = this.tpc.getLocalSSRC(audioTrack);
79
+            const audioMSID = audioTrack.storedMSID;
80
+
81
+            if (!audioSSRC) {
82
+                logger.error(
83
+                    `Can't fake SDP for ${audioTrack} - no SSRC stored`);
84
+
85
+                // Aborts the forEach on this particular track,
86
+                // but will continue with the other ones
87
+                // eslint-disable-next-line no-continue
88
+                continue;
89
+            } else if (!audioMSID) {
90
+                logger.error(
91
+                    `No MSID stored for local audio SSRC: ${audioSSRC}`);
92
+
93
+                // eslint-disable-next-line no-continue
94
+                continue;
95
+            }
96
+
97
+            if (audioMLine.getSSRCCount() > 0) {
98
+                logger.debug(
99
+                    'Doing nothing - audio SSRCs are still there');
100
+
101
+                // audio SSRCs are still there
102
+                // eslint-disable-next-line no-continue
103
+                continue;
104
+            }
105
+
106
+            modified = true;
107
+
108
+            // We need to fake sendrecv
109
+            audioMLine.direction = 'sendrecv';
110
+
111
+            logger.debug(`Injecting audio SSRC: ${audioSSRC}`);
112
+            audioMLine.addSSRCAttribute({
113
+                id: audioSSRC,
114
+                attribute: 'cname',
115
+                value: `injected-${audioSSRC}`
116
+            });
117
+            audioMLine.addSSRCAttribute({
118
+                id: audioSSRC,
119
+                attribute: 'msid',
120
+                value: audioMSID
121
+            });
122
+        }
123
+
124
+        return modified;
125
+    }
126
+
127
+    /**
128
+     * Makes sure that detached (or muted) local video tracks associated with
129
+     * the parent {@link TraceablePeerConnection} are described in the local
130
+     * SDP. It's done in order to prevent from sending
131
+     * 'source-remove'/'source-add' Jingle notifications when local video track
132
+     * is detached from the {@link TraceablePeerConnection} (or muted).
133
+     *
134
+     * NOTE 1 video track is assumed
135
+     *
136
+     * @param {SdpTransformWrap} transformer the transformer instance which will
137
+     * be used to process the SDP.
138
+     * @return {boolean} <tt>true</tt> if there were any modifications to
139
+     * the SDP wrapped by <tt>transformer</tt>.
140
+     * @private
141
+     */
142
+    _addDetachedLocalVideoTracksToSDP(transformer) {
143
+        // Go over each video tracks and check if the SDP has to be changed
144
+        const localVideos = this.tpc.getLocalTracks(MediaType.VIDEO);
145
+
146
+        if (!localVideos.length) {
147
+            return false;
148
+        } else if (localVideos.length !== 1) {
149
+            logger.error(
150
+                'There is more than 1 video track ! '
151
+                    + 'Strange things may happen !', localVideos);
152
+        }
153
+
154
+        const videoMLine = transformer.selectMedia('video');
155
+
156
+        if (!videoMLine) {
157
+            logger.error(
158
+                'Unable to hack local video track SDP - no "video" media');
159
+
160
+            return false;
161
+        }
162
+
163
+        if (videoMLine.direction === 'inactive') {
164
+            logger.error(
165
+                'Not doing local video transform for "inactive" direction.');
166
+
167
+            return false;
168
+        }
169
+
170
+        let modified = false;
171
+
172
+        for (const videoTrack of localVideos) {
173
+            const isMuted = videoTrack.isMuted();
174
+            const muteInProgress = videoTrack.inMuteOrUnmuteProgress;
175
+            const isAttached = videoTrack._isAttachedToPC(this.tpc);
176
+            const shouldFakeSdp = isMuted || muteInProgress || !isAttached;
177
+
178
+            logger.debug(
179
+                `${videoTrack
180
+                 } isMuted: ${isMuted
181
+                 }, is mute in progress: ${muteInProgress
182
+                 }, is attached ? : ${isAttached
183
+                 } => should fake sdp ? : ${shouldFakeSdp}`);
184
+
185
+            if (!shouldFakeSdp) {
186
+                // eslint-disable-next-line no-continue
187
+                continue;
188
+            }
189
+
190
+            // Inject removed SSRCs
191
+            const requiredSSRCs
192
+                = this.tpc.isSimulcastOn()
193
+                    ? this.tpc.simulcast.ssrcCache
194
+                    : [ this.tpc.sdpConsistency.cachedPrimarySsrc ];
195
+
196
+            if (!requiredSSRCs.length) {
197
+                logger.error(
198
+                    `No SSRCs stored for: ${videoTrack} in ${this.tpc}`);
199
+
200
+                // eslint-disable-next-line no-continue
201
+                continue;
202
+            }
203
+            if (!videoMLine.getSSRCCount()) {
204
+                logger.error(
205
+                    'No video SSRCs found '
206
+                        + '(should be at least the recv-only one');
207
+
208
+                // eslint-disable-next-line no-continue
209
+                continue;
210
+            }
211
+
212
+            modified = true;
213
+
214
+            // We need to fake sendrecv
215
+            videoMLine.direction = 'sendrecv';
216
+
217
+            // Check if the recvonly has MSID
218
+            const primarySSRC = requiredSSRCs[0];
219
+
220
+            // FIXME the cname could come from the stream, but may
221
+            // turn out to be too complex. It is fine to come up
222
+            // with any value, as long as we only care about
223
+            // the actual SSRC values when deciding whether or not
224
+            // an update should be sent
225
+            const primaryCname = `injected-${primarySSRC}`;
226
+
227
+            for (const ssrcNum of requiredSSRCs) {
228
+                // Remove old attributes
229
+                videoMLine.removeSSRC(ssrcNum);
230
+
231
+                // Inject
232
+                logger.debug(
233
+                    `Injecting video SSRC: ${ssrcNum} for ${videoTrack}`);
234
+                videoMLine.addSSRCAttribute({
235
+                    id: ssrcNum,
236
+                    attribute: 'cname',
237
+                    value: primaryCname
238
+                });
239
+                videoMLine.addSSRCAttribute({
240
+                    id: ssrcNum,
241
+                    attribute: 'msid',
242
+                    value: videoTrack.storedMSID
243
+                });
244
+            }
245
+            if (requiredSSRCs.length > 1) {
246
+                const group = {
247
+                    ssrcs: requiredSSRCs.join(' '),
248
+                    semantics: 'SIM'
249
+                };
250
+
251
+                if (!videoMLine.findGroup(group.semantics, group.ssrcs)) {
252
+                    // Inject the group
253
+                    logger.debug(
254
+                        `Injecting SIM group for ${videoTrack}`, group);
255
+                    videoMLine.addSSRCGroup(group);
256
+                }
257
+            }
258
+
259
+            // Insert RTX
260
+            // FIXME in P2P RTX is used by Chrome regardless of config option
261
+            // status. Because of that 'source-remove'/'source-add'
262
+            // notifications are still sent to remove/add RTX SSRC and FID group
263
+            if (!this.tpc.options.disableRtx) {
264
+                this.tpc.rtxModifier.modifyRtxSsrcs2(videoMLine);
265
+            }
266
+        }
267
+
268
+        return modified;
269
+    }
270
+
271
+    /**
272
+     * Maybe modifies local description to fake local tracks SDP when those are
273
+     * either muted or detached from the <tt>PeerConnection</tt>.
274
+     *
275
+     * @param {object} desc the WebRTC SDP object instance for the local
276
+     * description.
277
+     */
278
+    maybeMungeLocalSdp(desc) {
279
+        // Nothing to be done in early stage when localDescription
280
+        // is not available yet
281
+        if (!desc || !desc.sdp) {
282
+            return;
283
+        }
284
+
285
+        const transformer = new SdpTransformWrap(desc.sdp);
286
+        let modified = this._addDetachedLocalAudioTracksToSDP(transformer);
287
+
288
+        if (this._addDetachedLocalVideoTracksToSDP(transformer)) {
289
+            modified = true;
290
+        }
291
+        if (modified) {
292
+            // Write
293
+            desc.sdp = transformer.toRawSDP();
294
+
295
+            // logger.info("Post TRANSFORM: ", desc.sdp);
296
+        }
297
+    }
298
+}

+ 65
- 219
modules/RTC/RTC.js View File

3
 import { getLogger } from 'jitsi-meet-logger';
3
 import { getLogger } from 'jitsi-meet-logger';
4
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
4
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
5
 import JitsiLocalTrack from './JitsiLocalTrack.js';
5
 import JitsiLocalTrack from './JitsiLocalTrack.js';
6
-import JitsiRemoteTrack from './JitsiRemoteTrack.js';
7
 import JitsiTrackError from '../../JitsiTrackError';
6
 import JitsiTrackError from '../../JitsiTrackError';
8
 import * as JitsiTrackErrors from '../../JitsiTrackErrors';
7
 import * as JitsiTrackErrors from '../../JitsiTrackErrors';
9
 import Listenable from '../util/Listenable';
8
 import Listenable from '../util/Listenable';
15
 
14
 
16
 const logger = getLogger(__filename);
15
 const logger = getLogger(__filename);
17
 
16
 
17
+let rtcTrackIdCounter = 0;
18
+
18
 /**
19
 /**
19
  *
20
  *
20
  * @param tracksInfo
21
  * @param tracksInfo
30
         } else if (trackInfo.videoType === VideoType.CAMERA) {
31
         } else if (trackInfo.videoType === VideoType.CAMERA) {
31
             deviceId = options.cameraDeviceId;
32
             deviceId = options.cameraDeviceId;
32
         }
33
         }
34
+        rtcTrackIdCounter += 1;
33
         const localTrack
35
         const localTrack
34
             = new JitsiLocalTrack(
36
             = new JitsiLocalTrack(
37
+                rtcTrackIdCounter,
35
                 trackInfo.stream,
38
                 trackInfo.stream,
36
                 trackInfo.track,
39
                 trackInfo.track,
37
                 trackInfo.mediaType,
40
                 trackInfo.mediaType,
73
 
76
 
74
         this.localTracks = [];
77
         this.localTracks = [];
75
 
78
 
76
-        // FIXME: We should support multiple streams per jid.
77
-        this.remoteTracks = {};
78
         this.options = options;
79
         this.options = options;
79
 
80
 
80
         // A flag whether we had received that the data channel had opened
81
         // A flag whether we had received that the data channel had opened
270
         return RTCUtils.getDeviceAvailability();
271
         return RTCUtils.getDeviceAvailability();
271
     }
272
     }
272
 
273
 
274
+    /* eslint-disable max-params */
275
+
273
     /**
276
     /**
274
      * Creates new <tt>TraceablePeerConnection</tt>
277
      * Creates new <tt>TraceablePeerConnection</tt>
275
      * @param {SignalingLayer} signaling the signaling layer that will
278
      * @param {SignalingLayer} signaling the signaling layer that will
277
      * over SDP.
280
      * over SDP.
278
      * @param {Object} iceConfig an object describing the ICE config like
281
      * @param {Object} iceConfig an object describing the ICE config like
279
      * defined in the WebRTC specification.
282
      * defined in the WebRTC specification.
283
+     * @param {boolean} isP2P indicates whether or not the new TPC will be used
284
+     * in a peer to peer type of session
280
      * @param {Object} options the config options
285
      * @param {Object} options the config options
281
      * @param {boolean} options.disableSimulcast if set to 'true' will disable
286
      * @param {boolean} options.disableSimulcast if set to 'true' will disable
282
      * the simulcast
287
      * the simulcast
285
      * preferred over other video codecs.
290
      * preferred over other video codecs.
286
      * @return {TraceablePeerConnection}
291
      * @return {TraceablePeerConnection}
287
      */
292
      */
288
-    createPeerConnection(signaling, iceConfig, options) {
293
+    createPeerConnection(signaling, iceConfig, isP2P, options) {
289
         const newConnection
294
         const newConnection
290
             = new TraceablePeerConnection(
295
             = new TraceablePeerConnection(
291
                 this,
296
                 this,
292
                 this.peerConnectionIdCounter,
297
                 this.peerConnectionIdCounter,
293
-                signaling, iceConfig, RTC.getPCConstraints(), options);
298
+                signaling, iceConfig, RTC.getPCConstraints(), isP2P, options);
294
 
299
 
295
         this.peerConnections.set(newConnection.id, newConnection);
300
         this.peerConnections.set(newConnection.id, newConnection);
296
         this.peerConnectionIdCounter += 1;
301
         this.peerConnectionIdCounter += 1;
298
         return newConnection;
303
         return newConnection;
299
     }
304
     }
300
 
305
 
306
+    /* eslint-enable max-params */
307
+
301
     /**
308
     /**
302
      * Removed given peer connection from this RTC module instance.
309
      * Removed given peer connection from this RTC module instance.
303
      * @param {TraceablePeerConnection} traceablePeerConnection
310
      * @param {TraceablePeerConnection} traceablePeerConnection
379
      * @return {Array<JitsiRemoteTrack>}
386
      * @return {Array<JitsiRemoteTrack>}
380
      */
387
      */
381
     getRemoteTracks(mediaType) {
388
     getRemoteTracks(mediaType) {
382
-        const remoteTracks = [];
383
-        const remoteEndpoints = Object.keys(this.remoteTracks);
384
-
385
-        for (const endpoint of remoteEndpoints) {
386
-            const endpointMediaTypes = Object.keys(this.remoteTracks[endpoint]);
389
+        let remoteTracks = [];
387
 
390
 
388
-            for (const trackMediaType of endpointMediaTypes) {
389
-                // per media type filtering
390
-                if (!mediaType || mediaType === trackMediaType) {
391
-                    const mediaTrack
392
-                        = this.remoteTracks[endpoint][trackMediaType];
391
+        for (const tpc of this.peerConnections.values()) {
392
+            const pcRemoteTracks = tpc.getRemoteTracks(undefined, mediaType);
393
 
393
 
394
-                    if (mediaTrack) {
395
-                        remoteTracks.push(mediaTrack);
396
-                    }
397
-                }
394
+            if (pcRemoteTracks) {
395
+                remoteTracks = remoteTracks.concat(pcRemoteTracks);
398
             }
396
             }
399
         }
397
         }
400
 
398
 
401
         return remoteTracks;
399
         return remoteTracks;
402
     }
400
     }
403
 
401
 
404
-    /**
405
-     * Gets JitsiRemoteTrack for the passed MediaType associated with given MUC
406
-     * nickname (resource part of the JID).
407
-     * @param type audio or video.
408
-     * @param resource the resource part of the MUC JID
409
-     * @returns {JitsiRemoteTrack|null}
410
-     */
411
-    getRemoteTrackByType(type, resource) {
412
-        if (this.remoteTracks[resource]) {
413
-            return this.remoteTracks[resource][type];
414
-        }
415
-
416
-        return null;
417
-
418
-    }
419
-
420
-    /**
421
-     * Gets JitsiRemoteTrack for AUDIO MediaType associated with given MUC
422
-     * nickname (resource part of the JID).
423
-     * @param resource the resource part of the MUC JID
424
-     * @returns {JitsiRemoteTrack|null}
425
-     */
426
-    getRemoteAudioTrack(resource) {
427
-        return this.getRemoteTrackByType(MediaType.AUDIO, resource);
428
-    }
429
-
430
-    /**
431
-     * Gets JitsiRemoteTrack for VIDEO MediaType associated with given MUC
432
-     * nickname (resource part of the JID).
433
-     * @param resource the resource part of the MUC JID
434
-     * @returns {JitsiRemoteTrack|null}
435
-     */
436
-    getRemoteVideoTrack(resource) {
437
-        return this.getRemoteTrackByType(MediaType.VIDEO, resource);
438
-    }
439
-
440
     /**
402
     /**
441
      * Set mute for all local audio streams attached to the conference.
403
      * Set mute for all local audio streams attached to the conference.
442
      * @param value the mute value
404
      * @param value the mute value
469
         this.localTracks.splice(pos, 1);
431
         this.localTracks.splice(pos, 1);
470
     }
432
     }
471
 
433
 
472
-    /* eslint-disable max-params */
473
-
474
-    /**
475
-     * Initializes a new JitsiRemoteTrack instance with the data provided by
476
-     * the signaling layer and SDP.
477
-     *
478
-     * @param {string} ownerEndpointId
479
-     * @param {MediaStream} stream
480
-     * @param {MediaStreamTrack} track
481
-     * @param {MediaType} mediaType
482
-     * @param {VideoType|undefined} videoType
483
-     * @param {string} ssrc
484
-     * @param {boolean} muted
485
-     */
486
-    _createRemoteTrack(
487
-            ownerEndpointId,
488
-            stream,
489
-            track,
490
-            mediaType,
491
-            videoType,
492
-            ssrc,
493
-            muted) {
494
-        const remoteTrack
495
-            = new JitsiRemoteTrack(
496
-                this,
497
-                this.conference,
498
-                ownerEndpointId,
499
-                stream,
500
-                track,
501
-                mediaType,
502
-                videoType,
503
-                ssrc,
504
-                muted);
505
-        const remoteTracks
506
-            = this.remoteTracks[ownerEndpointId]
507
-                || (this.remoteTracks[ownerEndpointId] = {});
508
-
509
-        if (remoteTracks[mediaType]) {
510
-            logger.error(
511
-                'Overwriting remote track!',
512
-                ownerEndpointId,
513
-                mediaType);
514
-        }
515
-        remoteTracks[mediaType] = remoteTrack;
516
-
517
-        this.eventEmitter.emit(RTCEvents.REMOTE_TRACK_ADDED, remoteTrack);
518
-    }
519
-
520
-    /* eslint-enable max-params */
521
-
522
     /**
434
     /**
523
      * Removes all JitsiRemoteTracks associated with given MUC nickname
435
      * Removes all JitsiRemoteTracks associated with given MUC nickname
524
      * (resource part of the JID). Returns array of removed tracks.
436
      * (resource part of the JID). Returns array of removed tracks.
527
      * @returns {JitsiRemoteTrack[]}
439
      * @returns {JitsiRemoteTrack[]}
528
      */
440
      */
529
     removeRemoteTracks(owner) {
441
     removeRemoteTracks(owner) {
530
-        const removedTracks = [];
442
+        let removedTracks = [];
531
 
443
 
532
-        if (this.remoteTracks[owner]) {
533
-            const removedAudioTrack
534
-                = this.remoteTracks[owner][MediaType.AUDIO];
535
-            const removedVideoTrack
536
-                = this.remoteTracks[owner][MediaType.VIDEO];
444
+        for (const tpc of this.peerConnections.values()) {
445
+            const pcRemovedTracks = tpc.removeRemoteTracks(owner);
537
 
446
 
538
-            removedAudioTrack && removedTracks.push(removedAudioTrack);
539
-            removedVideoTrack && removedTracks.push(removedVideoTrack);
540
-
541
-            delete this.remoteTracks[owner];
447
+            removedTracks = removedTracks.concat(pcRemovedTracks);
542
         }
448
         }
543
 
449
 
544
-        return removedTracks;
545
-    }
546
-
547
-    /**
548
-     * Finds remote track by it's stream and track ids.
549
-     * @param {string} streamId the media stream id as defined by the WebRTC
550
-     * @param {string} trackId the media track id as defined by the WebRTC
551
-     * @return {JitsiRemoteTrack|undefined}
552
-     * @private
553
-     */
554
-    _getRemoteTrackById(streamId, trackId) {
555
-        let result;
556
-
557
-        // .find will break the loop once the first match is found
558
-        Object.keys(this.remoteTracks).find(endpoint => {
559
-            const endpointTracks = this.remoteTracks[endpoint];
560
-
561
-            return endpointTracks && Object.keys(endpointTracks).find(
562
-                mediaType => {
563
-                    const mediaTrack = endpointTracks[mediaType];
564
-
565
-                    if (mediaTrack
566
-                        && mediaTrack.getStreamId() === streamId
567
-                        && mediaTrack.getTrackId() === trackId) {
568
-                        result = mediaTrack;
569
-
570
-                        return true;
571
-                    }
572
-
573
-                    return false;
574
-
575
-                });
576
-        });
577
-
578
-        return result;
579
-    }
580
-
581
-    /**
582
-     * Removes <tt>JitsiRemoteTrack</tt> identified by given stream and track
583
-     * ids.
584
-     *
585
-     * @param {string} streamId media stream id as defined by the WebRTC
586
-     * @param {string} trackId media track id as defined by the WebRTC
587
-     * @returns {JitsiRemoteTrack|undefined} the track which has been removed or
588
-     * <tt>undefined</tt> if no track matching given stream and track ids was
589
-     * found.
590
-     */
591
-    _removeRemoteTrack(streamId, trackId) {
592
-        const toBeRemoved = this._getRemoteTrackById(streamId, trackId);
593
-
594
-        if (toBeRemoved) {
595
-            toBeRemoved.dispose();
450
+        logger.debug(
451
+            `Removed remote tracks for ${owner}`
452
+                + ` count: ${removedTracks.length}`);
596
 
453
 
597
-            delete this.remoteTracks[
598
-                toBeRemoved.getParticipantId()][toBeRemoved.getType()];
599
-
600
-            this.eventEmitter.emit(
601
-                RTCEvents.REMOTE_TRACK_REMOVED, toBeRemoved);
602
-        }
603
-
604
-        return toBeRemoved;
454
+        return removedTracks;
605
     }
455
     }
606
 
456
 
607
     /**
457
     /**
762
      * @param resource
612
      * @param resource
763
      * @param audioLevel
613
      * @param audioLevel
764
      */
614
      */
765
-    setAudioLevel(resource, audioLevel) {
766
-        if (!resource) {
615
+    setAudioLevel(ssrc, audioLevel) {
616
+        const track = this._getTrackBySSRC(ssrc);
617
+
618
+        if (!track) {
767
             return;
619
             return;
768
         }
620
         }
769
-        const audioTrack = this.getRemoteAudioTrack(resource);
621
+        if (!track.isAudioTrack()) {
622
+            logger.warn(`Received audio level for non-audio track: ${ssrc}`);
770
 
623
 
771
-        if (audioTrack) {
772
-            audioTrack.setAudioLevel(audioLevel);
624
+            return;
773
         }
625
         }
626
+
627
+        track.setAudioLevel(audioLevel);
774
     }
628
     }
775
 
629
 
776
     /**
630
     /**
779
      * @param ssrc the ssrc to check.
633
      * @param ssrc the ssrc to check.
780
      */
634
      */
781
     getResourceBySSRC(ssrc) {
635
     getResourceBySSRC(ssrc) {
636
+        const track = this._getTrackBySSRC(ssrc);
782
 
637
 
783
-        // FIXME: Convert the SSRCs in whole project to use the same type.
784
-        // Now we are using number and string.
785
-        if (this.getLocalTracks().find(
638
+        return track ? track.getParticipantId() : null;
639
+    }
786
 
640
 
787
-            // eslint-disable-next-line eqeqeq
788
-                localTrack => localTrack.getSSRC() == ssrc)) {
789
-            return this.conference.myUserId();
790
-        }
641
+    /**
642
+     * Finds a track (either local or remote) which runs on the given SSRC.
643
+     * @param {string|number} ssrc
644
+     * @return {JitsiTrack|undefined}
645
+     *
646
+     * FIXME figure out where SSRC is stored as a string and convert to number
647
+     * @private
648
+     */
649
+    _getTrackBySSRC(ssrc) {
650
+        let track
651
+            = this.getLocalTracks().find(
652
+                localTrack =>
791
 
653
 
792
-        const track = this.getRemoteTrackBySSRC(ssrc);
654
+                    // It is important that SSRC is not compared with ===,
655
+                    // because the code calling this method is inconsistent
656
+                    // about string vs number types
657
+                    Array.from(this.peerConnections.values())
658
+                         .find(pc => pc.getLocalSSRC(localTrack) == ssrc) // eslint-disable-line eqeqeq, max-len
659
+                );
793
 
660
 
661
+        if (!track) {
662
+            track = this._getRemoteTrackBySSRC(ssrc);
663
+        }
794
 
664
 
795
-        return track ? track.getParticipantId() : null;
665
+        return track;
796
     }
666
     }
797
 
667
 
798
     /**
668
     /**
801
      * @param ssrc the ssrc to check.
671
      * @param ssrc the ssrc to check.
802
      * @return {JitsiRemoteTrack|undefined} return the first remote track that
672
      * @return {JitsiRemoteTrack|undefined} return the first remote track that
803
      * matches given SSRC or <tt>undefined</tt> if no such track was found.
673
      * matches given SSRC or <tt>undefined</tt> if no such track was found.
674
+     * @private
804
      */
675
      */
805
-    getRemoteTrackBySSRC(ssrc) {
806
-
676
+    _getRemoteTrackBySSRC(ssrc) {
677
+        /* eslint-disable eqeqeq */
807
         // FIXME: Convert the SSRCs in whole project to use the same type.
678
         // FIXME: Convert the SSRCs in whole project to use the same type.
808
         // Now we are using number and string.
679
         // Now we are using number and string.
809
-        // eslint-disable-next-line eqeqeq
810
-        return this.getRemoteTracks().find(t => ssrc == t.getSSRC());
811
-    }
812
-
813
-    /**
814
-     * Handles remote track mute / unmute events.
815
-     * @param type {string} "audio" or "video"
816
-     * @param isMuted {boolean} the new mute state
817
-     * @param from {string} user id
818
-     */
819
-    handleRemoteTrackMute(type, isMuted, from) {
820
-        const track = this.getRemoteTrackByType(type, from);
680
+        return this.getRemoteTracks().find(
681
+            remoteTrack => ssrc == remoteTrack.getSSRC());
821
 
682
 
822
-        if (track) {
823
-            track.setMute(isMuted);
824
-        }
825
-    }
826
-
827
-    /**
828
-     * Handles remote track video type events
829
-     * @param value {string} the new video type
830
-     * @param from {string} user id
831
-     */
832
-    handleRemoteTrackVideoTypeChanged(value, from) {
833
-        const videoTrack = this.getRemoteVideoTrack(from);
834
-
835
-        if (videoTrack) {
836
-            videoTrack._setVideoType(value);
837
-        }
683
+        /* eslint-enable eqeqeq */
838
     }
684
     }
839
 
685
 
840
     /**
686
     /**

+ 20
- 0
modules/RTC/RTCBrowserType.js View File

24
 
24
 
25
     RTC_BROWSER_REACT_NATIVE: 'rtc_browser.react-native',
25
     RTC_BROWSER_REACT_NATIVE: 'rtc_browser.react-native',
26
 
26
 
27
+    /**
28
+     * Tells whether or not the <tt>MediaStream/tt> is removed from
29
+     * the <tt>PeerConnection</tt> and disposed on video mute (in order to turn
30
+     * off the camera device).
31
+     * @return {boolean} <tt>true</tt> if the current browser supports this
32
+     * strategy or <tt>false</tt> otherwise.
33
+     */
34
+    doesVideoMuteByStreamRemove() {
35
+        return !RTCBrowserType.isFirefox();
36
+    },
37
+
27
     /**
38
     /**
28
      * Gets current browser type.
39
      * Gets current browser type.
29
      * @returns {string}
40
      * @returns {string}
102
         return currentBrowser === RTCBrowserType.RTC_BROWSER_ELECTRON;
113
         return currentBrowser === RTCBrowserType.RTC_BROWSER_ELECTRON;
103
     },
114
     },
104
 
115
 
116
+    /**
117
+     * Check whether or not the current browser support peer to peer connections
118
+     * @return {boolean} <tt>true</tt> if p2p is supported or <tt>false</tt>
119
+     * otherwise.
120
+     */
121
+    isP2PSupported() {
122
+        return !RTCBrowserType.isFirefox() && !RTCBrowserType.isReactNative();
123
+    },
124
+
105
     /**
125
     /**
106
      * Checks if current environment is React Native.
126
      * Checks if current environment is React Native.
107
      * @returns {boolean}
127
      * @returns {boolean}

+ 967
- 151
modules/RTC/TraceablePeerConnection.js
File diff suppressed because it is too large
View File


+ 2
- 2
modules/connectivity/ConnectionQuality.js View File

214
 
214
 
215
         conference.room.addListener(
215
         conference.room.addListener(
216
             XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
216
             XMPPEvents.ICE_CONNECTION_STATE_CHANGED,
217
-            newState => {
218
-                if (newState === 'connected') {
217
+            (jingleSession, newState) => {
218
+                if (!jingleSession.isP2P && newState === 'connected') {
219
                     this._timeIceConnected = window.performance.now();
219
                     this._timeIceConnected = window.performance.now();
220
                 }
220
                 }
221
             });
221
             });

+ 30
- 5
modules/connectivity/ParticipantConnectionStatus.js View File

101
             RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
101
             RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
102
             this._onEndpointConnStatusChanged);
102
             this._onEndpointConnStatusChanged);
103
 
103
 
104
+        // Handles P2P status changes
105
+        this._onP2PStatus = this.refreshConnectionStatusForAll.bind(this);
106
+        this.conference.on(JitsiConferenceEvents.P2P_STATUS, this._onP2PStatus);
107
+
104
         // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
108
         // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
105
         // events for video type tracks when they stop receiving data which is
109
         // events for video type tracks when they stop receiving data which is
106
         // often a sign that remote user is having connectivity issues
110
         // often a sign that remote user is having connectivity issues
159
                 this._onRemoteTrackRemoved);
163
                 this._onRemoteTrackRemoved);
160
         }
164
         }
161
 
165
 
162
-        Object.keys(this.trackTimers).forEach(participantId => {
166
+        this.conference.off(
167
+            JitsiConferenceEvents.P2P_STATUS, this._onP2PStatus);
168
+
169
+        const participantIds = Object.keys(this.trackTimers);
170
+
171
+        for (const participantId of participantIds) {
163
             this.clearTimeout(participantId);
172
             this.clearTimeout(participantId);
164
             this.clearRtcMutedTimestamp(participantId);
173
             this.clearRtcMutedTimestamp(participantId);
165
-        });
174
+        }
166
 
175
 
167
         // Clear RTC connection status cache
176
         // Clear RTC connection status cache
168
         this.connStatusFromJvb = {};
177
         this.connStatusFromJvb = {};
321
             && (Date.now() - rtcMutedTimestamp) >= this.rtcMuteTimeout;
330
             && (Date.now() - rtcMutedTimestamp) >= this.rtcMuteTimeout;
322
     }
331
     }
323
 
332
 
333
+    /**
334
+     * Goes over every participant and updates connectivity status.
335
+     * Should be called when a parameter which affects all of the participants
336
+     * is changed (P2P for example).
337
+     */
338
+    refreshConnectionStatusForAll() {
339
+        const participants = this.conference.getParticipants();
340
+
341
+        for (const participant of participants) {
342
+            this.figureOutConnectionStatus(participant.getId());
343
+        }
344
+    }
345
+
324
     /**
346
     /**
325
      * Figures out (and updates) the current connectivity status for
347
      * Figures out (and updates) the current connectivity status for
326
      * the participant identified by the given id.
348
      * the participant identified by the given id.
348
         const isInLastN = this.rtc.isInLastN(id);
370
         const isInLastN = this.rtc.isInLastN(id);
349
         let isConnActiveByJvb = this.connStatusFromJvb[id];
371
         let isConnActiveByJvb = this.connStatusFromJvb[id];
350
 
372
 
351
-        // If no status was received from the JVB it means that it's active
352
-        // (the bridge does not send notification unless there is a problem).
353
-        if (typeof isConnActiveByJvb !== 'boolean') {
373
+        if (this.conference.isP2PActive()) {
374
+            logger.debug('Assuming connection active by JVB - in P2P mode');
375
+            isConnActiveByJvb = true;
376
+        } else if (typeof isConnActiveByJvb !== 'boolean') {
377
+            // If no status was received from the JVB it means that it's active
378
+            // (the bridge does not send notification unless there is a problem)
354
             logger.debug('Assuming connection active by JVB - no notification');
379
             logger.debug('Assuming connection active by JVB - no notification');
355
             isConnActiveByJvb = true;
380
             isConnActiveByJvb = true;
356
         }
381
         }

+ 12
- 7
modules/statistics/RTPStatsCollector.js View File

473
 
473
 
474
             if (!conferenceStatsTransport.some(
474
             if (!conferenceStatsTransport.some(
475
                     t =>
475
                     t =>
476
-                        t.ip === ip
477
-                        && t.type === type
478
-                        && t.localip === localip)) {
479
-                conferenceStatsTransport.push({ ip,
476
+                       t.ip === ip
477
+                       && t.type === type
478
+                       && t.localip === localip)) {
479
+                conferenceStatsTransport.push({
480
+                    ip,
480
                     type,
481
                     type,
481
-                    localip });
482
+                    localip,
483
+                    p2p: this.peerconnection.isP2P
484
+                });
482
             }
485
             }
483
             continue;
486
             continue;
484
         }
487
         }
495
             this.conferenceStats.transport.push({
498
             this.conferenceStats.transport.push({
496
                 ip: `${remote.ipAddress}:${remote.portNumber}`,
499
                 ip: `${remote.ipAddress}:${remote.portNumber}`,
497
                 type: local.transport,
500
                 type: local.transport,
498
-                localip: `${local.ipAddress}:${local.portNumber}`
501
+                localip: `${local.ipAddress}:${local.portNumber}`,
502
+                p2p: this.peerconnection.isP2P
499
             });
503
             });
500
         }
504
         }
501
 
505
 
661
         this
665
         this
662
     );
666
     );
663
 
667
 
664
-    this.eventEmitter.emit(StatisticsEvents.BYTE_SENT_STATS, byteSentStats);
668
+    this.eventEmitter.emit(
669
+        StatisticsEvents.BYTE_SENT_STATS, this.peerconnection, byteSentStats);
665
 
670
 
666
     this.conferenceStats.bitrate
671
     this.conferenceStats.bitrate
667
       = { 'upload': bitrateUpload,
672
       = { 'upload': bitrateUpload,

+ 32
- 8
modules/xmpp/ChatRoom.js View File

534
         // make sure we catch all errors coming from any handler
534
         // make sure we catch all errors coming from any handler
535
         // otherwise we can remove the presence handler from strophe
535
         // otherwise we can remove the presence handler from strophe
536
         try {
536
         try {
537
-            let tagHandler = this.presHandlers[node.tagName];
537
+            let tagHandlers = this.presHandlers[node.tagName];
538
 
538
 
539
             if (node.tagName.startsWith('jitsi_participant_')) {
539
             if (node.tagName.startsWith('jitsi_participant_')) {
540
-                tagHandler = this.participantPropertyListener;
540
+                tagHandlers = [ this.participantPropertyListener ];
541
             }
541
             }
542
 
542
 
543
-            if (tagHandler) {
544
-                tagHandler(node, Strophe.getResourceFromJid(from), from);
543
+            if (tagHandlers) {
544
+                tagHandlers.forEach(handler => {
545
+                    handler(node, Strophe.getResourceFromJid(from), from);
546
+                });
545
             }
547
             }
546
         } catch (e) {
548
         } catch (e) {
547
             GlobalOnErrorHandler.callErrorHandler(e);
549
             GlobalOnErrorHandler.callErrorHandler(e);
890
      * @param handler
892
      * @param handler
891
      */
893
      */
892
     addPresenceListener(name, handler) {
894
     addPresenceListener(name, handler) {
893
-        this.presHandlers[name] = handler;
895
+        if (typeof handler !== 'function') {
896
+            throw new Error('"handler" is not a function');
897
+        }
898
+        let tagHandlers = this.presHandlers[name];
899
+
900
+        if (!tagHandlers) {
901
+            this.presHandlers[name] = tagHandlers = [];
902
+        }
903
+        if (tagHandlers.indexOf(handler) === -1) {
904
+            tagHandlers.push(handler);
905
+        } else {
906
+            logger.warn(
907
+                `Trying to add the same handler more than once for: ${name}`);
908
+        }
894
     }
909
     }
895
 
910
 
896
     /**
911
     /**
897
      *
912
      *
898
      * @param name
913
      * @param name
914
+     * @param handler
899
      */
915
      */
900
-    removePresenceListener(name) {
901
-        delete this.presHandlers[name];
916
+    removePresenceListener(name, handler) {
917
+        const tagHandlers = this.presHandlers[name];
918
+        const handlerIdx = tagHandlers ? tagHandlers.indexOf(handler) : -1;
919
+
920
+        // eslint-disable-next-line no-negated-condition
921
+        if (handlerIdx !== -1) {
922
+            tagHandlers.splice(handlerIdx, 1);
923
+        } else {
924
+            logger.warn(`Handler for: ${name} was not registered`);
925
+        }
902
     }
926
     }
903
 
927
 
904
     /**
928
     /**
953
     /**
977
     /**
954
      *
978
      *
955
      * @param mute
979
      * @param mute
956
-     * @apram callback
980
+     * @param callback
957
      */
981
      */
958
     setAudioMute(mute, callback) {
982
     setAudioMute(mute, callback) {
959
         return this.sendAudioInfoPresence(mute, callback);
983
         return this.sendAudioInfoPresence(mute, callback);

+ 676
- 629
modules/xmpp/JingleSessionPC.js
File diff suppressed because it is too large
View File


+ 19
- 4
modules/xmpp/RtxModifier.js View File

120
 
120
 
121
             return sdpStr;
121
             return sdpStr;
122
         }
122
         }
123
+
124
+        return this.modifyRtxSsrcs2(videoMLine)
125
+            ? sdpTransformer.toRawSDP() : sdpStr;
126
+    }
127
+
128
+    /**
129
+     * Does the same thing as {@link modifyRtxSsrcs}, but takes the
130
+     *  {@link MLineWrap} instance wrapping video media as an argument.
131
+     * @param {MLineWrap} videoMLine
132
+     * @return {boolean} <tt>true</tt> if the SDP wrapped by
133
+     *  {@link SdpTransformWrap} has been modified or <tt>false</tt> otherwise.
134
+     */
135
+    modifyRtxSsrcs2(videoMLine) {
123
         if (videoMLine.direction === 'inactive'
136
         if (videoMLine.direction === 'inactive'
124
-                || videoMLine.direction === 'recvonly') {
137
+            || videoMLine.direction === 'recvonly') {
125
             logger.debug('RtxModifier doing nothing, video '
138
             logger.debug('RtxModifier doing nothing, video '
126
                 + 'm line is inactive or recvonly');
139
                 + 'm line is inactive or recvonly');
127
 
140
 
128
-            return sdpStr;
141
+            return false;
129
         }
142
         }
130
         if (videoMLine.getSSRCCount() < 1) {
143
         if (videoMLine.getSSRCCount() < 1) {
131
             logger.debug('RtxModifier doing nothing, no video ssrcs present');
144
             logger.debug('RtxModifier doing nothing, no video ssrcs present');
132
 
145
 
133
-            return sdpStr;
146
+            return false;
134
         }
147
         }
135
         logger.debug('Current ssrc mapping: ', this.correspondingRtxSsrcs);
148
         logger.debug('Current ssrc mapping: ', this.correspondingRtxSsrcs);
136
         const primaryVideoSsrcs = videoMLine.getPrimaryVideoSSRCs();
149
         const primaryVideoSsrcs = videoMLine.getPrimaryVideoSSRCs();
179
                 correspondingRtxSsrc);
192
                 correspondingRtxSsrc);
180
         }
193
         }
181
 
194
 
182
-        return sdpTransformer.toRawSDP();
195
+        // FIXME we're not looking into much details whether the SDP has been
196
+        // modified or not once the precondition requirements are met.
197
+        return true;
183
     }
198
     }
184
 
199
 
185
     /**
200
     /**

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

92
                 const mySsrcGroup = myMedia.ssrcGroups[i];
92
                 const mySsrcGroup = myMedia.ssrcGroups[i];
93
 
93
 
94
                 if (otherSsrcGroup.semantics === mySsrcGroup.semantics
94
                 if (otherSsrcGroup.semantics === mySsrcGroup.semantics
95
-                    && arrayEquals(otherSsrcGroup.ssrcs,
96
-                                      [ mySsrcGroup.ssrcs ])) {
95
+                    && arrayEquals(otherSsrcGroup.ssrcs, mySsrcGroup.ssrcs)) {
97
 
96
 
98
                     matched = true;
97
                     matched = true;
99
                     break;
98
                     break;

+ 14
- 0
modules/xmpp/SDPUtil.js View File

546
         return sdp.media.find(m => m.type === type);
546
         return sdp.media.find(m => m.type === type);
547
     },
547
     },
548
 
548
 
549
+    /**
550
+     * Extracts the ICE username fragment from an SDP string.
551
+     * @param {string} sdp the SDP in raw text format
552
+     */
553
+    getUfrag(sdp) {
554
+        const ufragLines
555
+            = sdp.split('\n').filter(
556
+                    line => line.startsWith('a=ice-ufrag:'));
557
+
558
+        if (ufragLines.length > 0) {
559
+            return ufragLines[0].substr('a=ice-ufrag:'.length);
560
+        }
561
+    },
562
+
549
     /**
563
     /**
550
      * Sets the given codecName as the preferred codec by
564
      * Sets the given codecName as the preferred codec by
551
      *  moving it to the beginning of the payload types
565
      *  moving it to the beginning of the payload types

+ 57
- 3
modules/xmpp/SdpConsistency.js View File

21
      * Constructor
21
      * Constructor
22
      */
22
      */
23
     constructor() {
23
     constructor() {
24
-        this.clearSsrcCache();
24
+        this.clearVideoSsrcCache();
25
+
26
+        /**
27
+         * Cached audio SSRC.
28
+         * @type {number|null}
29
+         */
30
+        this.cachedAudioSSRC = null;
25
     }
31
     }
26
 
32
 
27
     /**
33
     /**
28
-     * Clear the cached primary and primary rtx ssrcs so that
34
+     * Clear the cached video primary and primary rtx ssrcs so that
29
      *  they will not be used for the next call to
35
      *  they will not be used for the next call to
30
      *  makeVideoPrimarySsrcsConsistent
36
      *  makeVideoPrimarySsrcsConsistent
31
      */
37
      */
32
-    clearSsrcCache() {
38
+    clearVideoSsrcCache() {
33
         this.cachedPrimarySsrc = null;
39
         this.cachedPrimarySsrc = null;
34
     }
40
     }
35
 
41
 
120
 
126
 
121
         return sdpTransformer.toRawSDP();
127
         return sdpTransformer.toRawSDP();
122
     }
128
     }
129
+
130
+    /**
131
+     * Makes sure that audio SSRC is preserved between "detach" and "attach"
132
+     *  operations. The code assumes there can be only 1 audio track added to
133
+     *  the peer connection at a time.
134
+     * @param {string} sdpStr the sdp string to (potentially)
135
+     *  change to make the audio ssrc consistent
136
+     * @returns {string} a (potentially) modified sdp string
137
+     *  with ssrcs consistent with this class' cache
138
+     */
139
+    makeAudioSSRCConsistent(sdpStr) {
140
+        const sdpTransformer = new SdpTransformWrap(sdpStr);
141
+        const audioMLine = sdpTransformer.selectMedia('audio');
142
+
143
+        if (!audioMLine) {
144
+            logger.error(`No 'audio' media found in the sdp: ${sdpStr}`);
145
+
146
+            return sdpStr;
147
+        }
148
+        if (audioMLine.direction === 'inactive') {
149
+            logger.info(
150
+                'Sdp-consistency doing nothing, audio mline is inactive');
151
+
152
+            return sdpStr;
153
+        }
154
+
155
+        const audioSSRCObj = audioMLine.findSSRCByMSID(null);
156
+
157
+        if (audioSSRCObj) {
158
+            if (this.cachedAudioSSRC) {
159
+                const oldSSRC = audioSSRCObj.id;
160
+
161
+                if (oldSSRC !== this.cachedAudioSSRC) {
162
+                    logger.info(
163
+                        `Replacing audio SSRC ${
164
+                            oldSSRC} with ${this.cachedAudioSSRC}`);
165
+                    audioMLine.replaceSSRC(oldSSRC, this.cachedAudioSSRC);
166
+                }
167
+            } else {
168
+                this.cachedAudioSSRC = audioSSRCObj.id;
169
+                logger.info(`Storing audio SSRC: ${this.cachedAudioSSRC}`);
170
+            }
171
+        } else {
172
+            logger.info('Doing nothing - no audio stream in the SDP');
173
+        }
174
+
175
+        return sdpTransformer.toRawSDP();
176
+    }
123
 }
177
 }

+ 31
- 5
modules/xmpp/SdpTransformUtil.js View File

54
     }
54
     }
55
 
55
 
56
     /**
56
     /**
57
+     * Getter for the mLine's "ssrcs" array. If the array was undefined an empty
58
+     * one will be preassigned.
57
      *
59
      *
60
+     * @return {Array<Object>} an array of 'sdp-transform' SSRC attributes
61
+     * objects.
58
      */
62
      */
59
-    get _ssrcs() {
63
+    get ssrcs() {
60
         if (!this.mLine.ssrcs) {
64
         if (!this.mLine.ssrcs) {
61
             this.mLine.ssrcs = [];
65
             this.mLine.ssrcs = [];
62
         }
66
         }
64
         return this.mLine.ssrcs;
68
         return this.mLine.ssrcs;
65
     }
69
     }
66
 
70
 
71
+    /**
72
+     * Setter for the mLine's "ssrcs" array.
73
+     *
74
+     * @param {Array<Object>} ssrcs an array of 'sdp-transform' SSRC attributes
75
+     * objects.
76
+     */
77
+    set ssrcs(ssrcs) {
78
+        this.mLine.ssrcs = ssrcs;
79
+    }
80
+
67
     /**
81
     /**
68
      * Returns the direction of the underlying media description.
82
      * Returns the direction of the underlying media description.
69
      * @return {string} the media direction name as defined in the SDP.
83
      * @return {string} the media direction name as defined in the SDP.
110
      * <tt>undefined</tt> if no such attribute exists.
124
      * <tt>undefined</tt> if no such attribute exists.
111
      */
125
      */
112
     getSSRCAttrValue(ssrcNumber, attrName) {
126
     getSSRCAttrValue(ssrcNumber, attrName) {
113
-        const attribute = this._ssrcs.find(
127
+        const attribute = this.ssrcs.find(
114
             ssrcObj => ssrcObj.id === ssrcNumber
128
             ssrcObj => ssrcObj.id === ssrcNumber
115
             && ssrcObj.attribute === attrName);
129
             && ssrcObj.attribute === attrName);
116
 
130
 
138
      * the 'sdp-transform' lib.
152
      * the 'sdp-transform' lib.
139
      */
153
      */
140
     addSSRCAttribute(ssrcObj) {
154
     addSSRCAttribute(ssrcObj) {
141
-        this._ssrcs.push(ssrcObj);
155
+        this.ssrcs.push(ssrcObj);
142
     }
156
     }
143
 
157
 
144
     /**
158
     /**
179
                 && parsePrimarySSRC(group) === primarySSRC);
193
                 && parsePrimarySSRC(group) === primarySSRC);
180
     }
194
     }
181
 
195
 
196
+    /**
197
+     * @param {string|null} msid the media stream id or <tt>null</tt> to match
198
+     * the first SSRC object with any 'msid' value.
199
+     * @return {Object|undefined} the SSRC object as defined by 'sdp-transform'
200
+     * lib.
201
+     */
202
+    findSSRCByMSID(msid) {
203
+        return this.ssrcs.find(
204
+            ssrcObj => ssrcObj.attribute === 'msid'
205
+                && (msid === null || ssrcObj.value === msid));
206
+    }
207
+
182
     /**
208
     /**
183
      * Gets the SSRC count for the underlying media description.
209
      * Gets the SSRC count for the underlying media description.
184
      * @return {number}
210
      * @return {number}
212
         const numSsrcs = _getSSRCCount(this.mLine);
238
         const numSsrcs = _getSSRCCount(this.mLine);
213
 
239
 
214
         if (numSsrcs === 1) {
240
         if (numSsrcs === 1) {
215
-            // Not using _ssrcs on purpose here
241
+            // Not using "ssrcs" getter on purpose here
216
             return this.mLine.ssrcs[0].id;
242
             return this.mLine.ssrcs[0].id;
217
         }
243
         }
218
 
244
 
252
      * @return {Array.<number>} an array with all SSRC as numbers.
278
      * @return {Array.<number>} an array with all SSRC as numbers.
253
      */
279
      */
254
     getSSRCs() {
280
     getSSRCs() {
255
-        return this._ssrcs
281
+        return this.ssrcs
256
             .map(ssrcInfo => ssrcInfo.id)
282
             .map(ssrcInfo => ssrcInfo.id)
257
             .filter((ssrc, index, array) => array.indexOf(ssrc) === index);
283
             .filter((ssrc, index, array) => array.indexOf(ssrc) === index);
258
     }
284
     }

+ 104
- 0
modules/xmpp/SignalingLayerImpl.js View File

1
+/* global __filename */
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+import * as MediaType from '../../service/RTC/MediaType';
5
+import * as SignalingEvents from '../../service/RTC/SignalingEvents';
6
+import SignalingLayer from '../../service/RTC/SignalingLayer';
7
+
8
+const logger = getLogger(__filename);
9
+
10
+/**
11
+ * Default XMPP implementation of the {@link SignalingLayer} interface. Obtains
12
+ * the data from the MUC presence.
13
+ */
14
+export default class SignalingLayerImpl extends SignalingLayer {
15
+    /**
16
+     * Creates new instance.
17
+     */
18
+    constructor() {
19
+        super();
20
+
21
+        /**
22
+         * A map that stores SSRCs of remote streams. And is used only locally
23
+         * We store the mapping when jingle is received, and later is used
24
+         * onaddstream webrtc event where we have only the ssrc
25
+         * FIXME: This map got filled and never cleaned and can grow during long
26
+         * conference
27
+         * @type {Map<string, string>} maps SSRC number to jid
28
+         */
29
+        this.ssrcOwners = new Map();
30
+
31
+        /**
32
+         *
33
+         * @type {ChatRoom|null}
34
+         */
35
+        this.chatRoom = null;
36
+    }
37
+
38
+    /**
39
+     * Sets the <tt>ChatRoom</tt> instance used and binds presence listeners.
40
+     * @param {ChatRoom} room
41
+     */
42
+    setChatRoom(room) {
43
+        const oldChatRoom = this.chatRoom;
44
+
45
+        this.chatRoom = room;
46
+        if (oldChatRoom) {
47
+            oldChatRoom.removePresenceListener(
48
+                'audiomuted', this._audioMuteHandler);
49
+            oldChatRoom.removePresenceListener(
50
+                'videomuted', this._videoMuteHandler);
51
+            oldChatRoom.removePresenceListener(
52
+                'videoType', this._videoTypeHandler);
53
+        }
54
+        if (room) {
55
+            // SignalingEvents
56
+            this._audioMuteHandler = (node, from) => {
57
+                this.eventEmitter.emit(
58
+                    SignalingEvents.PEER_MUTED_CHANGED,
59
+                    from, MediaType.AUDIO, node.value === 'true');
60
+            };
61
+            room.addPresenceListener('audiomuted', this._audioMuteHandler);
62
+
63
+            this._videoMuteHandler = (node, from) => {
64
+                this.eventEmitter.emit(
65
+                    SignalingEvents.PEER_MUTED_CHANGED,
66
+                    from, MediaType.VIDEO, node.value === 'true');
67
+            };
68
+            room.addPresenceListener('videomuted', this._videoMuteHandler);
69
+
70
+            this._videoTypeHandler = (node, from) => {
71
+                this.eventEmitter.emit(
72
+                    SignalingEvents.PEER_VIDEO_TYPE_CHANGED,
73
+                    from, node.value);
74
+            };
75
+            room.addPresenceListener('videoType', this._videoTypeHandler);
76
+        }
77
+    }
78
+
79
+    /**
80
+     * @inheritDoc
81
+     */
82
+    getPeerMediaInfo(owner, mediaType) {
83
+        if (this.chatRoom) {
84
+            return this.chatRoom.getMediaPresenceInfo(owner, mediaType);
85
+        }
86
+        logger.error('Requested peer media info, before room was set');
87
+    }
88
+
89
+    /**
90
+     * @inheritDoc
91
+     */
92
+    getSSRCOwner(ssrc) {
93
+        return this.ssrcOwners.get(ssrc);
94
+    }
95
+
96
+    /**
97
+     * Set an SSRC owner.
98
+     * @param {string} ssrc an SSRC to be owned
99
+     * @param {string} endpointId owner's ID (MUC nickname)
100
+     */
101
+    setSSRCOwner(ssrc, endpointId) {
102
+        this.ssrcOwners.set(ssrc, endpointId);
103
+    }
104
+}

+ 70
- 12
modules/xmpp/strophe.jingle.js View File

1
-/* global $, $iq, Strophe */
1
+/* global $, $iq, __filename, Strophe */
2
 
2
 
3
 import { getLogger } from 'jitsi-meet-logger';
3
 import { getLogger } from 'jitsi-meet-logger';
4
 const logger = getLogger(__filename);
4
 const logger = getLogger(__filename);
5
 
5
 
6
 import JingleSessionPC from './JingleSessionPC';
6
 import JingleSessionPC from './JingleSessionPC';
7
+import * as JingleSessionState from './JingleSessionState';
7
 import XMPPEvents from '../../service/xmpp/XMPPEvents';
8
 import XMPPEvents from '../../service/xmpp/XMPPEvents';
8
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
9
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
10
+import RandomUtil from '../util/RandomUtil';
9
 import Statistics from '../statistics/statistics';
11
 import Statistics from '../statistics/statistics';
10
 import ConnectionPlugin from './ConnectionPlugin';
12
 import ConnectionPlugin from './ConnectionPlugin';
11
 
13
 
18
  */
20
  */
19
 class JingleConnectionPlugin extends ConnectionPlugin {
21
 class JingleConnectionPlugin extends ConnectionPlugin {
20
     /**
22
     /**
21
-     *
22
-     * @param xmpp
23
-     * @param eventEmitter
23
+     * Creates new <tt>JingleConnectionPlugin</tt>
24
+     * @param {XMPP} xmpp
25
+     * @param {EventEmitter} eventEmitter
26
+     * @param {Array<Object>} p2pStunServers an array which is part of the ice
27
+     * config passed to the <tt>PeerConnection</tt> with the structure defined
28
+     * by the WebRTC standard.
24
      */
29
      */
25
-    constructor(xmpp, eventEmitter) {
30
+    constructor(xmpp, eventEmitter, p2pStunServers) {
26
         super();
31
         super();
27
         this.xmpp = xmpp;
32
         this.xmpp = xmpp;
28
         this.eventEmitter = eventEmitter;
33
         this.eventEmitter = eventEmitter;
29
         this.sessions = {};
34
         this.sessions = {};
30
-        this.iceConfig = { iceServers: [] };
35
+        this.jvbIceConfig = { iceServers: [ ] };
36
+        this.p2pIceConfig = { iceServers: [ ] };
37
+        if (Array.isArray(p2pStunServers)) {
38
+            logger.info('Configured STUN servers: ', p2pStunServers);
39
+            this.p2pIceConfig.iceServers = p2pStunServers;
40
+        }
31
         this.mediaConstraints = {
41
         this.mediaConstraints = {
32
             mandatory: {
42
             mandatory: {
33
                 'OfferToReceiveAudio': true,
43
                 'OfferToReceiveAudio': true,
128
                 const videoMuted = startMuted.attr('video');
138
                 const videoMuted = startMuted.attr('video');
129
 
139
 
130
                 this.eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
140
                 this.eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
131
-                            audioMuted === 'true', videoMuted === 'true');
141
+                        audioMuted === 'true', videoMuted === 'true');
132
             }
142
             }
143
+
144
+            // FIXME that should work most of the time, but we'd have to
145
+            // think how secure it is to assume that user with "focus"
146
+            // nickname is Jicofo.
147
+            const isP2P = Strophe.getResourceFromJid(fromJid) !== 'focus';
148
+
149
+            logger.info(
150
+                `Marking session from ${fromJid
151
+                } as ${isP2P ? '' : '*not*'} P2P`);
133
             sess = new JingleSessionPC(
152
             sess = new JingleSessionPC(
134
                         $(iq).find('jingle').attr('sid'),
153
                         $(iq).find('jingle').attr('sid'),
135
                         $(iq).attr('to'),
154
                         $(iq).attr('to'),
136
                         fromJid,
155
                         fromJid,
137
                         this.connection,
156
                         this.connection,
138
                         this.mediaConstraints,
157
                         this.mediaConstraints,
139
-                        this.iceConfig, this.xmpp.options);
158
+                        isP2P ? this.p2pIceConfig : this.jvbIceConfig,
159
+                        isP2P /* P2P */,
160
+                        false /* initiator */,
161
+                        this.xmpp.options);
140
 
162
 
141
             this.sessions[sess.sid] = sess;
163
             this.sessions[sess.sid] = sess;
142
 
164
 
146
                     'xmpp.session-initiate', { value: now });
168
                     'xmpp.session-initiate', { value: now });
147
             break;
169
             break;
148
         }
170
         }
171
+        case 'session-accept': {
172
+            this.eventEmitter.emit(
173
+                XMPPEvents.CALL_ACCEPTED, sess, $(iq).find('>jingle'));
174
+            break;
175
+        }
176
+        case 'transport-info': {
177
+            this.eventEmitter.emit(
178
+                XMPPEvents.TRANSPORT_INFO, sess, $(iq).find('>jingle'));
179
+            break;
180
+        }
149
         case 'session-terminate': {
181
         case 'session-terminate': {
150
             logger.log('terminating...', sess.sid);
182
             logger.log('terminating...', sess.sid);
151
             let reasonCondition = null;
183
             let reasonCondition = null;
156
                     = $(iq).find('>jingle>reason>:first')[0].tagName;
188
                     = $(iq).find('>jingle>reason>:first')[0].tagName;
157
                 reasonText = $(iq).find('>jingle>reason>text').text();
189
                 reasonText = $(iq).find('>jingle>reason>text').text();
158
             }
190
             }
191
+            sess.state = JingleSessionState.ENDED;
159
             this.terminate(sess.sid, reasonCondition, reasonText);
192
             this.terminate(sess.sid, reasonCondition, reasonText);
160
             this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
193
             this.eventEmitter.emit(XMPPEvents.CALL_ENDED,
161
-                    sess, reasonCondition, reasonText);
194
+                sess, reasonCondition, reasonText);
162
             break;
195
             break;
163
         }
196
         }
164
         case 'transport-replace':
197
         case 'transport-replace':
202
         return true;
235
         return true;
203
     }
236
     }
204
 
237
 
238
+    /**
239
+     * Creates new <tt>JingleSessionPC</tt> meant to be used in a direct P2P
240
+     * connection, configured as 'initiator'.
241
+     * @param {string} me our JID
242
+     * @param {string} peer remote participant's JID
243
+     * @return {JingleSessionPC}
244
+     */
245
+    newP2PJingleSession(me, peer) {
246
+        const sess
247
+            = new JingleSessionPC(
248
+                    RandomUtil.randomHexString(12),
249
+                    me,
250
+                    peer,
251
+                    this.connection,
252
+                    this.mediaConstraints,
253
+                    this.p2pIceConfig,
254
+                    true /* P2P */,
255
+                    true /* initiator */,
256
+                    this.xmpp.options);
257
+
258
+        this.sessions[sess.sid] = sess;
259
+
260
+        return sess;
261
+    }
262
+
205
     /**
263
     /**
206
      *
264
      *
207
      * @param sid
265
      * @param sid
296
                     }
354
                     }
297
                     }
355
                     }
298
                 });
356
                 });
299
-                this.iceConfig.iceServers = iceservers;
357
+                this.jvbIceConfig.iceServers = iceservers;
300
             }, err => {
358
             }, err => {
301
                 logger.warn('getting turn credentials failed', err);
359
                 logger.warn('getting turn credentials failed', err);
302
                 logger.warn('is mod_turncredentials or similar installed?');
360
                 logger.warn('is mod_turncredentials or similar installed?');
331
 
389
 
332
 /* eslint-enable newline-per-chained-call */
390
 /* eslint-enable newline-per-chained-call */
333
 
391
 
334
-module.exports = function(XMPP, eventEmitter) {
392
+module.exports = function(XMPP, eventEmitter, p2pStunServers) {
335
     Strophe.addConnectionPlugin(
393
     Strophe.addConnectionPlugin(
336
         'jingle',
394
         'jingle',
337
-        new JingleConnectionPlugin(XMPP, eventEmitter));
395
+        new JingleConnectionPlugin(XMPP, eventEmitter, p2pStunServers));
338
 };
396
 };

+ 5
- 3
modules/xmpp/xmpp.js View File

36
  */
36
  */
37
 export default class XMPP extends Listenable {
37
 export default class XMPP extends Listenable {
38
     /**
38
     /**
39
-     *
40
-     * @param options
39
+     * FIXME describe all options
40
+     * @param {Object} options
41
+     * @param {Array<Object>} options.p2pStunServers see
42
+     * {@link JingleConnectionPlugin} for more details.
41
      * @param token
43
      * @param token
42
      */
44
      */
43
     constructor(options, token) {
45
     constructor(options, token) {
442
      */
444
      */
443
     _initStrophePlugins() {
445
     _initStrophePlugins() {
444
         initEmuc(this);
446
         initEmuc(this);
445
-        initJingle(this, this.eventEmitter);
447
+        initJingle(this, this.eventEmitter, this.options.p2pStunServers);
446
         initStropheUtil();
448
         initStropheUtil();
447
         initPing(this);
449
         initPing(this);
448
         initRayo();
450
         initRayo();

+ 21
- 19
service/RTC/RTCEvents.js View File

10
      */
10
      */
11
     CREATE_OFFER_FAILED: 'rtc.create_offer_failed',
11
     CREATE_OFFER_FAILED: 'rtc.create_offer_failed',
12
     RTC_READY: 'rtc.ready',
12
     RTC_READY: 'rtc.ready',
13
-
14
-    /**
15
-     * FIXME: rename to something closer to "local streams SDP changed"
16
-     * Indicates that the local sendrecv streams in local SDP are changed.
17
-     */
18
-    SENDRECV_STREAMS_CHANGED: 'rtc.sendrecv_streams_changed',
19
     DATA_CHANNEL_OPEN: 'rtc.data_channel_open',
13
     DATA_CHANNEL_OPEN: 'rtc.data_channel_open',
20
     ENDPOINT_CONN_STATUS_CHANGED: 'rtc.endpoint_conn_status_changed',
14
     ENDPOINT_CONN_STATUS_CHANGED: 'rtc.endpoint_conn_status_changed',
21
     LASTN_CHANGED: 'rtc.lastn_changed',
15
     LASTN_CHANGED: 'rtc.lastn_changed',
26
 
20
 
27
     /**
21
     /**
28
      * Event fired when we remote track is added to the conference.
22
      * Event fired when we remote track is added to the conference.
29
-     * The following structure is passed as an argument:
30
-     * {
31
-     *   stream: the WebRTC MediaStream instance
32
-     *   track: the WebRTC MediaStreamTrack
33
-     *   mediaType: the MediaType instance
34
-     *   owner: the MUC JID of the stream owner
35
-     *   muted: a boolean indicating initial 'muted' status of the track or
36
-      *         'null' if unknown
23
+     * 1st event argument is the added <tt>JitsiRemoteTrack</tt> instance.
37
      **/
24
      **/
38
     REMOTE_TRACK_ADDED: 'rtc.remote_track_added',
25
     REMOTE_TRACK_ADDED: 'rtc.remote_track_added',
39
 
26
 
43
 
30
 
44
     /**
31
     /**
45
      * Indicates that the remote track has been removed from the conference.
32
      * Indicates that the remote track has been removed from the conference.
46
-     * 1st event argument is the ID of the parent WebRTC stream to which
47
-     * the track being removed belongs to.
48
-     * 2nd event argument is the ID of the removed track.
33
+     * 1st event argument is the removed {@link JitsiRemoteTrack} instance.
49
      */
34
      */
50
     REMOTE_TRACK_REMOVED: 'rtc.remote_track_removed',
35
     REMOTE_TRACK_REMOVED: 'rtc.remote_track_removed',
51
 
36
 
70
      * Indicates that a message from another participant is received on
55
      * Indicates that a message from another participant is received on
71
      * data channel.
56
      * data channel.
72
      */
57
      */
73
-    ENDPOINT_MESSAGE_RECEIVED:
74
-        'rtc.endpoint_message_received'
58
+    ENDPOINT_MESSAGE_RECEIVED: 'rtc.endpoint_message_received',
59
+
60
+    /**
61
+     * Designates an event indicating that the local ICE username fragment of
62
+     * the jingle session has changed.
63
+     * The first argument of the vent is <tt>TraceablePeerConnection</tt> which
64
+     * is the source of the event.
65
+     * The second argument is the actual "ufrag" string.
66
+     */
67
+    LOCAL_UFRAG_CHANGED: 'rtc.local_ufrag_changed',
68
+
69
+    /**
70
+     * Designates an event indicating that the local ICE username fragment of
71
+     * the jingle session has changed.
72
+     * The first argument of the vent is <tt>TraceablePeerConnection</tt> which
73
+     * is the source of the event.
74
+     * The second argument is the actual "ufrag" string.
75
+     */
76
+    REMOTE_UFRAG_CHANGED: 'rtc.remote_ufrag_changed'
75
 };
77
 };
76
 
78
 
77
 module.exports = RTCEvents;
79
 module.exports = RTCEvents;

+ 14
- 0
service/RTC/SignalingEvents.js View File

1
+/**
2
+ * Event triggered when participant's muted status changes.
3
+ * @param {string} endpointId the track owner's identifier (MUC nickname)
4
+ * @param {MediaType} mediaType "audio" or "video"
5
+ * @param {boolean} isMuted the new muted state
6
+ */
7
+export const PEER_MUTED_CHANGED = 'signaling.peerMuted';
8
+
9
+/**
10
+ * Event triggered when participant's video type changes.
11
+ * @param {string} endpointId the video owner's ID (MUC nickname)
12
+ * @param {VideoType} videoType the new value
13
+ */
14
+export const PEER_VIDEO_TYPE_CHANGED = 'signaling.peerVideoType';

+ 5
- 3
service/RTC/SignalingLayer.js View File

1
 
1
 
2
+import Listenable from '../../modules/util/Listenable';
3
+
2
 /**
4
 /**
3
  * An object that carries the info about specific media type advertised by
5
  * An object that carries the info about specific media type advertised by
4
- * participant in the signalling channel.
6
+ * participant in the signaling channel.
5
  * @typedef {Object} PeerMediaInfo
7
  * @typedef {Object} PeerMediaInfo
6
  * @property {boolean} muted indicates if the media is currently muted
8
  * @property {boolean} muted indicates if the media is currently muted
7
  * @property {VideoType|undefined} videoType the type of the video if applicable
9
  * @property {VideoType|undefined} videoType the type of the video if applicable
8
  */
10
  */
9
 
11
 
10
 /**
12
 /**
11
- * Interface used to expose the information carried over the signalling channel
13
+ * Interface used to expose the information carried over the signaling channel
12
  * which is not available to the RTC module in the media SDP.
14
  * which is not available to the RTC module in the media SDP.
13
  *
15
  *
14
  * @interface SignalingLayer
16
  * @interface SignalingLayer
15
  */
17
  */
16
-export default class SignalingLayer {
18
+export default class SignalingLayer extends Listenable {
17
 
19
 
18
     /**
20
     /**
19
      * Obtains the endpoint ID for given SSRC.
21
      * Obtains the endpoint ID for given SSRC.

+ 15
- 7
service/xmpp/XMPPEvents.js View File

10
     AUTHENTICATION_REQUIRED: 'xmpp.authentication_required',
10
     AUTHENTICATION_REQUIRED: 'xmpp.authentication_required',
11
     BRIDGE_DOWN: 'xmpp.bridge_down',
11
     BRIDGE_DOWN: 'xmpp.bridge_down',
12
 
12
 
13
+    /**
14
+     * Triggered when 'session-accept' is received from the responder.
15
+     */
16
+    CALL_ACCEPTED: 'xmpp.callaccepted.jingle',
17
+
13
     // Designates an event indicating that an offer (e.g. Jingle
18
     // Designates an event indicating that an offer (e.g. Jingle
14
     // session-initiate) was received.
19
     // session-initiate) was received.
15
     CALL_INCOMING: 'xmpp.callincoming.jingle',
20
     CALL_INCOMING: 'xmpp.callincoming.jingle',
22
     CHAT_ERROR_RECEIVED: 'xmpp.chat_error_received',
27
     CHAT_ERROR_RECEIVED: 'xmpp.chat_error_received',
23
     CONFERENCE_SETUP_FAILED: 'xmpp.conference_setup_failed',
28
     CONFERENCE_SETUP_FAILED: 'xmpp.conference_setup_failed',
24
 
29
 
30
+    /**
31
+     * This event is triggered when the ICE connects for the first time.
32
+     */
33
+    CONNECTION_ESTABLISHED: 'xmpp.connection.connected',
34
+
25
     // Designates an event indicating that the connection to the XMPP server
35
     // Designates an event indicating that the connection to the XMPP server
26
     // failed.
36
     // failed.
27
     CONNECTION_FAILED: 'xmpp.connection.failed',
37
     CONNECTION_FAILED: 'xmpp.connection.failed',
181
     // changed.
191
     // changed.
182
     SUBJECT_CHANGED: 'xmpp.subject_changed',
192
     SUBJECT_CHANGED: 'xmpp.subject_changed',
183
 
193
 
194
+    // FIXME: how does it belong to XMPP ? - it's detected by the PeerConnection
184
     // suspending detected
195
     // suspending detected
185
     SUSPEND_DETECTED: 'xmpp.suspend_detected',
196
     SUSPEND_DETECTED: 'xmpp.suspend_detected',
186
 
197
 
187
-    // Designates an event indicating that the local ICE username fragment of
188
-    // the jingle session has changed.
189
-    LOCAL_UFRAG_CHANGED: 'xmpp.local_ufrag_changed',
190
-
191
-    // Designates an event indicating that the local ICE username fragment of
192
-    // the jingle session has changed.
193
-    REMOTE_UFRAG_CHANGED: 'xmpp.remote_ufrag_changed',
198
+    /**
199
+     * Event fired when 'transport-info' with new ICE candidates is received.
200
+     */
201
+    TRANSPORT_INFO: 'xmpp.transportinfo.jingle',
194
 
202
 
195
     // Designates an event indicating that the local ICE connection state has
203
     // Designates an event indicating that the local ICE connection state has
196
     // changed.
204
     // changed.

Loading…
Cancel
Save