Bläddra i källkod

Delayed ice candidates (#434)

* ref(JingleSessionPC): add local tracks with offer/answer

Refactor the current code to add local tracks together with initial
offer/answer to make this atomic/single operation on
the modificationQueue.

This is required to be able to get rid of 'delayedIceCandidates' list
and to execute 'addIceCandidate' task on the queue. If local track
addition is not bundled with initial offer it will often happen that ICE
candidates are queued, before the offer/answer which is queued only once
the local tracks task is done.

* ref(JSPC): use queue for ICE candidates

Refactors the code to get rid of 'delayedIceCandidates' buffer and
use the modification queue to synchronise with the initial offer/answer.

* doc(JingleSessionPC): improve docs
dev1
Paweł Domas 8 år sedan
förälder
incheckning
e821d0cc41
2 ändrade filer med 112 tillägg och 132 borttagningar
  1. 24
    41
      JitsiConference.js
  2. 88
    91
      modules/xmpp/JingleSessionPC.js

+ 24
- 41
JitsiConference.js Visa fil

@@ -1296,22 +1296,23 @@ JitsiConference.prototype.onIncomingCall
1296 1296
 
1297 1297
     // Add local tracks to the session
1298 1298
     try {
1299
-        jingleSession.addLocalTracks(this.getLocalTracks()).then(() => {
1300
-            jingleSession.acceptOffer(jingleOffer, null,
1301
-                error => {
1302
-                    GlobalOnErrorHandler.callErrorHandler(error);
1303
-                    logger.error(
1304
-                        'Failed to accept incoming Jingle session', error);
1305
-                }
1306
-            );
1307
-
1308
-            // Start callstats as soon as peerconnection is initialized,
1309
-            // do not wait for XMPPEvents.PEERCONNECTION_READY, as it may never
1310
-            // happen in case if user doesn't have or denied permission to
1311
-            // both camera and microphone.
1312
-            this.statistics.startCallStats(jingleSession);
1313
-            this._startRemoteStats();
1314
-        });
1299
+        jingleSession.acceptOffer(
1300
+            jingleOffer,
1301
+            null /* success */,
1302
+            error => {
1303
+                GlobalOnErrorHandler.callErrorHandler(error);
1304
+                logger.error(
1305
+                    'Failed to accept incoming Jingle session', error);
1306
+            },
1307
+            this.getLocalTracks()
1308
+        );
1309
+
1310
+        // Start callstats as soon as peerconnection is initialized,
1311
+        // do not wait for XMPPEvents.PEERCONNECTION_READY, as it may never
1312
+        // happen in case if user doesn't have or denied permission to
1313
+        // both camera and microphone.
1314
+        this.statistics.startCallStats(jingleSession);
1315
+        this._startRemoteStats();
1315 1316
     } catch (e) {
1316 1317
         GlobalOnErrorHandler.callErrorHandler(e);
1317 1318
         logger.error(e);
@@ -1883,25 +1884,16 @@ JitsiConference.prototype._acceptP2PIncomingCall
1883 1884
 
1884 1885
     const localTracks = this.getLocalTracks();
1885 1886
 
1886
-    logger.debug(`Adding ${localTracks} to P2P...`);
1887
-    this.p2pJingleSession.addLocalTracks(localTracks).then(
1887
+    this.p2pJingleSession.acceptOffer(
1888
+        jingleOffer,
1888 1889
         () => {
1889
-            logger.debug(`Add ${localTracks} to P2P done!`);
1890
-            this.p2pJingleSession.acceptOffer(
1891
-                jingleOffer,
1892
-                () => {
1893
-                    logger.debug('Got RESULT for P2P "session-accept"');
1894
-                },
1895
-                error => {
1896
-                    logger.error(
1897
-                        'Failed to accept incoming P2P Jingle session', error);
1898
-                }
1899
-            );
1890
+            logger.debug('Got RESULT for P2P "session-accept"');
1900 1891
         },
1901 1892
         error => {
1902 1893
             logger.error(
1903
-                `Failed to add ${localTracks} to the P2P connection`, error);
1904
-        });
1894
+                'Failed to accept incoming P2P Jingle session', error);
1895
+        },
1896
+        localTracks);
1905 1897
 };
1906 1898
 
