Bug 1041832: add support for trickle ICE to mochitests r=bwc
authorNils Ohlmeier [:drno] <drno@ohlmeier.org>
Thu, 28 Aug 2014 13:36:00 -0700
changeset 223912 029654998ef32ce7b995ca21c631e5b384a7e504
parent 223911 206fa95f0df30342cdfc88bb669ccfbbd2d6956f
child 223913 6fac9e4a3738f357b09af562d3e77f58601b316f
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbwc
bugs1041832
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1041832: add support for trickle ICE to mochitests r=bwc
dom/media/tests/identity/test_setIdentityProvider.html
dom/media/tests/identity/test_setIdentityProviderWithErrors.html
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/templates.js
dom/media/tests/mochitest/test_peerConnection_bug1042791.html
dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html
dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html
dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
--- a/dom/media/tests/identity/test_setIdentityProvider.html
+++ b/dom/media/tests/identity/test_setIdentityProvider.html
@@ -78,18 +78,18 @@ function theTest() {
       ok(localEvents.peeridentity, "local got peer identity");
       ok(remoteEvents.peeridentity, "remote got peer identity");
       test.next();
     }
   ],
   [
     "OFFERS_AND_ANSWERS_INCLUDE_IDENTITY",
     function(test) {
-      ok(test.pcLocal._last_offer.sdp.contains("a=identity"), "a=identity is in the offer SDP");
-      ok(test.pcRemote._last_answer.sdp.contains("a=identity"), "a=identity is in the answer SDP");
+      ok(test.originalOffer.sdp.contains("a=identity"), "a=identity is in the offer SDP");
+      ok(test.originalAnswer.sdp.contains("a=identity"), "a=identity is in the answer SDP");
       test.next();
     }
   ],
   [
     "DESCRIPTIONS_CONTAIN_IDENTITY",
     function(test) {
       ok(test.pcLocal.localDescription.sdp.contains("a=identity"),
                          "a=identity is in the local copy of the offer");
--- a/dom/media/tests/identity/test_setIdentityProviderWithErrors.html
+++ b/dom/media/tests/identity/test_setIdentityProviderWithErrors.html
@@ -65,18 +65,18 @@ runNetworkTest(function () {
         ok(!test.pcLocal._pc.peerIdentity, 'local peerIdentity is not set');
         ok(!test.pcRemote._pc.peerIdentity, 'remote peerIdentity is not set');
         test.next();
       }
     ],
     [
       'ONLY_REMOTE_SDP_INCLUDES_IDENTITY_ASSERTION',
       function(test) {
-        ok(!test.pcLocal._last_offer.sdp.contains('a=identity'), 'a=identity not contained in the offer SDP');
-        ok(test.pcRemote._last_answer.sdp.contains('a=identity'), 'a=identity is contained in the answer SDP');
+        ok(!test.originalOffer.sdp.contains('a=identity'), 'a=identity not contained in the offer SDP');
+        ok(test.originalAnswer.sdp.contains('a=identity'), 'a=identity is contained in the answer SDP');
         test.next();
       }
     ]
   ]);
   test.run();
 });
 
 </script>
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -667,33 +667,41 @@ PeerConnectionTest.prototype.setStepTime
  *
  * @param {PeerConnectionWrapper} peer
  *        The peer connection wrapper to run the command on
  * @param {function} onSuccess
  *        Callback to execute if the offer was created successfully
  */
 PeerConnectionTest.prototype.createAnswer =
 function PCT_createAnswer(peer, onSuccess) {
+  var self = this;
+
   peer.createAnswer(function (answer) {
+    // make a copy so this does not get updated with ICE candidates
+    self.originalAnswer = new mozRTCSessionDescription(JSON.parse(JSON.stringify(answer)));
     onSuccess(answer);
   });
 };
 
 /**
  * Creates an offer for the specified peer connection instance
  * and automatically handles the failure case.
  *
  * @param {PeerConnectionWrapper} peer
  *        The peer connection wrapper to run the command on
  * @param {function} onSuccess
  *        Callback to execute if the offer was created successfully
  */
 PeerConnectionTest.prototype.createOffer =
 function PCT_createOffer(peer, onSuccess) {
+  var self = this;
+
   peer.createOffer(function (offer) {
+    // make a copy so this does not get updated with ICE candidates
+    self.originalOffer = new mozRTCSessionDescription(JSON.parse(JSON.stringify(offer)));
     onSuccess(offer);
   });
 };
 
 PeerConnectionTest.prototype.setIdentityProvider =
 function(peer, provider, protocol, identity) {
   peer.setIdentityProvider(provider, protocol, identity);
 };
