Sfoglia il codice sorgente

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 anni fa
parent
commit
e821d0cc41
2 ha cambiato i file con 112 aggiunte e 132 eliminazioni
  1. 24
    41
      JitsiConference.js
  2. 88
    91
      modules/xmpp/JingleSessionPC.js

+ 24
- 41
JitsiConference.js Vedi File

1296
 
1296
 
1297
     // Add local tracks to the session
1297
     // Add local tracks to the session
1298
     try {
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
     } catch (e) {
1316
     } catch (e) {
1316
         GlobalOnErrorHandler.callErrorHandler(e);
1317
         GlobalOnErrorHandler.callErrorHandler(e);
1317
         logger.error(e);
1318
         logger.error(e);
1883
 
1884
 
1884
     const localTracks = this.getLocalTracks();
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
         error => {
1892
         error => {
1902
             logger.error(
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
     // immediately once the P2P ICE connects.
2098
     // immediately once the P2P ICE connects.
2107
     const localTracks = this.getLocalTracks();
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 Vedi File

69
             options) {
69
             options) {
70
         super(sid, me, peerjid, connection, mediaConstraints, iceConfig);
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
         this.lasticecandidate = false;
72
         this.lasticecandidate = false;
83
         this.closed = false;
73
         this.closed = false;
84
 
74
 
163
         return true;
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
      * Finds all "source" elements under RTC "description" in given Jingle IQ
157
      * Finds all "source" elements under RTC "description" in given Jingle IQ
189
      * and adds 'ssrc-info' with the owner attribute set to
158
      * and adds 'ssrc-info' with the owner attribute set to
505
             return;
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
             .each((idx, candidate) => {
480
             .each((idx, candidate) => {
512
                 let line = SDPUtil.candidateFromJingle(candidate);
481
                 let line = SDPUtil.candidateFromJingle(candidate);
513
 
482
 
514
                 line = line.replace('\r\n', '').replace('a=', '');
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
                 const rtcCandidate = new RTCIceCandidate({
487
                 const rtcCandidate = new RTCIceCandidate({
518
                     sdpMLineIndex: 0,
488
                     sdpMLineIndex: 0,
519
 
489
 
520
                     // FF comes up with more complex names like audio-23423,
490
                     // FF comes up with more complex names like audio-23423,
521
                     // Given that it works on both Chrome and FF without
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
                     // sdpMid: 'audio',
494
                     // sdpMid: 'audio',
524
                     candidate: line
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
         }
582
         }
586
     }
583
     }
587
 
584
 
585
+    /* eslint-disable max-params */
588
     /**
586
     /**
589
      * Accepts incoming Jingle 'session-initiate' and should send
587
      * Accepts incoming Jingle 'session-initiate' and should send
590
      * 'session-accept' in result.
588
      * 'session-accept' in result.
591
      * @param jingleOffer jQuery selector pointing to the jingle element of
589
      * @param jingleOffer jQuery selector pointing to the jingle element of
592
-     *        the offer IQ
590
+     * the offer IQ
593
      * @param success callback called when we accept incoming session
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
      * @param failure function(error) called if for any reason we fail to accept
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
         this.setOfferAnswerCycle(
606
         this.setOfferAnswerCycle(
601
             jingleOffer,
607
             jingleOffer,
602
             () => {
608
             () => {
607
                 // modify sendSessionAccept method to do that
613
                 // modify sendSessionAccept method to do that
608
                 this.sendSessionAccept(success, failure);
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
      * Creates an offer and sends Jingle 'session-initiate' to the remote peer.
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
         if (!this.isInitiator) {
629
         if (!this.isInitiator) {
618
             throw new Error('Trying to invite from the responder session');
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
         this.peerconnection.createOffer(
635
         this.peerconnection.createOffer(
621
             this.sendSessionInitiate.bind(this),
636
             this.sendSessionInitiate.bind(this),
622
             error => logger.error('Failed to create offer', error),
637
             error => logger.error('Failed to create offer', error),
686
             });
701
             });
687
     }
702
     }
688
 
703
 
704
+    /* eslint-disable max-params */
689
     /**
705
     /**
690
      * This is a setRemoteDescription/setLocalDescription cycle which starts at
706
      * This is a setRemoteDescription/setLocalDescription cycle which starts at
691
      * converting Strophe Jingle IQ into remote offer SDP. Once converted
707
      * converting Strophe Jingle IQ into remote offer SDP. Once converted
695
      * @param success callback called when sRD/sLD cycle finishes successfully.
711
      * @param success callback called when sRD/sLD cycle finishes successfully.
696
      * @param failure callback called with an error object as an argument if we
712
      * @param failure callback called with an error object as an argument if we
697
      *        fail at any point during setRD, createAnswer, setLD.
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
         const workFunction = finishedCallback => {
720
         const workFunction = finishedCallback => {
721
+
722
+            if (localTracks) {
723
+                for (const track of localTracks) {
724
+                    this.peerconnection.addTrack(track);
725
+                }
726
+            }
727
+
701
             const newRemoteSdp
728
             const newRemoteSdp
702
                 = this._processNewJingleOfferIq(jingleOfferAnswerIq);
729
                 = this._processNewJingleOfferIq(jingleOfferAnswerIq);
703
 
730
 
721
             });
748
             });
722
     }
749
     }
723
 
750
 
751
+    /* eslint-enable max-params */
752
+
724
     /**
753
     /**
725
      * Although it states "replace transport" it does accept full Jingle offer
754
      * Although it states "replace transport" it does accept full Jingle offer
726
      * which should contain new ICE transport details.
755
      * which should contain new ICE transport details.
1254
                         this.peerconnection.setLocalDescription(
1283
                         this.peerconnection.setLocalDescription(
1255
                             answer,
1284
                             answer,
1256
                             () => {
1285
                             () => {
1257
-                                this._dequeIceCandidates();
1258
                                 resolve();
1286
                                 resolve();
1259
                             },
1287
                             },
1260
                             error => {
1288
                             error => {
1289
             this.peerconnection.setRemoteDescription(
1317
             this.peerconnection.setRemoteDescription(
1290
                 remoteDescription,
1318
                 remoteDescription,
1291
                 () => {
1319
                 () => {
1292
-                    this._dequeIceCandidates();
1293
                     resolve();
1320
                     resolve();
1294
                 },
1321
                 },
1295
                 error => reject(`setRemoteDescription failed: ${error}`)
1322
                 error => reject(`setRemoteDescription failed: ${error}`)
1306
                             this.peerconnection.setRemoteDescription(
1333
                             this.peerconnection.setRemoteDescription(
1307
                                 remoteDescription,
1334
                                 remoteDescription,
1308
                                 () => {
1335
                                 () => {
1309
-                                    this._dequeIceCandidates();
1310
                                     resolve();
1336
                                     resolve();
1311
                                 },
1337
                                 },
1312
                                 error => reject(
1338
                                 error => reject(
1443
         return removeSsrcInfo;
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
      * Adds local track back to this session, as part of the unmute operation.
1473
      * Adds local track back to this session, as part of the unmute operation.
1477
      * @param {JitsiLocalTrack} track
1474
      * @param {JitsiLocalTrack} track

Loading…
Annulla
Salva