1907 1899
 /**
@@ -2106,16 +2098,7 @@ JitsiConference.prototype._startP2PSession = function(peerJid) {
2106 2098
     // immediately once the P2P ICE connects.
2107 2099
     const localTracks = this.getLocalTracks();
2108 2100
 
2109
-    logger.info(`Adding ${localTracks} to P2P...`);
2110
-    this.p2pJingleSession.addLocalTracks(localTracks).then(
2111
-        () => {
2112
-            logger.info(`Added ${localTracks} to P2P`);
2113
-            logger.info('About to send P2P \'session-initiate\'...');
2114
-            this.p2pJingleSession.invite();
2115
-        },
2116
-        error => {
2117
-            logger.error(`Failed to add ${localTracks} to P2P`, error);
2118
-        });
2101
+    this.p2pJingleSession.invite(localTracks);
2119 2102
 };
2120 2103
 
2121 2104
 /**

+ 88
- 91
modules/xmpp/JingleSessionPC.js Visa fil

@@ -69,16 +69,6 @@ export default class JingleSessionPC extends JingleSession {
69 69
             options) {
70 70
         super(sid, me, peerjid, connection, mediaConstraints, iceConfig);
71 71
 
72
-        /**
73
-         * Stores "delayed" ICE candidates which are added to the PC once
74
-         * the first sRD/sLD cycle is done. If there was at least one sRD/sLD
75
-         * cycle already then the candidates are added as they come and this
76
-         * queue is skipped.
77
-         * @type {Array} an array of ICE candidate lines which can be added
78
-         * directly to the PC
79
-         */
80
-        this.delayedIceCandidiates = [];
81
-
82 72
         this.lasticecandidate = false;
83 73
         this.closed = false;
84 74
 
@@ -163,27 +153,6 @@ export default class JingleSessionPC extends JingleSession {
163 153
         return true;
164 154
     }
165 155
 
166
-    /**
167
-     * Adds all "delayed" ICE candidates to the PeerConnection.
168
-     * @private
169
-     */
170
-    _dequeIceCandidates() {
171
-        this.delayedIceCandidiates.forEach(candidate => {
172
-            const line = candidate.candidate;
173
-
174
-            this.peerconnection.addIceCandidate(
175
-                candidate,
176
-                () => {
177
-                    logger.debug(`Add ICE candidate OK ${this}: ${line}`);
178
-                },
179
-                error => {
180
-                    logger.error(
181
-                        `Add ICE candidate failed ${this}: ${line}`, error);
182
-                });
183
-        });
184
-        this.delayedIceCandidiates = [];
185
-    }
186
-
187 156
     /**
188 157
      * Finds all "source" elements under RTC "description" in given Jingle IQ
189 158
      * and adds 'ssrc-info' with the owner attribute set to
@@ -505,41 +474,69 @@ export default class JingleSessionPC extends JingleSession {
505 474
             return;
506 475
         }
507 476
 
508
-        // NOTE operates on each content element, can't use () =>
509
-        elem.each((contentIdx, content) => {
510
-            $(content).find('transport>candidate')
477
+        const iceCandidates = [];
478
+
479
+        elem.find('>content>transport>candidate')
511 480
             .each((idx, candidate) => {
512 481
                 let line = SDPUtil.candidateFromJingle(candidate);
513 482
 
514 483
                 line = line.replace('\r\n', '').replace('a=', '');
515 484
 
516
-                // FIXME this code does not care to handle non-bundle transport
485
+                // FIXME this code does not care to handle
486
+                // non-bundle transport
517 487
                 const rtcCandidate = new RTCIceCandidate({
518 488
                     sdpMLineIndex: 0,
519 489
 
520 490
                     // FF comes up with more complex names like audio-23423,
521 491
                     // Given that it works on both Chrome and FF without
522
-                    // providing it, let's leave it like this for the time being
492
+                    // providing it, let's leave it like this for the time
493
+                    // being...
523 494
                     // sdpMid: 'audio',
524 495
                     candidate: line
525 496
                 });
526 497
 
527
-                // Will delay the addition until the remoteDescription is set
528
-                if (this.peerconnection.remoteDescription.sdp) {
529
-                    logger.debug(`Trying to add ICE candidate: ${line}`);
530
-                    this.peerconnection.addIceCandidate(
531
-                        rtcCandidate,
532
-                        () => logger.debug(`addIceCandidate ok: ${line}`),
533
-                        error => {
534
-                            logger.error(
535
-                                `addIceCandidate failed: ${line}`, error);
536
-                        });
537
-                } else {
538
-                    logger.debug(`Delaying ICE candidate: ${line}`);
539
-                    this.delayedIceCandidiates.push(rtcCandidate);
540
-                }
498
+                iceCandidates.push(rtcCandidate);
499
+            });
500
+
501
+        if (!iceCandidates.length) {
502
+            logger.error(
503
+                'No ICE candidates to add ?', elem[0] && elem[0].outerHTML);
504
+
505
+            return;
506
+        }
507
+
508
+        // We want to have this task queued, so that we know it is executed,
509
+        // after the initial sRD/sLD offer/answer cycle was done (based on
510
+        // the assumption that candidates are spawned after the offer/answer
511
+        // and XMPP preserves order).
512
+        const workFunction = () => {
513
+            for (const iceCandidate of iceCandidates) {
514
+                this.peerconnection.addIceCandidate(
515
+                    iceCandidate,
516
+                    () => {
517
+                        logger.debug('addIceCandidate ok!');
518
+                    },
519
+                    error => {
520
+                        logger.error('addIceCandidate failed!', error);
521
+                    });
522
+            }
523
+
524
+            // There's no need to renegotiate for ICE candidates added with
525
+            // 'peerconnection.addIceCandidate'
526
+            return false;
527
+        };
528
+
529
+        logger.debug(`Queued add ICE candidates(${iceCandidates.length}) task`);
530
+        this._doRenegotiate('add ICE candidate', workFunction)
531
+            .then(() => {
532
+                logger.debug('Add ICE candidate done !');
533
+            },
534
+            error => {
535
+                logger.error(
536
+                    'Failed to add ICE candidate',
537
+                    elem[0] && elem[0].outerHTML,
538
+                    error);
541 539
             });
542
-        });
543 540
     }
544 541
 
545 542
     /**
@@ -585,18 +582,27 @@ export default class JingleSessionPC extends JingleSession {
585 582
         }
586 583
     }
587 584
 
585
+    /* eslint-disable max-params */
588 586
     /**
589 587
      * Accepts incoming Jingle 'session-initiate' and should send
590 588
      * 'session-accept' in result.
591 589
      * @param jingleOffer jQuery selector pointing to the jingle element of
592
-     *        the offer IQ
590
+     * the offer IQ
593 591
      * @param success callback called when we accept incoming session
594
-     *        successfully and receive RESULT packet to 'session-accept' sent.
592
+     * successfully and receive RESULT packet to 'session-accept' sent.
595 593
      * @param failure function(error) called if for any reason we fail to accept
596
-     *        the incoming offer. 'error' argument can be used to log some
597
-     *        details about the error.
594
+     * the incoming offer. 'error' argument can be used to log some details
595
+     * about the error.
596
+     * @param {Array<JitsiLocalTrack>} [localTracks] the optional list of
597
+     * the local tracks that will be added, before the offer/answer cycle
598
+     * executes. We allow the localTracks to optionally be passed in so that
599
+     * the addition of the local tracks and the processing of the initial offer
600
+     * can all be done atomically. We want to make sure that any other
601
+     * operations which originate in the XMPP Jingle messages related with
602
+     * this session to be executed with an assumption that the initial
603
+     * offer/answer cycle has been executed already.
598 604
      */