@@ -824,16 +832,47 @@ PeerConnectionTest.prototype.teardown = 
     if (window.SimpleTest)
       networkTestFinished();
     else
       finish();
   });
 };
 
 /**
+ * Routes ice candidates from one PCW to the other PCW
+ */
+PeerConnectionTest.prototype.iceCandidateHandler = function
+PCT_iceCandidateHandler(caller, candidate) {
+  var self = this;
+
+  info("Received: " + JSON.stringify(candidate) + " from " + caller);
+
+  var target = null;
+  if (caller.contains("pcLocal")) {
+    if (self.pcRemote) {
+      target = self.pcRemote;
+    }
+  } else if (caller.contains("pcRemote")) {
+    if (self.pcLocal) {
+      target = self.pcLocal;
+    }
+  } else {
+    ok(false, "received event from unknown caller: " + caller);
+    return;
+  }
+
+  if (target) {
+    target.storeOrAddIceCandidate(candidate);
+  } else {
+    info("sending ice candidate to signaling server");
+    send_message({"ice_candidate": candidate});
+  }
+};
+
+/**
  * This class handles tests for data channels.
  *
  * @constructor
  * @param {object} [options={}]
  *        Optional options for the peer connection test
  * @param {object} [options.commands=commandsDataChannel]
  *        Commands to run for the test
  * @param {object} [options.config_local=undefined]
@@ -1098,16 +1137,22 @@ DataChannelTest.prototype = Object.creat
       target.onmessage = function (recv_data) {
         onSuccess(target, recv_data);
       };
 
       source.send(data);
     }
   },
 
+  createOffer : {
+    value : function DCT_createOffer(peer, onSuccess) {
+      PeerConnectionTest.prototype.createOffer.call(this, peer, onSuccess);
+    }
+  },
+
   setLocalDescription : {
     /**
      * Sets the local description for the specified peer connection instance
      * and automatically handles the failure case.
      *
      * @param {PeerConnectionWrapper} peer
               The peer connection wrapper to run the command on
      * @param {mozRTCSessionDescription} desc
@@ -1367,16 +1412,21 @@ function PeerConnectionWrapper(label, co
   this.streams = [ ];
   this.mediaCheckers = [ ];
 
   this.dataChannels = [ ];
 
   this.onAddStreamFired = false;
   this.addStreamCallbacks = {};
 
+  this.remoteDescriptionSet = false;
+  this.endOfTrickleIce = false;
+  this.localRequiresTrickleIce = false;
+  this.remoteRequiresTrickleIce  = false;
+
   this.h264 = typeof h264 !== "undefined" ? true : false;
 
   info("Creating " + this);
   this._pc = new mozRTCPeerConnection(this.configuration);
 
   /**
    * Setup callback handlers
    */
@@ -1643,17 +1693,18 @@ PeerConnectionWrapper.prototype = {
    * @param {function} onSuccess
    *        Callback to execute if the offer was created successfully
    */
   createOffer : function PCW_createOffer(onSuccess) {
     var self = this;
 
     this._pc.createOffer(function (offer) {
       info("Got offer: " + JSON.stringify(offer));
-      self._last_offer = offer;
+      // note: this might get updated through ICE gathering
+      self._latest_offer = offer;
       if (self.h264) {
         isnot(offer.sdp.search("H264/90000"), -1, "H.264 should be present in the SDP offer");
         offer.sdp = removeVP8(offer.sdp);
       }
       onSuccess(offer);
     }, generateErrorCallback(), this.offerOptions);
   },
 
@@ -1715,16 +1766,25 @@ PeerConnectionWrapper.prototype = {
    *        mozRTCSessionDescription for the remote description request
    * @param {function} onSuccess
    *        Callback to execute if the remote description was set successfully
    */
   setRemoteDescription : function PCW_setRemoteDescription(desc, onSuccess) {
     var self = this;
     this._pc.setRemoteDescription(desc, function () {
       info(self + ": Successfully set remote description");
+      self.remoteDescriptionSet = true;
+      if ((self._ice_candidates_to_add) &&
+          (self._ice_candidates_to_add.length > 0)) {
+        info("adding stored ice candidates");
+        for (var i = 0; i < self._ice_candidates_to_add.length; i++) {
+          self.addIceCandidate(self._ice_candidates_to_add[i]);
+        }
+        self._ice_candidates_to_add = [];
+      }
       onSuccess();
     }, generateErrorCallback());
   },
 
   /**
    * Tries to set the remote description and expect failure. Automatically
    * causes the test case to fail if the call succeeds.
    *
@@ -1761,29 +1821,50 @@ PeerConnectionWrapper.prototype = {
       self.signalingStateLog.push(newstate);
     }
 
     self.signalingStateLog = [self._pc.signalingState];
     self.signalingStateCallbacks.logSignalingStatus = _logSignalingState;
   },
 
   /**
+   * Either adds a given ICE candidate right away or stores it to be added
+   * later, depending on the state of the PeerConnection.
+   *
+   * @param {object} candidate
+   *        The mozRTCIceCandidate to be added or stored
+   */
+  storeOrAddIceCandidate : function PCW_storeOrAddIceCandidate(candidate) {
+    var self = this;
+
+    self._remote_ice_candidates.push(candidate);
+    if (self.remoteDescriptionSet) {
+      self.addIceCandidate(candidate);
+    } else {
+      self._ice_candidates_to_add.push(candidate);
+    }
+  },
+
+  /**
    * Adds an ICE candidate and automatically handles the failure case.
    *
    * @param {object} candidate
    *        SDP candidate
    * @param {function} onSuccess
    *        Callback to execute if the local description was set successfully
    */
   addIceCandidate : function PCW_addIceCandidate(candidate, onSuccess) {
     var self = this;
 
+    info(self + ": adding ICE candidate " + JSON.stringify(candidate));
     this._pc.addIceCandidate(candidate, function () {
       info(self + ": Successfully added an ICE candidate");
-      onSuccess();
+      if (onSuccess) {
+        onSuccess();
+      }
     }, generateErrorCallback());
   },
 
   /**
    * Tries to add an ICE candidate and expects failure. Automatically
    * causes the test case to fail if the call succeeds.
    *
    * @param {object} candidate
@@ -1803,17 +1884,17 @@ PeerConnectionWrapper.prototype = {
   },
 
   /**
    * Returns if the ICE the connection state is "connected".
    *
    * @returns {boolean} True if the connection state is "connected", otherwise false.
    */
   isIceConnected : function PCW_isIceConnected() {
-    info("iceConnectionState: " + this.iceConnectionState);
+    info(this + ": iceConnectionState = " + this.iceConnectionState);
     return this.iceConnectionState === "connected";
   },
 
   /**
    * Returns if the ICE the connection state is "checking".
    *
    * @returns {boolean} True if the connection state is "checking", otherwise false.
    */
@@ -1888,16 +1969,53 @@ PeerConnectionWrapper.prototype = {
         myFailure();
       }
     }
 
     self.ice_connection_callbacks.waitForIceConnected = iceConnectedChanged;
   },
 
   /**
+   * Setup a onicecandidate handler
+   *
+   * @param {object} test
+   *        A PeerConnectionTest object to which the ice candidates gets
+   *        forwarded.
+   */
+  setupIceCandidateHandler : function PCW_setupIceCandidateHandler(test) {
+    var self = this;
+    self._local_ice_candidates = [];
+    self._remote_ice_candidates = [];
+    self._ice_candidates_to_add = [];
+
+    function iceCandidateCallback (anEvent) {
+      info(self.label + ": received iceCandidateEvent");
+      if (!anEvent.candidate) {
+        info(self.label + ": received end of trickle ICE event");
+        self.endOfTrickleIce = true;
+      } else {
+        if (self.endOfTrickleIce) {
+          ok(false, "received ICE candidate after end of trickle");
+        }
+        info(self.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate));
+        ok(anEvent.candidate.candidate.length > 0, "ICE candidate contains candidate");
+        // we don't support SDP MID's yet
+        ok(anEvent.candidate.sdpMid.length === 0, "SDP MID has length zero");
+        ok(typeof anEvent.candidate.sdpMLineIndex === 'number', "SDP MLine Index needs to exist");
+        self._local_ice_candidates.push(anEvent.candidate);
+        test.iceCandidateHandler(self.label, anEvent.candidate);
+      }
+    }
+
+    //FIXME: in the steeplecase scenario we need to setup a permanent listener
+    //       for ice candidates from the signaling server here
+    self._pc.onicecandidate = iceCandidateCallback;
+  },
+
+  /**
    * Counts the amount of audio tracks in a given media constraint.
    *
    * @param constraints
    *        The contraint to be examined.
    */
   countAudioTracksInMediaConstraint : function
     PCW_countAudioTracksInMediaConstraint(constraints) {
     if ((!constraints) || (constraints.length === 0)) {
@@ -2068,30 +2186,36 @@ PeerConnectionWrapper.prototype = {
         ok(self.onAddStreamFired, self + " checkMediaTracks() timed out waiting for onaddstream event to fire");
         if (!self.onAddStreamFired) {
           onSuccess();
         }
       }, 60000);
     }
   },
 