599
-    acceptOffer(jingleOffer, success, failure) {
605
+    acceptOffer(jingleOffer, success, failure, localTracks) {
600 606
         this.setOfferAnswerCycle(
601 607
             jingleOffer,
602 608
             () => {
@@ -607,16 +613,25 @@ export default class JingleSessionPC extends JingleSession {
607 613
                 // modify sendSessionAccept method to do that
608 614
                 this.sendSessionAccept(success, failure);
609 615
             },
610
-            failure);
616
+            failure,
617
+            localTracks);
611 618
     }
612 619
 
620
+    /* eslint-enable max-params */
621
+
613 622
     /**
614 623
      * Creates an offer and sends Jingle 'session-initiate' to the remote peer.
624
+     * @param {Array<JitsiLocalTrack>} localTracks the local tracks that will be
625
+     * added, before the offer/answer cycle executes (for the local track
626
+     * addition to be an atomic operation together with the offer/answer).
615 627
      */
616
-    invite() {
628
+    invite(localTracks) {
617 629
         if (!this.isInitiator) {
618 630
             throw new Error('Trying to invite from the responder session');
619 631
         }
632
+        for (const localTrack of localTracks) {
633
+            this.peerconnection.addTrack(localTrack);
634
+        }
620 635
         this.peerconnection.createOffer(
621 636
             this.sendSessionInitiate.bind(this),
622 637
             error => logger.error('Failed to create offer', error),
@@ -686,6 +701,7 @@ export default class JingleSessionPC extends JingleSession {
686 701
             });
687 702
     }
688 703
 
704
+    /* eslint-disable max-params */
689 705
     /**
690 706
      * This is a setRemoteDescription/setLocalDescription cycle which starts at
691 707
      * converting Strophe Jingle IQ into remote offer SDP. Once converted
@@ -695,9 +711,20 @@ export default class JingleSessionPC extends JingleSession {
695 711
      * @param success callback called when sRD/sLD cycle finishes successfully.
696 712
      * @param failure callback called with an error object as an argument if we
697 713
      *        fail at any point during setRD, createAnswer, setLD.
714
+     * @param {Array<JitsiLocalTrack>} [localTracks] the optional list of
715
+     * the local tracks that will be added, before the offer/answer cycle
716
+     * executes (for the local track addition to be an atomic operation together
717
+     * with the offer/answer).
698 718
      */
699
-    setOfferAnswerCycle(jingleOfferAnswerIq, success, failure) {
719
+    setOfferAnswerCycle(jingleOfferAnswerIq, success, failure, localTracks) {
700 720
         const workFunction = finishedCallback => {
721
+
722
+            if (localTracks) {
723
+                for (const track of localTracks) {
724
+                    this.peerconnection.addTrack(track);
725
+                }
726
+            }
727
+
701 728
             const newRemoteSdp
702 729
                 = this._processNewJingleOfferIq(jingleOfferAnswerIq);
703 730
 
@@ -721,6 +748,8 @@ export default class JingleSessionPC extends JingleSession {
721 748
             });
722 749
     }
723 750
 
751
+    /* eslint-enable max-params */
752
+
724 753
     /**
725 754
      * Although it states "replace transport" it does accept full Jingle offer
726 755
      * which should contain new ICE transport details.
@@ -1254,7 +1283,6 @@ export default class JingleSessionPC extends JingleSession {
1254 1283
                         this.peerconnection.setLocalDescription(
1255 1284
                             answer,
1256 1285
                             () => {
1257
-                                this._dequeIceCandidates();
1258 1286
                                 resolve();
1259 1287
                             },
1260 1288
                             error => {
@@ -1289,7 +1317,6 @@ export default class JingleSessionPC extends JingleSession {
1289 1317
             this.peerconnection.setRemoteDescription(
1290 1318
                 remoteDescription,
1291 1319
                 () => {
1292
-                    this._dequeIceCandidates();
1293 1320
                     resolve();
1294 1321
                 },
1295 1322
                 error => reject(`setRemoteDescription failed: ${error}`)
@@ -1306,7 +1333,6 @@ export default class JingleSessionPC extends JingleSession {
1306 1333
                             this.peerconnection.setRemoteDescription(
1307 1334
                                 remoteDescription,
1308 1335
                                 () => {
1309
-                                    this._dequeIceCandidates();
1310 1336
                                     resolve();
1311 1337
                                 },
1312 1338
                                 error => reject(
@@ -1443,35 +1469,6 @@ export default class JingleSessionPC extends JingleSession {
1443 1469
         return removeSsrcInfo;
1444 1470
     }
1445 1471
 
1446
-    /**
1447
-     * Adds <tt>JitsiLocalTrack</tt>s to this session.
1448
-     * @param {JitsiLocalTrack[]} tracks new local tracks that will be added.
1449
-     * @return {Promise} a promise that will resolve once all local tracks are
1450
-     * added. Will be rejected with a <tt>string</tt> which describes the error.
1451
-     * NOTE(brian): there is a decent amount of overlap here with replaceStream
1452
-     *  that could be re-used...however we can't leverage that currently because
1453
-     *  the extra work we do here must be in the work function context and if we
1454
-     *  then called replaceTrack we'd be adding another task on the queue
1455
-     *  from within a task which would then deadlock.  The 'replaceTrack' core
1456
-     *  logic should be moved into a helper function that could be called within
1457
-     *  the 'doReplaceStream' task or the 'doAddStream' task (for example)
1458
-     */
1459
-    addLocalTracks(tracks) {
1460
-        const workFunction = () => {
1461
-            if (!this.peerconnection) {
1462
-                return 'Error: tried adding stream with no active peer'
1463
-                    + ' connection';
1464
-            }
1465
-            for (const track of tracks) {
1466
-                this.peerconnection.addTrack(track);
1467
-            }
1468
-
1469
-            return true;
1470
-        };
1471
-
1472
-        return this._doRenegotiate('addStreams', workFunction);
1473
-    }
1474
-
1475 1472
     /**
1476 1473
      * Adds local track back to this session, as part of the unmute operation.
1477 1474
      * @param {JitsiLocalTrack} track

Laddar…
Avbryt
Spara