-  verifySdp : function PCW_verifySdp(desc, expectedType, constraints, offerOptions) {
+  verifySdp : function PCW_verifySdp(desc, expectedType, constraints,
+      offerOptions, trickleIceCallback) {
     info("Examining this SessionDescription: " + JSON.stringify(desc));
     info("constraints: " + JSON.stringify(constraints));
     info("offerOptions: " + JSON.stringify(offerOptions));
     ok(desc, "SessionDescription is not null");
     is(desc.type, expectedType, "SessionDescription type is " + expectedType);
     ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
     ok(desc.sdp.contains("a=ice-ufrag"), "ICE username is present in SDP");
     ok(desc.sdp.contains("a=ice-pwd"), "ICE password is present in SDP");
     ok(desc.sdp.contains("a=fingerprint"), "ICE fingerprint is present in SDP");
     //TODO: update this for loopback support bug 1027350
     ok(!desc.sdp.contains(LOOPBACK_ADDR), "loopback interface is absent from SDP");
-    //TODO: update this for trickle ICE bug 1041832
-    ok(desc.sdp.contains("a=candidate"), "at least one ICE candidate is present in SDP");
+    if (desc.sdp.contains("a=candidate")) {
+      ok(true, "at least one ICE candidate is present in SDP");
+      trickleIceCallback(false);
+    } else {
+      info("No ICE candidate in SDP -> requiring trickle ICE");
+      trickleIceCallback(true);
+    }
     //TODO: how can we check for absence/presence of m=application?
 
     //TODO: how to handle media contraints + offer options
     var audioTracks = this.countAudioTracksInMediaConstraint(constraints);
     if (constraints.length === 0) {
       audioTracks = this.audioInOfferOptions(offerOptions);
     }
     info("expected audio tracks: " + audioTracks);
--- a/dom/media/tests/mochitest/templates.js
+++ b/dom/media/tests/mochitest/templates.js
@@ -1,18 +1,21 @@
 /**
  * Default list of commands to execute for a PeerConnection test.
  */
 
-var STABLE = "stable";
-var HAVE_LOCAL_OFFER = "have-local-offer";
-var HAVE_REMOTE_OFFER = "have-remote-offer";
-var CLOSED = "closed";
+const STABLE = "stable";
+const HAVE_LOCAL_OFFER = "have-local-offer";
+const HAVE_REMOTE_OFFER = "have-remote-offer";
+const CLOSED = "closed";
 
 const ICE_NEW = "new";
+const GATH_NEW = "new";
+const GATH_GATH = "gathering";
+const GATH_COMPLETE = "complete"
 
 function deltaSeconds(date1, date2) {
   return (date2.getTime() - date1.getTime())/1000;
 }
 
 function dumpSdp(test) {
   if (typeof test._local_offer !== 'undefined') {
     dump("ERROR: SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, ''));
@@ -118,48 +121,62 @@ var commandsPeerConnection = [
     'PC_REMOTE_CHECK_INITIAL_ICE_STATE',
     function (test) {
       is(test.pcRemote.iceConnectionState, ICE_NEW,
         "Initial remote ICE connection state is 'new'");
       test.next();
     }
   ],
   [
+    'PC_LOCAL_SETUP_ICE_HANDLER',
+    function (test) {
+      test.pcLocal.setupIceCandidateHandler(test);
+      test.next();
+    }
+  ],
+  [
+    'PC_REMOTE_SETUP_ICE_HANDLER',
+    function (test) {
+      test.pcRemote.setupIceCandidateHandler(test);
+      test.next();
+    }
+  ],
+  [
     'PC_LOCAL_CREATE_OFFER',
     function (test) {
       test.createOffer(test.pcLocal, function (offer) {
         is(test.pcLocal.signalingState, STABLE,
            "Local create offer does not change signaling state");
         if (!test.pcRemote) {
-          send_message({"offer": test.pcLocal._last_offer,
+          send_message({"offer": test.originalOffer,
                         "offer_constraints": test.pcLocal.constraints,
                         "offer_options": test.pcLocal.offerOptions});
           test._local_offer = test.pcLocal._last_offer;
           test._offer_constraints = test.pcLocal.constraints;
           test._offer_options = test.pcLocal.offerOptions;
         }
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_SET_LOCAL_DESCRIPTION',
     function (test) {
-      test.setLocalDescription(test.pcLocal, test.pcLocal._last_offer, HAVE_LOCAL_OFFER, function () {
+      test.setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER, function () {
         is(test.pcLocal.signalingState, HAVE_LOCAL_OFFER,
            "signalingState after local setLocalDescription is 'have-local-offer'");
         test.next();
       });
     }
   ],
   [
     'PC_REMOTE_GET_OFFER',
     function (test) {
       if (test.pcLocal) {
-        test._local_offer = test.pcLocal._last_offer;
+        test._local_offer = test.originalOffer;
         test._offer_constraints = test.pcLocal.constraints;
         test._offer_options = test.pcLocal.offerOptions;
         test.next();
       } else {
         wait_for_message().then(function(message) {
           ok("offer" in message, "Got an offer message");
           test._local_offer = new mozRTCSessionDescription(message.offer);
           test._offer_constraints = message.offer_constraints;
@@ -177,35 +194,43 @@ var commandsPeerConnection = [
            "signalingState after remote setRemoteDescription is 'have-remote-offer'");
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_SANE_LOCAL_SDP',
     function (test) {
-      test.pcLocal.verifySdp(test.pcLocal.localDescription, "offer", test._offer_constraints, test._offer_options);
+      test.pcLocal.verifySdp(test._local_offer, "offer",
+        test._offer_constraints, test._offer_options,
+        function(trickle) {
+          test.pcLocal.localRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_REMOTE_SANE_REMOTE_SDP',
     function (test) {
-      test.pcRemote.verifySdp(test.pcRemote.remoteDescription, "offer", test._offer_constraints, test._offer_options);
+      test.pcRemote.verifySdp(test._local_offer, "offer",
+        test._offer_constraints, test._offer_options,
+        function (trickle) {
+          test.pcRemote.remoteRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_REMOTE_CREATE_ANSWER',
     function (test) {
       test.createAnswer(test.pcRemote, function (answer) {
         is(test.pcRemote.signalingState, HAVE_REMOTE_OFFER,
            "Remote createAnswer does not change signaling state");
         if (!test.pcLocal) {
-          send_message({"answer": test.pcRemote._last_answer,
+          send_message({"answer": test.originalAnswer,
                         "answer_constraints": test.pcRemote.constraints});
           test._remote_answer = test.pcRemote._last_answer;
           test._answer_constraints = test.pcRemote.constraints;
         }
         test.next();
       });
     }
   ],
@@ -231,81 +256,93 @@ var commandsPeerConnection = [
             dump("SDP: " + sdp.replace(/[\r]/g, '') + "\n");
             ok(false, "This Transport:IP:Port " + triple + " appears twice in the SDP above!");
           }
           resultArray.push(triple);
         }
         return resultArray;
       }
 
-      const offerTriples = _sdpCandidatesIntoArray(test._local_offer.sdp);
+      const offerTriples = _sdpCandidatesIntoArray(test.originalOffer.sdp);
       info("Offer ICE host candidates: " + JSON.stringify(offerTriples));
 
-      const answerTriples = _sdpCandidatesIntoArray(test.pcRemote._last_answer.sdp);
+      const answerTriples = _sdpCandidatesIntoArray(test.originalAnswer.sdp);
       info("Answer ICE host candidates: " + JSON.stringify(answerTriples));
 
       for (var i=0; i< offerTriples.length; i++) {
         if (answerTriples.indexOf(offerTriples[i]) !== -1) {
-          dump("SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, '') + "\n");
-          dump("SDP answer: " + test.pcRemote._last_answer.sdp.replace(/[\r]/g, '') + "\n");
+          dump("SDP offer: " + test.originalOffer.sdp.replace(/[\r]/g, '') + "\n");
+          dump("SDP answer: " + test.originalAnswer.sdp.replace(/[\r]/g, '') + "\n");
           ok(false, "This IP:Port " + offerTriples[i] + " appears in SDP offer and answer!");
         }
       }
 
       test.next();
     }
   ],
   [
     'PC_REMOTE_SET_LOCAL_DESCRIPTION',
     function (test) {
-      test.setLocalDescription(test.pcRemote, test.pcRemote._last_answer, STABLE, function () {
-        is(test.pcRemote.signalingState, STABLE,
-           "signalingState after remote setLocalDescription is 'stable'");
-        test.next();
-      });
+      test.setLocalDescription(test.pcRemote, test.originalAnswer, STABLE,
+        function () {
+          is(test.pcRemote.signalingState, STABLE,
+            "signalingState after remote setLocalDescription is 'stable'");
+          test.next();
+        }
+      );
     }
   ],
   [
     'PC_LOCAL_GET_ANSWER',
     function (test) {
       if (test.pcRemote) {
-        test._remote_answer = test.pcRemote._last_answer;
+        test._remote_answer = test.originalAnswer;
         test._answer_constraints = test.pcRemote.constraints;
         test.next();
       } else {
         wait_for_message().then(function(message) {
           ok("answer" in message, "Got an answer message");
           test._remote_answer = new mozRTCSessionDescription(message.answer);
           test._answer_constraints = message.answer_constraints;
           test.next();
         });
       }
     }
   ],
   [
     'PC_LOCAL_SET_REMOTE_DESCRIPTION',
     function (test) {
-      test.setRemoteDescription(test.pcLocal, test._remote_answer, STABLE, function () {
-        is(test.pcLocal.signalingState, STABLE,
-           "signalingState after local setRemoteDescription is 'stable'");
-        test.next();
-      });
+      test.setRemoteDescription(test.pcLocal, test._remote_answer, STABLE,
+        function () {
+          is(test.pcLocal.signalingState, STABLE,
+            "signalingState after local setRemoteDescription is 'stable'");
+          test.next();
+        }
+      );
     }
   ],
   [
     'PC_REMOTE_SANE_LOCAL_SDP',
     function (test) {
-      test.pcRemote.verifySdp(test.pcRemote.localDescription, "answer", test._answer_constraints, test._offer_options);
+      test.pcRemote.verifySdp(test._remote_answer, "answer",
+        test._answer_constraints, test._offer_options,
+        function (trickle) {
+          test.pcRemote.localRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_LOCAL_SANE_REMOTE_SDP',
     function (test) {
-      test.pcLocal.verifySdp(test.pcLocal.remoteDescription, "answer", test._answer_constraints, test._offer_options);
+      test.pcLocal.verifySdp(test._remote_answer, "answer",
+        test._answer_constraints, test._offer_options,
+        function (trickle) {
+          test.pcLocal.remoteRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_LOCAL_WAIT_FOR_ICE_CONNECTED',
     function (test) {
       var myTest = test;
       var myPc = myTest.pcLocal;
@@ -330,16 +367,28 @@ var commandsPeerConnection = [
       } else {
         dumpSdp(myTest);
         ok(false, "pc_local: ICE is already in bad state: " + myPc.iceConnectionState);
         myTest.next();
       }
     }
   ],
   [
+    'PC_LOCAL_VERIFY_ICE_GATHERING',
+    function (test) {
+      if (test.pcLocal.localRequiresTrickleIce) {
+        ok(test.pcLocal._local_ice_candidates.length > 0, "Received local trickle ICE candidates");
+      }
+      // a super slow TURN server might make this fail
+      ok(test.pcLocal.endOfTrickleIce, "Received end of ICE gathering candidate");
+      isnot(test.pcLocal._pc.iceGatheringState, GATH_NEW, "ICE gathering state is not 'new'");
+      test.next();
+    }
+  ],
+  [
     'PC_REMOTE_WAIT_FOR_ICE_CONNECTED',
     function (test) {
       var myTest = test;
       var myPc = myTest.pcRemote;
 
       function onIceConnectedSuccess () {
         info("pcRemote ICE connection state log: " + test.pcRemote.iceConnectionLog);
         ok(true, "pc_remote: ICE switched to 'connected' state");
@@ -360,16 +409,28 @@ var commandsPeerConnection = [
       } else {
         dumpSdp(myTest);
         ok(false, "pc_remote: ICE is already in bad state: " + myPc.iceConnectionState);
         myTest.next();
       }
     }
   ],
   [
+    'PC_REMOTE_VERIFY_ICE_GATHERING',
+    function (test) {
+      if (test.pcRemote.localRequiresTrickleIce) {
+        ok(test.pcRemote._local_ice_candidates.length > 0, "Received local trickle ICE candidates");
+      }
+      // a super slow TURN server might make this fail
+      ok(test.pcRemote.endOfTrickleIce, "Received end of ICE gathering candidate");
+      isnot(test.pcRemote._pc.iceGatheringState, GATH_NEW, "ICE gathering state is not 'new'");
+      test.next();
+    }
+  ],
+  [
     'PC_LOCAL_CHECK_MEDIA_TRACKS',
     function (test) {
       test.pcLocal.checkMediaTracks(test._answer_constraints, function () {
         test.next();
       });
     }
   ],
   [
@@ -661,87 +722,140 @@ var commandsDataChannel = [
     'PC_REMOTE_CHECK_INITIAL_ICE_STATE',
     function (test) {
       is(test.pcRemote.iceConnectionState, ICE_NEW,
         "Initial remote ICE connection state is 'new'");
       test.next();
     }
   ],
   [
+    'PC_LOCAL_SETUP_ICE_HANDLER',
+    function (test) {
+      test.pcLocal.setupIceCandidateHandler(test);
+      test.next();
+    }
+  ],
+  [
+    'PC_REMOTE_SETUP_ICE_HANDLER',
+    function (test) {
+      test.pcRemote.setupIceCandidateHandler(test);
+      test.next();
+    }
+  ],
+  [
     'PC_LOCAL_CREATE_DATA_CHANNEL',
     function (test) {
       var channel = test.pcLocal.createDataChannel({});
 
       is(channel.binaryType, "blob", channel + " is of binary type 'blob'");
       is(channel.readyState, "connecting", channel + " is in state: 'connecting'");
 
       is(test.pcLocal.signalingState, STABLE,
          "Create datachannel does not change signaling state");
 
       test.next();
     }
   ],
   [
     'PC_LOCAL_CREATE_OFFER',
     function (test) {
-      test.pcLocal.createOffer(function (offer) {
+      test.createOffer(test.pcLocal, function (offer) {
         is(test.pcLocal.signalingState, STABLE,
            "Local create offer does not change signaling state");
-        ok(!offer.sdp.contains(LOOPBACK_ADDR),
-           "loopback interface is absent from SDP");
         ok(offer.sdp.contains("m=application"),
            "m=application is contained in the SDP");
+        if (!test.pcRemote) {
+          send_message({"offer": test.originalOffer,
+                        "offer_constraints": test.pcLocal.constraints,
+                        "offer_options": test.pcLocal.offerOptions});
+          test._local_offer = test.pcLocal._last_offer;
+          test._offer_constraints = test.pcLocal.constraints;
+          test._offer_options = test.pcLocal.offerOptions;
+        }
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_SET_LOCAL_DESCRIPTION',
     function (test) {
-      test.setLocalDescription(test.pcLocal, test.pcLocal._last_offer, HAVE_LOCAL_OFFER,
+      test.setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER,
         function () {
         is(test.pcLocal.signalingState, HAVE_LOCAL_OFFER,
            "signalingState after local setLocalDescription is 'have-local-offer'");
         test.next();
       });
     }
   ],
   [
+    'PC_REMOTE_GET_OFFER',
+    function (test) {
+      if (test.pcLocal) {
+        test._local_offer = test.originalOffer;
+        test._offer_constraints = test.pcLocal.constraints;
+        test._offer_options = test.pcLocal.offerOptions;
+        test.next();
+      } else {
+        wait_for_message().then(function(message) {
+          ok("offer" in message, "Got an offer message");
+          test._local_offer = new mozRTCSessionDescription(message.offer);
+          test._offer_constraints = message.offer_constraints;
+          test._offer_options = message.offer_options;
+          test.next();
+        });
+      }
+    }
+  ],
+  [
     'PC_REMOTE_SET_REMOTE_DESCRIPTION',
     function (test) {
-      test.setRemoteDescription(test.pcRemote, test.pcLocal._last_offer, HAVE_REMOTE_OFFER,
+      test.setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER,
         function () {
         is(test.pcRemote.signalingState, HAVE_REMOTE_OFFER,
            "signalingState after remote setRemoteDescription is 'have-remote-offer'");
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_SANE_LOCAL_SDP',
     function (test) {
-      test.pcLocal.verifySdp(test.pcLocal.localDescription, "offer", test.pcLocal.constraints);
+      test.pcLocal.verifySdp(test._local_offer, "offer",
+        test._offer_constraints, test._offer_options,
+        function(trickle) {
+          test.pcLocal.localRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_REMOTE_SANE_REMOTE_SDP',
     function (test) {
-      test.pcRemote.verifySdp(test.pcRemote.remoteDescription, "offer", test.pcLocal.constraints);
+      test.pcRemote.verifySdp(test._local_offer, "offer",
+        test._offer_constraints, test._offer_options,
+        function (trickle) {
+          test.pcRemote.remoteRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_REMOTE_CREATE_ANSWER',
     function (test) {
       test.createAnswer(test.pcRemote, function (answer) {
         is(test.pcRemote.signalingState, HAVE_REMOTE_OFFER,
-           "Remote create offer does not change signaling state");
-        ok(!answer.sdp.contains(LOOPBACK_ADDR),
-           "loopback interface is absent in SDP");
+           "Remote createAnswer does not change signaling state");
+        ok(answer.sdp.contains("m=application"),
+           "m=application is contained in the SDP");
+        if (!test.pcLocal) {
+          send_message({"answer": test.originalAnswer,
+                        "answer_constraints": test.pcRemote.constraints});
+          test._remote_answer = test.pcRemote._last_answer;
+          test._answer_constraints = test.pcRemote.constraints;
+        }
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_SETUP_DATA_CHANNEL_CALLBACK',
     function (test) {
       test.waitForInitialDataChannel(test.pcLocal, function () {
@@ -761,47 +875,73 @@ var commandsDataChannel = [
       // At this point a timeout failure will be of no value
       null);
       test.next();
     }
   ],
   [
     'PC_REMOTE_SET_LOCAL_DESCRIPTION',
     function (test) {
-      test.setLocalDescription(test.pcRemote, test.pcRemote._last_answer, STABLE,
+      test.setLocalDescription(test.pcRemote, test.originalAnswer, STABLE,
         function () {
           is(test.pcRemote.signalingState, STABLE,
-             "signalingState after remote setLocalDescription is 'stable'");
+            "signalingState after remote setLocalDescription is 'stable'");
           test.next();
         }
       );
     }
   ],
   [
+    'PC_LOCAL_GET_ANSWER',
+    function (test) {
+      if (test.pcRemote) {
+        test._remote_answer = test.originalAnswer;
+        test._answer_constraints = test.pcRemote.constraints;
+        test.next();
+      } else {
+        wait_for_message().then(function(message) {
+          ok("answer" in message, "Got an answer message");
+          test._remote_answer = new mozRTCSessionDescription(message.answer);
+          test._answer_constraints = message.answer_constraints;
+          test.next();
+        });
+      }
+    }
+  ],
+  [
     'PC_LOCAL_SET_REMOTE_DESCRIPTION',
     function (test) {
-      test.setRemoteDescription(test.pcLocal, test.pcRemote._last_answer, STABLE,
+      test.setRemoteDescription(test.pcLocal, test._remote_answer, STABLE,
         function () {
-        is(test.pcLocal.signalingState, STABLE,
-           "signalingState after local setRemoteDescription is 'stable'");
-        test.next();
-      });
+          is(test.pcLocal.signalingState, STABLE,
+            "signalingState after local setRemoteDescription is 'stable'");
+          test.next();
+        }
+      );
     }
   ],
   [
     'PC_REMOTE_SANE_LOCAL_SDP',
     function (test) {
-      test.pcRemote.verifySdp(test.pcRemote.localDescription, "answer", test.pcRemote.constraints);
+      test.pcRemote.verifySdp(test._remote_answer, "answer",
+        test._answer_constraints, test._offer_options,
+        function (trickle) {
+          test.pcRemote.localRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_LOCAL_SANE_REMOTE_SDP',
     function (test) {
-      test.pcLocal.verifySdp(test.pcLocal.remoteDescription, "answer", test.pcRemote.constraints);
+      test.pcLocal.verifySdp(test._remote_answer, "answer",
+        test._answer_constraints, test._offer_options,
+        function (trickle) {
+          test.pcLocal.remoteRequiresTrickleIce = trickle;
+        });
       test.next();
     }
   ],
   [
     'PC_LOCAL_WAIT_FOR_ICE_CONNECTED',
     function (test) {
       var myTest = test;
       var myPc = myTest.pcLocal;
@@ -826,16 +966,28 @@ var commandsDataChannel = [
       } else {
         dumpSdp(myTest);
         ok(false, "pc_local: ICE is already in bad state: " + myPc.iceConnectionState);
         myTest.next();
       }
     }
   ],
   [
+    'PC_LOCAL_VERIFY_ICE_GATHERING',
+    function (test) {
+      if (test.pcLocal.localRequiresTrickleIce) {
+        ok(test.pcLocal._local_ice_candidates.length > 0, "Received local trickle ICE candidates");
+      }
+      // a super slow TURN server might make this fail
+      ok(test.pcLocal.endOfTrickleIce, "Received end of ICE gathering candidate");
+      isnot(test.pcLocal._pc.iceGatheringState, GATH_NEW, "ICE gathering state is not 'new'");
+      test.next();
+    }
+  ],
+  [
     'PC_REMOTE_WAIT_FOR_ICE_CONNECTED',
     function (test) {
       var myTest = test;
       var myPc = myTest.pcRemote;
 
       function onIceConnectedSuccess () {
         info("pcRemote ICE connection state log: " + test.pcRemote.iceConnectionLog);
         ok(true, "pc_remote: ICE switched to 'connected' state");
@@ -856,16 +1008,28 @@ var commandsDataChannel = [
       } else {
         dumpSdp(myTest);
         ok(false, "pc_remote: ICE is already in bad state: " + myPc.iceConnectionState);
         myTest.next();
       }
     }
   ],
   [
+    'PC_REMOTE_VERIFY_ICE_GATHERING',
+    function (test) {
+      if (test.pcRemote.localRequiresTrickleIce) {
+        ok(test.pcRemote._local_ice_candidates.length > 0, "Received local trickle ICE candidates");
+      }
+      // a super slow TURN server might make this fail
+      ok(test.pcRemote.endOfTrickleIce, "Received end of ICE gathering candidate");
+      isnot(test.pcRemote._pc.iceGatheringState, GATH_NEW, "ICE gathering state is not 'new'");
+      test.next();
+    }
+  ],
+  [
     'PC_LOCAL_VERIFY_DATA_CHANNEL_STATE',
     function (test) {
       test.waitForInitialDataChannel(test.pcLocal, function() {
         test.next();
       }, function() {
         ok(false, test.pcLocal + " initial dataChannels[0] failed to switch to 'open'");
         //TODO: use stopAndExit() once bug 1019323 has landed
         unexpectedEventAndFinish(this, 'timeout')
@@ -886,25 +1050,25 @@ var commandsDataChannel = [
         // to prevent test framework timeouts
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_CHECK_MEDIA_TRACKS',
     function (test) {
-      test.pcLocal.checkMediaTracks(test.pcRemote.constraints, function () {
+      test.pcLocal.checkMediaTracks(test._answer_constraints, function () {
         test.next();
       });
     }
   ],
   [
     'PC_REMOTE_CHECK_MEDIA_TRACKS',
     function (test) {
-      test.pcRemote.checkMediaTracks(test.pcLocal.constraints, function () {
+      test.pcRemote.checkMediaTracks(test._offer_constraints, function () {
         test.next();
       });
     }
   ],
   [
     'PC_LOCAL_CHECK_MEDIA_FLOW_PRESENT',
     function (test) {
       test.pcLocal.checkMediaFlowPresent(function () {
--- a/dom/media/tests/mochitest/test_peerConnection_bug1042791.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug1042791.html
@@ -23,19 +23,19 @@
     options.h264 = true;
     test = new PeerConnectionTest(options);
     test.setMediaConstraints([{video: true}], [{video: true}]);
     test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
 
     test.chain.append([[
       "PC_LOCAL_VERIFY_H264_OFFER",
       function (test) {
-        ok(!test.pcLocal._last_offer.sdp.toLowerCase().contains("profile-level-id=0x42e0"),
+        ok(!test.pcLocal._latest_offer.sdp.toLowerCase().contains("profile-level-id=0x42e0"),
           "H264 offer does not contain profile-level-id=0x42e0");
-        ok(test.pcLocal._last_offer.sdp.toLowerCase().contains("profile-level-id=42e0"),
+        ok(test.pcLocal._latest_offer.sdp.toLowerCase().contains("profile-level-id=42e0"),
           "H264 offer contains profile-level-id=42e0");
         test.next();
       }
     ]]);
 
     test.run();
   });
 </script>
--- a/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
@@ -21,18 +21,18 @@
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
 
     test.chain.append([[
       "PC_LOCAL_SET_LOCAL_ANSWER",
       function (test) {
-        test.pcLocal._last_offer.type="answer";
-        test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._last_offer,
+        test.pcLocal._latest_offer.type="answer";
+        test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer,
           function(err) {
             is(err.name, "INVALID_STATE", "Error is INVALID_STATE");
             test.next();
           } );
       }
     ]]);
 
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setLocalAnswerInStable.html
@@ -21,18 +21,18 @@
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
 
     test.chain.append([[
       "PC_LOCAL_SET_LOCAL_ANSWER",
       function (test) {
-        test.pcLocal._last_offer.type="answer";
-        test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._last_offer,
+        test.pcLocal._latest_offer.type="answer";
+        test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer,
           function(err) {
             is(err.name, "INVALID_STATE", "Error is INVALID_STATE");
             test.next();
           } );
       }
     ]]);
 
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
@@ -21,17 +21,17 @@
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION");
 
     test.chain.append([[
       "PC_REMOTE_SET_LOCAL_OFFER",
       function (test) {
-        test.pcRemote.setLocalDescriptionAndFail(test.pcLocal._last_offer,
+        test.pcRemote.setLocalDescriptionAndFail(test.pcLocal._latest_offer,
           function(err) {
             is(err.name, "INVALID_STATE", "Error is INVALID_STATE");
             test.next();
           } );
       }
     ]]);
 
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
@@ -21,18 +21,18 @@
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION");
 
     test.chain.append([[
       "PC_REMOTE_SET_REMOTE_ANSWER",
       function (test) {
-        test.pcLocal._last_offer.type="answer";
-        test.pcRemote.setRemoteDescriptionAndFail(test.pcLocal._last_offer,
+        test.pcLocal._latest_offer.type="answer";
+        test.pcRemote.setRemoteDescriptionAndFail(test.pcLocal._latest_offer,
           function(err) {
             is(err.name, "INVALID_STATE", "Error is INVALID_STATE");
             test.next();
           } );
       }
     ]]);
 
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setRemoteAnswerInStable.html
@@ -21,18 +21,18 @@
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
 
     test.chain.append([[
       "PC_LOCAL_SET_REMOTE_ANSWER",
       function (test) {
-        test.pcLocal._last_offer.type="answer";
-        test.pcLocal.setRemoteDescriptionAndFail(test.pcLocal._last_offer,
+        test.pcLocal._latest_offer.type="answer";
+        test.pcLocal.setRemoteDescriptionAndFail(test.pcLocal._latest_offer,
           function(err) {
             is(err.name, "INVALID_STATE", "Error is INVALID_STATE");
             test.next();
           } );
       }
     ]]);
 
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
@@ -21,17 +21,17 @@
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
 
     test.chain.append([[
       "PC_LOCAL_SET_REMOTE_OFFER",
       function (test) {
-        test.pcLocal.setRemoteDescriptionAndFail(test.pcLocal._last_offer,
+        test.pcLocal.setRemoteDescriptionAndFail(test.pcLocal._latest_offer,
           function(err) {
             is(err.name, "INVALID_STATE", "Error is INVALID_STATE");
             test.next();
           } );
       }
     ]]);
 
     test.run();