Bug 1017888 - Part 2: Testing for renegotiation. r=mt, r=drno
authorByron Campen [:bwc] <docfaraday@gmail.com>
Tue, 10 Feb 2015 10:17:03 -0800
changeset 256624 cf5ec9f850cde06c9e2ef679f31bdbbaa43fe904
parent 256623 e888d63dbd3307fe4830a97eafcdb7431db9f251
child 256625 95b94b9901f1ea6c996c0008d0a6f72262e69cca
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmt, drno
bugs1017888
milestone38.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 1017888 - Part 2: Testing for renegotiation. r=mt, r=drno
dom/media/tests/mochitest/dataChannel.js
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/templates.js
dom/media/tests/mochitest/test_peerConnection_addDataChannel.html
dom/media/tests/mochitest/test_peerConnection_addDataChannelNoBundle.html
dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html
dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html
dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html
media/mtransport/test/ice_unittest.cpp
media/webrtc/signaling/test/FakeMediaStreams.h
media/webrtc/signaling/test/jsep_session_unittest.cpp
media/webrtc/signaling/test/signaling_unittests.cpp
--- a/dom/media/tests/mochitest/dataChannel.js
+++ b/dom/media/tests/mochitest/dataChannel.js
@@ -12,159 +12,164 @@ function getBlobContent(blob) {
   return new Promise(resolve => {
     var reader = new FileReader();
     // Listen for 'onloadend' which will always be called after a success or failure
     reader.onloadend = event => resolve(event.target.result);
     reader.readAsText(blob);
   });
 }
 
-function addInitialDataChannel(chain) {
-  chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
-    function PC_REMOTE_EXPECT_DATA_CHANNEL(test) {
-      test.pcRemote.expectDataChannel();
-    },
+var commandsCreateDataChannel = [
+  function PC_REMOTE_EXPECT_DATA_CHANNEL(test) {
+    test.pcRemote.expectDataChannel();
+  },
 
-    function PC_LOCAL_CREATE_DATA_CHANNEL(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'");
+  function PC_LOCAL_CREATE_DATA_CHANNEL(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");
+  }
+];
 
-      is(test.pcLocal.signalingState, STABLE,
-         "Create datachannel does not change signaling state");
-    }
-  ]);
+var commandsWaitForDataChannel = [
+  function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) {
+    return test.pcLocal.dataChannels[0].opened;
+  },
 
-  chain.insertBefore('PC_LOCAL_CHECK_MEDIA_TRACKS', [
-    function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) {
-      return test.pcLocal.dataChannels[0].opened;
-    },
+  function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) {
+    return test.pcRemote.nextDataChannel.then(channel => channel.opened);
+  },
+];
 
-    function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) {
-      return test.pcRemote.nextDataChannel.then(channel => channel.opened);
-    }
-  ]);
-  chain.removeAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS');
-  chain.append([
-    function SEND_MESSAGE(test) {
-      var message = "Lorem ipsum dolor sit amet";
+var commandsCheckDataChannel = [
+  function SEND_MESSAGE(test) {
+    var message = "Lorem ipsum dolor sit amet";
+
+    return test.send(message).then(result => {
+      is(result.data, message, "Message correctly transmitted from pcLocal to pcRemote.");
+    });
+  },
 
-      return test.send(message).then(result => {
-        is(result.data, message, "Message correctly transmitted from pcLocal to pcRemote.");
-      });
-    },
-
-    function SEND_BLOB(test) {
-      var contents = ["At vero eos et accusam et justo duo dolores et ea rebum."];
-      var blob = new Blob(contents, { "type" : "text/plain" });
+  function SEND_BLOB(test) {
+    var contents = ["At vero eos et accusam et justo duo dolores et ea rebum."];
+    var blob = new Blob(contents, { "type" : "text/plain" });
 
-      return test.send(blob).then(result => {
-        ok(result.data instanceof Blob, "Received data is of instance Blob");
-        is(result.data.size, blob.size, "Received data has the correct size.");
+    return test.send(blob).then(result => {
+      ok(result.data instanceof Blob, "Received data is of instance Blob");
+      is(result.data.size, blob.size, "Received data has the correct size.");
 
-        return getBlobContent(result.data);
-      }).then(recv_contents =>
-              is(recv_contents, contents, "Received data has the correct content."));
-    },
+      return getBlobContent(result.data);
+    }).then(recv_contents =>
+            is(recv_contents, contents, "Received data has the correct content."));
+  },
 
-    function CREATE_SECOND_DATA_CHANNEL(test) {
-      return test.createDataChannel({ }).then(result => {
-        var sourceChannel = result.local;
-        var targetChannel = result.remote;
-        is(sourceChannel.readyState, "open", sourceChannel + " is in state: 'open'");
-        is(targetChannel.readyState, "open", targetChannel + " is in state: 'open'");
+  function CREATE_SECOND_DATA_CHANNEL(test) {
+    return test.createDataChannel({ }).then(result => {
+      var sourceChannel = result.local;
+      var targetChannel = result.remote;
+      is(sourceChannel.readyState, "open", sourceChannel + " is in state: 'open'");
+      is(targetChannel.readyState, "open", targetChannel + " is in state: 'open'");
 
-        is(targetChannel.binaryType, "blob", targetChannel + " is of binary type 'blob'");
-      });
-    },
+      is(targetChannel.binaryType, "blob", targetChannel + " is of binary type 'blob'");
+    });
+  },
 
-    function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) {
-      var channels = test.pcRemote.dataChannels;
-      var message = "I am the Omega";
+  function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) {
+    var channels = test.pcRemote.dataChannels;
+    var message = "I am the Omega";
 
-      return test.send(message).then(result => {
-        is(channels.indexOf(result.channel), channels.length - 1, "Last channel used");
-        is(result.data, message, "Received message has the correct content.");
-      });
-    },
+    return test.send(message).then(result => {
+      is(channels.indexOf(result.channel), channels.length - 1, "Last channel used");
+      is(result.data, message, "Received message has the correct content.");
+    });
+  },
 
 
-    function SEND_MESSAGE_THROUGH_FIRST_CHANNEL(test) {
-      var message = "Message through 1st channel";
-      var options = {
-        sourceChannel: test.pcLocal.dataChannels[0],
-        targetChannel: test.pcRemote.dataChannels[0]
-      };
+  function SEND_MESSAGE_THROUGH_FIRST_CHANNEL(test) {
+    var message = "Message through 1st channel";
+    var options = {
+      sourceChannel: test.pcLocal.dataChannels[0],
+      targetChannel: test.pcRemote.dataChannels[0]
+    };
 
-      return test.send(message, options).then(result => {
-        is(test.pcRemote.dataChannels.indexOf(result.channel), 0, "1st channel used");
-        is(result.data, message, "Received message has the correct content.");
-      });
-    },
+    return test.send(message, options).then(result => {
+      is(test.pcRemote.dataChannels.indexOf(result.channel), 0, "1st channel used");
+      is(result.data, message, "Received message has the correct content.");
+    });
+  },
 
 
-    function SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL(test) {
-      var message = "Return a message also through 1st channel";
-      var options = {
-        sourceChannel: test.pcRemote.dataChannels[0],
-        targetChannel: test.pcLocal.dataChannels[0]
-      };
+  function SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL(test) {
+    var message = "Return a message also through 1st channel";
+    var options = {
+      sourceChannel: test.pcRemote.dataChannels[0],
+      targetChannel: test.pcLocal.dataChannels[0]
+    };
 
-      return test.send(message, options).then(result => {
-        is(test.pcLocal.dataChannels.indexOf(result.channel), 0, "1st channel used");
-        is(result.data, message, "Return message has the correct content.");
-      });
-    },
+    return test.send(message, options).then(result => {
+      is(test.pcLocal.dataChannels.indexOf(result.channel), 0, "1st channel used");
+      is(result.data, message, "Return message has the correct content.");
+    });
+  },
 
-    function CREATE_NEGOTIATED_DATA_CHANNEL(test) {
-      var options = {
-        negotiated:true,
-        id: 5,
-        protocol: "foo/bar",
-        ordered: false,
-        maxRetransmits: 500
-      };
-      return test.createDataChannel(options).then(result => {
-        var sourceChannel2 = result.local;
-        var targetChannel2 = result.remote;
-        is(sourceChannel2.readyState, "open", sourceChannel2 + " is in state: 'open'");
-        is(targetChannel2.readyState, "open", targetChannel2 + " is in state: 'open'");
+  function CREATE_NEGOTIATED_DATA_CHANNEL(test) {
+    var options = {
+      negotiated:true,
+      id: 5,
+      protocol: "foo/bar",
+      ordered: false,
+      maxRetransmits: 500
+    };
+    return test.createDataChannel(options).then(result => {
+      var sourceChannel2 = result.local;
+      var targetChannel2 = result.remote;
+      is(sourceChannel2.readyState, "open", sourceChannel2 + " is in state: 'open'");
+      is(targetChannel2.readyState, "open", targetChannel2 + " is in state: 'open'");
 
-        is(targetChannel2.binaryType, "blob", targetChannel2 + " is of binary type 'blob'");
+      is(targetChannel2.binaryType, "blob", targetChannel2 + " is of binary type 'blob'");
 
-        is(sourceChannel2.id, options.id, sourceChannel2 + " id is:" + sourceChannel2.id);
-        var reliable = !options.ordered ? false : (options.maxRetransmits || options.maxRetransmitTime);
-        is(sourceChannel2.protocol, options.protocol, sourceChannel2 + " protocol is:" + sourceChannel2.protocol);
-        is(sourceChannel2.reliable, reliable, sourceChannel2 + " reliable is:" + sourceChannel2.reliable);
-        /*
-          These aren't exposed by IDL yet
-          is(sourceChannel2.ordered, options.ordered, sourceChannel2 + " ordered is:" + sourceChannel2.ordered);
-          is(sourceChannel2.maxRetransmits, options.maxRetransmits, sourceChannel2 + " maxRetransmits is:" +
-          sourceChannel2.maxRetransmits);
-          is(sourceChannel2.maxRetransmitTime, options.maxRetransmitTime, sourceChannel2 + " maxRetransmitTime is:" +
-          sourceChannel2.maxRetransmitTime);
-        */
+      is(sourceChannel2.id, options.id, sourceChannel2 + " id is:" + sourceChannel2.id);
+      var reliable = !options.ordered ? false : (options.maxRetransmits || options.maxRetransmitTime);
+      is(sourceChannel2.protocol, options.protocol, sourceChannel2 + " protocol is:" + sourceChannel2.protocol);
+      is(sourceChannel2.reliable, reliable, sourceChannel2 + " reliable is:" + sourceChannel2.reliable);
+      /*
+        These aren't exposed by IDL yet
+        is(sourceChannel2.ordered, options.ordered, sourceChannel2 + " ordered is:" + sourceChannel2.ordered);
+        is(sourceChannel2.maxRetransmits, options.maxRetransmits, sourceChannel2 + " maxRetransmits is:" +
+        sourceChannel2.maxRetransmits);
+        is(sourceChannel2.maxRetransmitTime, options.maxRetransmitTime, sourceChannel2 + " maxRetransmitTime is:" +
+        sourceChannel2.maxRetransmitTime);
+      */
 
-        is(targetChannel2.id, options.id, targetChannel2 + " id is:" + targetChannel2.id);
-        is(targetChannel2.protocol, options.protocol, targetChannel2 + " protocol is:" + targetChannel2.protocol);
-        is(targetChannel2.reliable, reliable, targetChannel2 + " reliable is:" + targetChannel2.reliable);
-        /*
-          These aren't exposed by IDL yet
-          is(targetChannel2.ordered, options.ordered, targetChannel2 + " ordered is:" + targetChannel2.ordered);
-          is(targetChannel2.maxRetransmits, options.maxRetransmits, targetChannel2 + " maxRetransmits is:" +
-          targetChannel2.maxRetransmits);
-          is(targetChannel2.maxRetransmitTime, options.maxRetransmitTime, targetChannel2 + " maxRetransmitTime is:" +
-          targetChannel2.maxRetransmitTime);
-        */
-      });
-    },
+      is(targetChannel2.id, options.id, targetChannel2 + " id is:" + targetChannel2.id);
+      is(targetChannel2.protocol, options.protocol, targetChannel2 + " protocol is:" + targetChannel2.protocol);
+      is(targetChannel2.reliable, reliable, targetChannel2 + " reliable is:" + targetChannel2.reliable);
+      /*
+        These aren't exposed by IDL yet
+        is(targetChannel2.ordered, options.ordered, targetChannel2 + " ordered is:" + targetChannel2.ordered);
+        is(targetChannel2.maxRetransmits, options.maxRetransmits, targetChannel2 + " maxRetransmits is:" +
+        targetChannel2.maxRetransmits);
+        is(targetChannel2.maxRetransmitTime, options.maxRetransmitTime, targetChannel2 + " maxRetransmitTime is:" +
+        targetChannel2.maxRetransmitTime);
+      */
+    });
+  },
 
-    function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
-      var channels = test.pcRemote.dataChannels;
-      var message = "I am the walrus; Goo goo g'joob";
+  function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
+    var channels = test.pcRemote.dataChannels;
+    var message = "I am the walrus; Goo goo g'joob";
 
-      return test.send(message).then(result => {
-        is(channels.indexOf(result.channel), channels.length - 1, "Last channel used");
-        is(result.data, message, "Received message has the correct content.");
-      });
-    }
-  ]);
+    return test.send(message).then(result => {
+      is(channels.indexOf(result.channel), channels.length - 1, "Last channel used");
+      is(result.data, message, "Received message has the correct content.");
+    });
+  }
+];
+
+function addInitialDataChannel(chain) {
+  chain.insertBefore('PC_LOCAL_CREATE_OFFER', commandsCreateDataChannel);
+  chain.insertBefore('PC_LOCAL_CHECK_MEDIA_TRACKS', commandsWaitForDataChannel);
+  chain.removeAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS');
+  chain.append(commandsCheckDataChannel);
 }
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -358,46 +358,68 @@ CommandChain.prototype = {
    * Add new commands to the end of the chain
    */
   append: function(commands) {
     this.commands = this.commands.concat(commands);
   },
 
   /**
    * Returns the index of the specified command in the chain.
+   * @param {start} Optional param specifying the index at which the search will
+   * start. If not specified, the search starts at index 0.
    */
-  indexOf: function(functionOrName) {
+  indexOf: function(functionOrName, start) {
+    start = start || 0;
     if (typeof functionOrName === 'string') {
-      return this.commands.findIndex(f => f.name === functionOrName);
+      var index = this.commands.slice(start).findIndex(f => f.name === functionOrName);
+      if (index !== -1) {
+        index += start;
+      }
+      return index;
     }
-    return this.commands.indexOf(functionOrName);
+    return this.commands.indexOf(functionOrName, start);
   },
 
   /**
    * Inserts the new commands after the specified command.
    */
   insertAfter: function(functionOrName, commands) {
     this._insertHelper(functionOrName, commands, 1);
   },
 
   /**
+   * Inserts the new commands after every occurrence of the specified command
+   */
+  insertAfterEach: function(functionOrName, commands) {
+    this._insertHelper(functionOrName, commands, 1, true);
+  },
+
+  /**
    * Inserts the new commands before the specified command.
    */
-  insertBefore: function(functionOrName, commands) {
-    this._insertHelper(functionOrName, commands, 0);
+  insertBefore: function(functionOrName, commands, all, start) {
+    this._insertHelper(functionOrName, commands, 0, all, start);
   },
 
-  _insertHelper: function(functionOrName, commands, delta) {
+  _insertHelper: function(functionOrName, commands, delta, all, start) {
     var index = this.indexOf(functionOrName);
-
-    if (index >= 0) {
-      this.commands = [].concat(
-        this.commands.slice(0, index + delta),
-        commands,
-        this.commands.slice(index + delta));
+    start = start || 0;
+    for (; index !== -1; index = this.indexOf(functionOrName, index)) {
+      if (!start) {
+        this.commands = [].concat(
+          this.commands.slice(0, index + delta),
+          commands,
+          this.commands.slice(index + delta));
+        if (!all) {
+          break;
+        }
+      } else {
+        start -= 1;
+      }
+      index += (commands.length + 1);
     }
   },
 
   /**
    * Removes the specified command, returns what was removed.
    */
   remove: function(functionOrName) {
     var index = this.indexOf(functionOrName);
@@ -455,17 +477,17 @@ CommandChain.prototype = {
     return oldCommands;
   },
 
   /**
    * Remove all commands whose name match the specified regex.
    */
   filterOut: function (id_match) {
     this.commands = this.commands.filter(c => !id_match.test(c.name));
-  }
+  },
 };
 
 
 function IsMacOSX10_6orOlder() {
   if (navigator.platform.indexOf("Mac") !== 0) {
     return false;
   }
 
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -146,11 +146,36 @@ skip-if = toolkit == 'gonk' # b2g (Bug 1
 skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 [test_peerConnection_twoAudioVideoStreams.html]
 skip-if = (toolkit == 'gonk' || (e10s && debug)) # b2g (Bug 1059867) or fd exhaustion on e10s debug intermittent (Bug 1126078)
 [test_peerConnection_twoAudioVideoStreamsCombined.html]
 skip-if = (toolkit == 'gonk' || (e10s && debug)) # b2g (Bug 1059867) or fd exhaustion on e10s debug intermittent (Bug 1126078)
 [test_peerConnection_twoVideoStreams.html]
 skip-if = (toolkit == 'gonk' || (e10s && debug)) # b2g (Bug 1059867) or fd exhaustion on e10s debug intermittent (Bug 1126078)
 [test_peerConnection_addSecondAudioStream.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_answererAddSecondAudioStream.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_removeAudioTrack.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_removeThenAddAudioTrack.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_addSecondVideoStream.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_removeVideoTrack.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_removeThenAddVideoTrack.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_addSecondAudioStreamNoBundle.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_removeThenAddAudioTrackNoBundle.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_addSecondVideoStreamNoBundle.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_removeThenAddVideoTrackNoBundle.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_addDataChannel.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
+[test_peerConnection_addDataChannelNoBundle.html]
+skip-if = toolkit == 'gonk' # b2g (Bug 1059867)
 
 # Bug 950317: Hack for making a cleanup hook after finishing all WebRTC cases
 [test_zmedia_cleanup.html]
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -116,16 +116,22 @@ function removeVP8(sdp) {
   updated_sdp = updated_sdp.replace("RTP/SAVPF 120 126 97\r\n","RTP/SAVPF 126 97\r\n");
   updated_sdp = updated_sdp.replace("RTP/SAVPF 120 126\r\n","RTP/SAVPF 126\r\n");
   updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack\r\n","");
   updated_sdp = updated_sdp.replace("a=rtcp-fb:120 nack pli\r\n","");
   updated_sdp = updated_sdp.replace("a=rtcp-fb:120 ccm fir\r\n","");
   return updated_sdp;
 }
 
+var makeDefaultCommands = () => {
+  return [].concat(commandsPeerConnectionInitial,
+                   commandsGetUserMedia,
+                   commandsPeerConnectionOfferAnswer);
+};
+
 /**
  * This class handles tests for peer connections.
  *
  * @constructor
  * @param {object} [options={}]
  *        Optional options for the peer connection test
  * @param {object} [options.commands=commandsPeerConnection]
  *        Commands to run for the test
@@ -137,17 +143,17 @@ function removeVP8(sdp) {
  *        Configuration for the local peer connection instance
  * @param {object} [options.config_remote=undefined]
  *        Configuration for the remote peer connection instance. If not defined
  *        the configuration from the local instance will be used
  */
 function PeerConnectionTest(options) {
   // If no options are specified make it an empty object
   options = options || { };
-  options.commands = options.commands || commandsPeerConnection;
+  options.commands = options.commands || makeDefaultCommands();
   options.is_local = "is_local" in options ? options.is_local : true;
   options.is_remote = "is_remote" in options ? options.is_remote : true;
 
   if (typeof turnServers !== "undefined") {
     if ((!options.turn_disabled_local) && (turnServers.local)) {
       if (!options.hasOwnProperty("config_local")) {
         options.config_local = {};
       }
@@ -735,25 +741,33 @@ function PeerConnectionWrapper(label, co
 
   this.constraints = [ ];
   this.offerOptions = {};
   this.streams = [ ];
   this.mediaCheckers = [ ];
 
   this.dataChannels = [ ];
 
-  this.addStreamCounter = {audio: 0, video: 0 };
-
   this._local_ice_candidates = [];
   this._remote_ice_candidates = [];
   this.holdIceCandidates = new Promise(r => this.releaseIceCandidates = r);
   this.localRequiresTrickleIce = false;
   this.remoteRequiresTrickleIce = false;
   this.localMediaElements = [];
 
+  this.expectedLocalTrackTypesById = {};
+  this.expectedRemoteTrackTypesById = {};
+  this.observedRemoteTrackTypesById = {};
+
+  this.disableRtpCountChecking = false;
+
+  this.negotiationNeededFired = false;
+
+  this.iceCheckingRestartExpected = false;
+
   this.h264 = typeof h264 !== "undefined" ? true : false;
 
   info("Creating " + this);
   this._pc = new mozRTCPeerConnection(this.configuration);
 
   /**
    * Setup callback handlers
    */
@@ -764,44 +778,24 @@ function PeerConnectionWrapper(label, co
     isnot(typeof this._pc.iceConnectionState, "undefined",
           "iceConnectionState should not be undefined");
     info(this + ": oniceconnectionstatechange fired, new state is: " + this._pc.iceConnectionState);
     Object.keys(this.ice_connection_callbacks).forEach(name => {
       this.ice_connection_callbacks[name]();
     });
   };
 
-  /**
-   * Callback for native peer connection 'onaddstream' events.
-   *
-   * @param {Object} event
-   *        Event data which includes the stream to be added
-   */
-  this._pc.onaddstream = event => {
-    info(this + ": 'onaddstream' event fired for " + JSON.stringify(event.stream));
-
-    var type = '';
-    if (event.stream.getAudioTracks().length > 0) {
-      type = 'audio';
-      this.addStreamCounter.audio += this.countTracksInStreams('audio', [event.stream]);
-    }
-    if (event.stream.getVideoTracks().length > 0) {
-      type += 'video';
-      this.addStreamCounter.video += this.countTracksInStreams('video', [event.stream]);
-    }
-    this.attachMedia(event.stream, type, 'remote');
-  };
-
   createOneShotEventWrapper(this, this._pc, 'datachannel');
   this._pc.addEventListener('datachannel', e => {
     var wrapper = new DataChannelWrapper(e.channel, this);
     this.dataChannels.push(wrapper);
   });
 
   createOneShotEventWrapper(this, this._pc, 'signalingstatechange');
+  createOneShotEventWrapper(this, this._pc, 'negotiationneeded');
 }
 
 PeerConnectionWrapper.prototype = {
 
   /**
    * Returns the local description.
    *
    * @returns {object} The local description
@@ -883,30 +877,42 @@ PeerConnectionWrapper.prototype = {
         ok(this._pc.getSenders().find(sender => sender.track == stream.getVideoTracks()[0]),
            "addStream adds sender");
       } else {
         stream.getTracks().forEach(track => {
           var sender = this._pc.addTrack(track, stream);
           is(sender.track, track, "addTrack returns sender");
         });
       }
+
+      stream.getTracks().forEach(track => {
+        ok(track.id, "track has id");
+        ok(track.kind, "track has kind");
+        this.expectedLocalTrackTypesById[track.id] = track.kind;
+      });
     }
 
     var element = createMediaElement(type, this.label + '_' + side + this.streams.length);
     this.mediaCheckers.push(new MediaElementChecker(element));
     element.mozSrcObject = stream;
     element.play();
 
     // Store local media elements so that we can stop them when done.
     // Don't store remote ones because they should stop when the PC does.
     if (side === 'local') {
       this.localMediaElements.push(element);
     }
   },
 
+  removeSender : function(index) {
+    var sender = this._pc.getSenders()[index];
+    delete this.expectedLocalTrackTypesById[sender.track.id];
+    this._pc.removeTrack(sender);
+  },
+
   /**
    * Requests all the media streams as specified in the constrains property.
    *
    * @param {array} constraintsList
    *        Array of constraints for GUM calls
    */
   getAllUserMedia : function(constraintsList) {
     if (constraintsList.length === 0) {
@@ -1064,16 +1070,69 @@ PeerConnectionWrapper.prototype = {
       } else {
         ok(false, this + ": old signaling state " + oldstate + " missing in signaling transition array");
       }
       this.signalingStateLog.push(newstate);
     });
   },
 
   /**
+   * Checks whether a given track is expected, has not been observed yet, and
+   * is of the correct type. Then, moves the track from
+   * |expectedTrackTypesById| to |observedTrackTypesById|.
+   */
+  checkTrackIsExpected : function(track,
+                                  expectedTrackTypesById,
+                                  observedTrackTypesById) {
+    ok(expectedTrackTypesById[track.id], "track id " + track.id + " was expected");
+    ok(!observedTrackTypesById[track.id], "track id " + track.id + " was not yet observed");
+    var observedKind = track.kind;
+    var expectedKind = expectedTrackTypesById[track.id];
+    is(observedKind, expectedKind,
+        "track id " + track.id + " was of kind " +
+        observedKind + ", which matches " + expectedKind);
+    observedTrackTypesById[track.id] = expectedTrackTypesById[track.id];
+    delete expectedTrackTypesById[track.id];
+  },
+
+  setupAddStreamEventHandler: function() {
+    var resolveAllAddStreamEventsDone;
+
+    // checkMediaTracks waits on this promise later on in the test.
+    this.allAddStreamEventsDonePromise =
+      new Promise(resolve => resolveAllAddStreamEventsDone = resolve);
+
+    this._pc.addEventListener('addstream', event => {
+      info(this + ": 'onaddstream' event fired for " + JSON.stringify(event.stream));
+
+      // TODO(bug 1130185): We need to handle addtrack events once we start
+      // testing addTrack on pre-existing streams.
+
+      event.stream.getTracks().forEach(track => {
+        this.checkTrackIsExpected(track,
+                                  this.expectedRemoteTrackTypesById,
+                                  this.observedRemoteTrackTypesById);
+      });
+
+      if (Object.keys(this.expectedRemoteTrackTypesById).length === 0) {
+        resolveAllAddStreamEventsDone();
+      }
+
+      var type = '';
+      if (event.stream.getAudioTracks().length > 0) {
+        type = 'audio';
+      }
+      if (event.stream.getVideoTracks().length > 0) {
+        type += 'video';
+      }
+      this.attachMedia(event.stream, type, 'remote');
+    });
+  },
+
+  /**
    * 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(candidate) {
     this._remote_ice_candidates.push(candidate);
@@ -1143,17 +1202,24 @@ PeerConnectionWrapper.prototype = {
    * appends the new state to an array for logging it later.
    */
   logIceConnectionState: function() {
     this.iceConnectionLog = [this._pc.iceConnectionState];
     this.ice_connection_callbacks.logIceStatus = () => {
       var newstate = this._pc.iceConnectionState;
       var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1]
       if (Object.keys(iceStateTransitions).indexOf(oldstate) != -1) {
-        ok(iceStateTransitions[oldstate].indexOf(newstate) != -1, this + ": legal ICE state transition from " + oldstate + " to " + newstate);
+        if (this.iceCheckingRestartExpected) {
+          is(newstate, "checking",
+             "iceconnectionstate event \'" + newstate +
+             "\' matches expected state \'checking\'");
+          this.iceCheckingRestartExpected = false;
+        } else {
+          ok(iceStateTransitions[oldstate].indexOf(newstate) != -1, this + ": legal ICE state transition from " + oldstate + " to " + newstate);
+        }
       } else {
         ok(false, this + ": old ICE state " + oldstate + " missing in ICE transition array");
       }
       this.iceConnectionLog.push(newstate);
     };
   },
 
   /**
@@ -1279,77 +1345,55 @@ PeerConnectionWrapper.prototype = {
 
     if (offerToReceiveVideo) {
       return 1;
     } else {
       return 0;
     }
   },
 
-  /*
-   * Counts the amount of tracks of the given type in a set of streams.
-   *
-   * @param type audio|video
-   * @param streams
-   *        An array of streams (as returned by getLocalStreams()) to be
-   *        examined.
-   */
-  countTracksInStreams: function(type, streams) {
-    if (!Array.isArray(streams)) {
-      return 0;
-    }
-    var f = (type === 'video') ? "getVideoTracks" : "getAudioTracks";
+  checkLocalMediaTracks : function() {
+    var observedLocalTrackTypesById = {};
+    // We do not want to empty out this.expectedLocalTrackTypesById, so make a
+    // copy.
+    var expectedLocalTrackTypesById =
+      JSON.parse(JSON.stringify((this.expectedLocalTrackTypesById)));
+    info(this + " Checking local tracks " +
+         JSON.stringify(expectedLocalTrackTypesById));
+    this._pc.getLocalStreams().forEach(stream => {
+      stream.getTracks().forEach(track => {
+        this.checkTrackIsExpected(track,
+                                  expectedLocalTrackTypesById,
+                                  observedLocalTrackTypesById);
+      });
+    });
 
-    return streams.reduce((count, st) => {
-      return count + st[f]().length;
-    }, 0);
+    Object.keys(expectedLocalTrackTypesById).forEach(id => {
+      ok(false, this + " local id " + id + " was observed");
+    });
   },
 
   /**
    * Checks that we are getting the media tracks we expect.
    *
    * @param {object} constraints
    *        The media constraints of the remote peer connection object
    */
-  checkMediaTracks : function(remoteConstraints) {
-    var waitForExpectedTracks = type => {
-      var outstandingCount = this.countTracksInConstraint(type, remoteConstraints);
-      outstandingCount -= this.addStreamCounter[type];
-      if (outstandingCount <= 0) {
-        return Promise.resolve();
-      }
+  checkMediaTracks : function() {
+    this.checkLocalMediaTracks();
 
-      return new Promise(resolve => {
-        this._pc.addEventListener('addstream', e => {
-          outstandingCount -= this.countTracksInStreams(type, [e.stream]);
-          if (outstandingCount <= 0) {
-            resolve();
-          }
-        });
-      });
-    };
+    info(this + " Checking remote tracks " +
+         JSON.stringify(this.expectedRemoteTrackTypesById));
 
-    var checkTrackCounts = (side, streams, constraints) => {
-      ['audio', 'video'].forEach(type => {
-        var actual = this.countTracksInStreams(type, streams);
-        var expected = this.countTracksInConstraint(type, constraints);
-        is(actual, expected, this + ' has ' + actual + ' ' +
-           side + ' ' + type + ' tracks');
-      });
-    };
+    // No tracks are expected
+    if (Object.keys(this.expectedRemoteTrackTypesById).length === 0) {
+      return;
+    }
 
-    info(this + " checkMediaTracks() got called before onAddStream fired");
-    var checkPromise = Promise.all([
-      waitForExpectedTracks('audio'),
-      waitForExpectedTracks('video')
-    ]).then(() => {
-      checkTrackCounts('local', this._pc.getLocalStreams(), this.constraints);
-      checkTrackCounts('remote', this._pc.getRemoteStreams(), remoteConstraints);
-    });
-    return timerGuard(checkPromise, 60000, "onaddstream never fired");
+    return timerGuard(this.allAddStreamEventsDonePromise, 60000, "onaddstream never fired");
   },
 
   checkMsids: function() {
     var checkSdpForMsids = (desc, streams, side) => {
       streams.forEach(stream => {
         stream.getTracks().forEach(track => {
           // TODO(bug 1089798): Once DOMMediaStream has an id field, we
           // should be verifying that the SDP contains
@@ -1510,20 +1554,22 @@ PeerConnectionWrapper.prototype = {
               }
               if (res.remoteId) {
                 var rem = stats[res.remoteId];
                 ok(rem.isRemote, "Remote is rtcp");
                 ok(rem.remoteId == res.id, "Remote backlink match");
                 if(res.type == "outboundrtp") {
                   ok(rem.type == "inboundrtp", "Rtcp is inbound");
                   ok(rem.packetsReceived !== undefined, "Rtcp packetsReceived");
-                  ok(rem.packetsReceived <= res.packetsSent, "No more than sent");
                   ok(rem.packetsLost !== undefined, "Rtcp packetsLost");
                   ok(rem.bytesReceived >= rem.packetsReceived, "Rtcp bytesReceived");
-                  ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
+                  if (!this.disableRtpCountChecking) {
+                    ok(rem.packetsReceived <= res.packetsSent, "No more than sent packets");
+                    ok(rem.bytesReceived <= res.bytesSent, "No more than sent bytes");
+                  }
                   ok(rem.jitter !== undefined, "Rtcp jitter");
                   ok(rem.mozRtt !== undefined, "Rtcp rtt");
                   ok(rem.mozRtt >= 0, "Rtcp rtt " + rem.mozRtt + " >= 0");
                   ok(rem.mozRtt < 60000, "Rtcp rtt " + rem.mozRtt + " < 1 min");
                 } else {
                   ok(rem.type == "outboundrtp", "Rtcp is outbound");
                   ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
                   // We may have received more than outdated Rtcp packetsSent
--- a/dom/media/tests/mochitest/templates.js
+++ b/dom/media/tests/mochitest/templates.js
@@ -58,27 +58,29 @@ function dumpSdp(test) {
     (typeof test.pcRemote.setLocalDescDate !== 'undefined') &&
     (typeof test.pcRemote.setLocalDescStableEventDate !== 'undefined')) {
     var delta = deltaSeconds(test.pcRemote.setLocalDescDate, test.pcRemote.setLocalDescStableEventDate);
     dump("Delay between pcRemote.setLocal <-> pcRemote.signalingStateStable: " + delta + "\n");
   }
 }
 
 function waitForIceConnected(test, pc) {
-  if (pc.isIceConnected()) {
-    info(pc + ": ICE connection state log: " + pc.iceConnectionLog);
-    ok(true, pc + ": ICE is in connected state");
-    return Promise.resolve();
-  }
+  if (!pc.iceCheckingRestartExpected) {
+    if (pc.isIceConnected()) {
+      info(pc + ": ICE connection state log: " + pc.iceConnectionLog);
+      ok(true, pc + ": ICE is in connected state");
+      return Promise.resolve();
+    }
 
-  if (!pc.isIceConnectionPending()) {
-    dumpSdp(test);
-    var details = pc + ": ICE is already in bad state: " + pc.iceConnectionState;
-    ok(false, details);
-    return Promise.reject(new Error(details));
+    if (!pc.isIceConnectionPending()) {
+      dumpSdp(test);
+      var details = pc + ": ICE is already in bad state: " + pc.iceConnectionState;
+      ok(false, details);
+      return Promise.reject(new Error(details));
+    }
   }
 
   return pc.waitForIceConnected()
     .then(() => {
       info(pc + ": ICE connection state log: " + pc.iceConnectionLog);
       ok(pc.isIceConnected(), pc + ": ICE switched to 'connected' state");
     });
 }
@@ -130,17 +132,19 @@ function checkTrackStats(pc, audio, outb
     }), msg + "3");
   });
 }
 
 // checks all stats combinations inbound/outbound, audio/video
 var checkAllTrackStats = pc =>
     Promise.all([0, 1, 2, 3].map(i => checkTrackStats(pc, i & 1, i & 2)));
 
-var commandsPeerConnection = [
+// Commands run once at the beginning of each test, even when performing a
+// renegotiation test.
+var commandsPeerConnectionInitial = [
   function PC_SETUP_SIGNALING_CLIENT(test) {
     if (test.steeplechase) {
       setTimeout(() => {
         ok(false, "PeerConnectionTest timed out");
         test.teardown();
       }, 30000);
       test.setupSignalingClient();
       test.registerSignalingCallback("ice_candidate", function (message) {
@@ -164,22 +168,22 @@ var commandsPeerConnection = [
   function PC_LOCAL_SETUP_SIGNALING_LOGGER(test) {
     test.pcLocal.logSignalingState();
   },
 
   function PC_REMOTE_SETUP_SIGNALING_LOGGER(test) {
     test.pcRemote.logSignalingState();
   },
 
-  function PC_LOCAL_GUM(test) {
-    return test.pcLocal.getAllUserMedia(test.pcLocal.constraints);
+  function PC_LOCAL_SETUP_ADDSTREAM_HANDLER(test) {
+    test.pcLocal.setupAddStreamEventHandler();
   },
 
-  function PC_REMOTE_GUM(test) {
-    return test.pcRemote.getAllUserMedia(test.pcRemote.constraints);
+  function PC_REMOTE_SETUP_ADDSTREAM_HANDLER(test) {
+    test.pcRemote.setupAddStreamEventHandler();
   },
 
   function PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE(test) {
     is(test.pcLocal.signalingState, STABLE,
        "Initial local signalingState is 'stable'");
   },
 
   function PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE(test) {
@@ -192,16 +196,44 @@ var commandsPeerConnection = [
        "Initial local ICE connection state is 'new'");
   },
 
   function PC_REMOTE_CHECK_INITIAL_ICE_STATE(test) {
     is(test.pcRemote.iceConnectionState, ICE_NEW,
        "Initial remote ICE connection state is 'new'");
   },
 
+];
+
+var commandsGetUserMedia = [
+  function PC_LOCAL_GUM(test) {
+    return test.pcLocal.getAllUserMedia(test.pcLocal.constraints);
+  },
+
+  function PC_REMOTE_GUM(test) {
+    return test.pcRemote.getAllUserMedia(test.pcRemote.constraints);
+  },
+];
+
+var commandsBeforeRenegotiation = [
+  function PC_LOCAL_SETUP_NEGOTIATION_CALLBACK(test) {
+    test.pcLocal.onnegotiationneeded = event => {
+      test.pcLocal.negotiationNeededFired = true;
+    };
+  },
+];
+
+var commandsAfterRenegotiation = [
+  function PC_LOCAL_CHECK_NEGOTIATION_CALLBACK(test) {
+    ok(test.pcLocal.negotiationNeededFired, "Expected negotiationneeded event");
+    test.pcLocal.negotiationNeededFired = false;
+  },
+];
+
+var commandsPeerConnectionOfferAnswer = [
   function PC_LOCAL_SETUP_ICE_HANDLER(test) {
     test.pcLocal.setupIceCandidateHandler(test);
     if (test.steeplechase) {
       test.pcLocal.endOfTrickleIce.then(() => {
         send_message({"type": "end_of_trickle_ice"});
       });
     }
   },
@@ -210,16 +242,66 @@ var commandsPeerConnection = [
     test.pcRemote.setupIceCandidateHandler(test);
     if (test.steeplechase) {
       test.pcRemote.endOfTrickleIce.then(() => {
         send_message({"type": "end_of_trickle_ice"});
       });
     }
   },
 
+  function PC_LOCAL_STEEPLECHASE_SIGNAL_EXPECTED_LOCAL_TRACKS(test) {
+    if (test.steeplechase) {
+      send_message({"type": "local_expected_tracks",
+                    "expected_tracks": test.pcLocal.expectedLocalTrackTypesById});
+    }
+  },
+
+  function PC_REMOTE_STEEPLECHASE_SIGNAL_EXPECTED_LOCAL_TRACKS(test) {
+    if (test.steeplechase) {
+      send_message({"type": "remote_expected_tracks",
+                    "expected_tracks": test.pcRemote.expectedLocalTrackTypesById});
+    }
+  },
+
+  function PC_LOCAL_GET_EXPECTED_REMOTE_TRACKS(test) {
+    if (test.steeplechase) {
+      return test.getSignalingMessage("remote_expected_tracks").then(
+          message => {
+            test.pcLocal.expectedRemoteTrackTypesById = message.expected_tracks;
+          });
+    } else {
+      // Deep copy, as similar to steeplechase as possible
+      test.pcLocal.expectedRemoteTrackTypesById =
+        JSON.parse(JSON.stringify((test.pcRemote.expectedLocalTrackTypesById)));
+    }
+
+    // Remove what we've already observed
+    Object.keys(test.pcLocal.observedRemoteTrackTypesById).forEach(id => {
+      delete test.pcLocal.expectedRemoteTrackTypesById[id];
+    });
+  },
+
+  function PC_LOCAL_GET_EXPECTED_REMOTE_TRACKS(test) {
+    if (test.steeplechase) {
+      return test.getSignalingMessage("local_expected_tracks").then(
+          message => {
+            test.pcRemote.expectedRemoteTrackTypesById = message.expected_tracks;
+          });
+    } else {
+      // Deep copy, as similar to steeplechase as possible
+      test.pcRemote.expectedRemoteTrackTypesById =
+        JSON.parse(JSON.stringify((test.pcLocal.expectedLocalTrackTypesById)));
+    }
+
+    // Remove what we've already observed
+    Object.keys(test.pcRemote.observedRemoteTrackTypesById).forEach(id => {
+      delete test.pcRemote.expectedRemoteTrackTypesById[id];
+    });
+  },
+
   function PC_LOCAL_CREATE_OFFER(test) {
     return test.createOffer(test.pcLocal).then(offer => {
       is(test.pcLocal.signalingState, STABLE,
          "Local create offer does not change signaling state");
     });
   },
 
   function PC_LOCAL_STEEPLECHASE_SIGNAL_OFFER(test) {
@@ -385,21 +467,21 @@ var commandsPeerConnection = [
     return waitForAnIceCandidate(test.pcLocal);
   },
 
   function PC_REMOTE_VERIFY_ICE_GATHERING(test) {
     return waitForAnIceCandidate(test.pcRemote);
   },
 
   function PC_LOCAL_CHECK_MEDIA_TRACKS(test) {
-    return test.pcLocal.checkMediaTracks(test._answer_constraints);
+    return test.pcLocal.checkMediaTracks();
   },
 
   function PC_REMOTE_CHECK_MEDIA_TRACKS(test) {
-    return test.pcRemote.checkMediaTracks(test._offer_constraints);
+    return test.pcRemote.checkMediaTracks();
   },
 
   function PC_LOCAL_CHECK_MEDIA_FLOW_PRESENT(test) {
     return test.pcLocal.checkMediaFlowPresent();
   },
 
   function PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT(test) {
     return test.pcRemote.checkMediaFlowPresent();
@@ -463,8 +545,37 @@ var commandsPeerConnection = [
 
   function PC_LOCAL_CHECK_STATS(test) {
     return checkAllTrackStats(test.pcLocal);
   },
   function PC_REMOTE_CHECK_STATS(test) {
     return checkAllTrackStats(test.pcRemote);
   }
 ];
+
+function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
+  test.originalOffer.sdp = test.originalOffer.sdp.replace(
+      /a=group:BUNDLE .*\r\n/g,
+      ""
+      );
+  info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
+};
+
+var addRenegotiation = (chain, commands, checks) => {
+  chain.append(commandsBeforeRenegotiation);
+  chain.append(commands);
+  chain.append(commandsAfterRenegotiation);
+  chain.append(commandsPeerConnectionOfferAnswer);
+  if (checks) {
+    chain.append(checks);
+  }
+};
+
+var addRenegotiationAnswerer = (chain, commands, checks) => {
+  chain.append(function SWAP_PC_LOCAL_PC_REMOTE(test) {
+    var temp = test.pcLocal;
+    test.pcLocal = test.pcRemote;
+    test.pcRemote = temp;
+  });
+  addRenegotiation(chain, commands, checks);
+};
+
+
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addDataChannel.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: add DataChannel"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+                     commandsCreateDataChannel,
+                     commandsCheckDataChannel);
+
+    // Insert before the second PC_LOCAL_CHECK_MEDIA_TRACKS
+    test.chain.insertBefore('PC_LOCAL_CHECK_MEDIA_TRACKS',
+                            commandsWaitForDataChannel,
+                            false,
+                            1);
+
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addDataChannelNoBundle.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: add DataChannel"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+                     commandsCreateDataChannel.concat(
+                       [
+                         function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+                           test.pcLocal.iceCheckingRestartExpected = true;
+                         },
+                         function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+                           test.pcRemote.iceCheckingRestartExpected = true;
+                         },
+                       ]
+                      ),
+                     commandsCheckDataChannel);
+
+    test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+                              PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+    // Insert before the second PC_LOCAL_CHECK_MEDIA_TRACKS
+    test.chain.insertBefore('PC_LOCAL_CHECK_MEDIA_TRACKS',
+                            commandsWaitForDataChannel,
+                            false,
+                            1);
+
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
@@ -2,58 +2,33 @@
 <html>
 <head>
   <script type="application/javascript" src="pc.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
-    bug: "1091242",
+    bug: "1017888",
     title: "Renegotiation: add second audio stream"
   });
 
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
-    test.chain.append([
-      function PC_LOCAL_SETUP_NEGOTIATION_CALLBACK(test) {
-        test.pcLocal.onNegotiationneededFired = false;
-        test.pcLocal._pc.onnegotiationneeded = anEvent => {
-          info("pcLocal.onnegotiationneeded fired");
-          test.pcLocal.onNegotiationneededFired = true;
-        };
-      },
-      function PC_LOCAL_ADD_SECOND_STREAM(test) {
-        return test.pcLocal.getAllUserMedia([{audio: true}]);
-      },
-      function PC_LOCAL_CREATE_NEW_OFFER(test) {
-        ok(test.pcLocal.onNegotiationneededFired, "onnegotiationneeded");
-        return test.createOffer(test.pcLocal).then(offer => {
-          test._new_offer = offer;
-        });
-      },
-      function PC_LOCAL_SET_NEW_LOCAL_DESCRIPTION(test) {
-        return test.setLocalDescription(test.pcLocal, test._new_offer, HAVE_LOCAL_OFFER);
-      },
-      function PC_REMOTE_SET_NEW_REMOTE_DESCRIPTION(test) {
-        return test.setRemoteDescription(test.pcRemote, test._new_offer, HAVE_REMOTE_OFFER);
-      },
-      function PC_REMOTE_CREATE_NEW_ANSWER(test) {
-        return test.createAnswer(test.pcRemote).then(answer => {
-          test._new_answer = answer;
-        });
-      },
-      function PC_REMOTE_SET_NEW_LOCAL_DESCRIPTION(test) {
-        return test.setLocalDescription(test.pcRemote, test._new_answer, STABLE);
-      },
-      function PC_LOCAL_SET_NEW_REMOTE_DESCRIPTION(test) {
-        return test.setRemoteDescription(test.pcLocal, test._new_answer, STABLE);
-      }
-      // TODO(bug 1093835): figure out how to verify if media flows through the new stream
-    ]);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_ADD_SECOND_STREAM(test) {
+          test.setMediaConstraints([{audio: true}, {audio: true}],
+                                   [{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
+        },
+      ]
+    );
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: add second audio stream, no bundle"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_ADD_SECOND_STREAM(test) {
+          test.setMediaConstraints([{audio: true}, {audio: true}],
+                                   [{audio: true}]);
+          // Since this is a NoBundle variant, adding a track will cause us to
+          // go back to checking.
+          test.pcLocal.iceCheckingRestartExpected = true;
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
+        },
+        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+          test.pcRemote.iceCheckingRestartExpected = true;
+        },
+      ]
+    );
+
+    test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+                              PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: add second video stream"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_ADD_SECOND_STREAM(test) {
+          test.setMediaConstraints([{video: true}, {video: true}],
+                                   [{video: true}]);
+          return test.pcLocal.getAllUserMedia([{video: true}]);
+        },
+      ]
+    );
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{video: true}], [{video: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: add second video stream, no bundle"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_ADD_SECOND_STREAM(test) {
+          test.setMediaConstraints([{video: true}, {video: true}],
+                                   [{video: true}]);
+          // Since this is a NoBundle variant, adding a track will cause us to
+          // go back to checking.
+          test.pcLocal.iceCheckingRestartExpected = true;
+          return test.pcLocal.getAllUserMedia([{video: true}]);
+        },
+        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+          test.pcRemote.iceCheckingRestartExpected = true;
+        },
+      ]
+    );
+
+    test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+                              PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{video: true}], [{video: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: answerer adds second audio stream"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiationAnswerer(test.chain,
+      [
+        function PC_LOCAL_ADD_SECOND_STREAM(test) {
+          test.setMediaConstraints([{audio: true}, {audio: true}],
+                                   [{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
+        },
+      ]
+    );
+
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
+
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoNoBundle.html
@@ -10,25 +10,17 @@
     bug: "1016476",
     title: "Basic audio/video peer connection with no Bundle"
   });
 
   runNetworkTest(options => {
     var test = new PeerConnectionTest(options);
     test.chain.insertAfter(
       'PC_LOCAL_CREATE_OFFER',
-      [
-        function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
-          test.originalOffer.sdp = test.originalOffer.sdp.replace(
-              /a=group:BUNDLE .*\r\n/g,
-            ""
-          );
-          info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
-        }
-      ]);
+      [PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER]);
     test.setMediaConstraints([{audio: true}, {video: true}],
                              [{audio: true}, {video: true}]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
+++ b/dom/media/tests/mochitest/test_peerConnection_capturedVideo.html
@@ -26,17 +26,20 @@ runNetworkTest(function() {
   test.setOfferOptions({ offerToReceiveVideo: false,
                          offerToReceiveAudio: false });
   test.chain.insertAfter("PC_LOCAL_GUM", [
     function PC_LOCAL_CAPTUREVIDEO(test) {
       return metadataLoaded
         .then(() => {
           var stream = v1.mozCaptureStreamUntilEnded();
           is(stream.getTracks().length, 2, "Captured stream has 2 tracks");
-          stream.getTracks().forEach(tr => test.pcLocal._pc.addTrack(tr, stream));
+          stream.getTracks().forEach(tr => {
+            test.pcLocal._pc.addTrack(tr, stream);
+            test.pcLocal.expectedLocalTrackTypesById[tr.id] = tr.kind;
+          });
           test.pcLocal.constraints = [{ video: true, audio:true }]; // fool tests
         });
     }
   ]);
   test.chain.removeAfter("PC_REMOTE_CHECK_MEDIA_FLOW_PRESENT");
   test.run();
 });
 </script>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: remove audio track"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+          test.setOfferOptions({ offerToReceiveAudio: true });
+          test.setMediaConstraints([], [{audio: true}]);
+          return test.pcLocal.removeSender(0);
+        },
+      ]
+    );
+
+    // TODO(bug 1093835): figure out how to verify that media stopped flowing from pcLocal
+
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: remove then add audio track"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+          return test.pcLocal.removeSender(0);
+        },
+        function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+          // The new track's pipeline will start with a packet count of
+          // 0, but the remote side will keep its old pipeline and packet
+          // count.
+          test.pcLocal.disableRtpCountChecking = true;
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
+        },
+      ]
+    );
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: remove then add audio track"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+          // The new track's pipeline will start with a packet count of
+          // 0, but the remote side will keep its old pipeline and packet
+          // count.
+          test.pcLocal.disableRtpCountChecking = true;
+          return test.pcLocal.removeSender(0);
+        },
+        function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
+        },
+      ]
+    );
+
+    test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+                              PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{audio: true}], [{audio: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: remove then add video track"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+          // The new track's pipeline will start with a packet count of
+          // 0, but the remote side will keep its old pipeline and packet
+          // count.
+          test.pcLocal.disableRtpCountChecking = true;
+          return test.pcLocal.removeSender(0);
+        },
+        function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+          return test.pcLocal.getAllUserMedia([{video: true}]);
+        },
+      ]
+    );
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{video: true}], [{video: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: remove then add video track, no bundle"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+          // The new track's pipeline will start with a packet count of
+          // 0, but the remote side will keep its old pipeline and packet
+          // count.
+          test.pcLocal.disableRtpCountChecking = true;
+          return test.pcLocal.removeSender(0);
+        },
+        function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+          return test.pcLocal.getAllUserMedia([{video: true}]);
+        },
+      ]
+    );
+
+    test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+                              PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+    // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+    test.setMediaConstraints([{video: true}], [{video: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1017888",
+    title: "Renegotiation: remove video track"
+  });
+
+  var test;
+  runNetworkTest(function (options) {
+    test = new PeerConnectionTest(options);
+    addRenegotiation(test.chain,
+      [
+        function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+          test.setOfferOptions({ offerToReceiveVideo: true });
+          test.setMediaConstraints([], [{video: true}]);
+          return test.pcLocal.removeSender(0);
+        },
+      ]
+    );
+
+    // TODO(bug 1093835): figure out how to verify that media stopped flowing from pcLocal
+
+    test.setMediaConstraints([{video: true}], [{video: true}]);
+    test.run();
+  });
+</script>
+</pre>
+</body>
+</html>
--- a/media/mtransport/test/ice_unittest.cpp
+++ b/media/mtransport/test/ice_unittest.cpp
@@ -879,17 +879,16 @@ class IceTestPeer : public sigslot::has_
 
   void ParseCandidate_s(size_t i, const std::string& candidate) {
     std::vector<std::string> attributes;
 
     attributes.push_back(candidate);
     streams_[i]->ParseAttributes(attributes);
   }
 
-  // Allow us to parse candidates directly on the current thread.
   void ParseCandidate(size_t i, const std::string& candidate)
   {
     test_utils->sts_target()->Dispatch(
         WrapRunnable(this,
                         &IceTestPeer::ParseCandidate_s,
                         i,
                         candidate),
         NS_DISPATCH_SYNC);
--- a/media/webrtc/signaling/test/FakeMediaStreams.h
+++ b/media/webrtc/signaling/test/FakeMediaStreams.h
@@ -218,55 +218,59 @@ class Fake_SourceMediaStream : public Fa
 
 class Fake_DOMMediaStream;
 
 class Fake_MediaStreamTrack
 {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Fake_MediaStreamTrack)
 
-  explicit Fake_MediaStreamTrack(bool aIsVideo) : mIsVideo (aIsVideo)
+  Fake_MediaStreamTrack(bool aIsVideo, Fake_DOMMediaStream* aStream) :
+    mIsVideo (aIsVideo),
+    mStream (aStream)
   {
     static size_t counter = 0;
     std::ostringstream os;
     os << counter++;
     mID = os.str();
   }
+
   mozilla::TrackID GetTrackID() { return mIsVideo ? 1 : 0; }
   std::string GetId() const { return mID; }
   void AssignId(const std::string& id) { mID = id; }
-  Fake_DOMMediaStream *GetStream() { return nullptr; }
+  Fake_DOMMediaStream *GetStream() { return mStream; }
   const Fake_MediaStreamTrack* AsVideoStreamTrack() const
   {
     return mIsVideo? this : nullptr;
   }
   const Fake_MediaStreamTrack* AsAudioStreamTrack() const
   {
     return mIsVideo? nullptr : this;
   }
 private:
   ~Fake_MediaStreamTrack() {}
 
   const bool mIsVideo;
+  Fake_DOMMediaStream* mStream;
   std::string mID;
 };
 
 class Fake_DOMMediaStream : public nsISupports
 {
 protected:
   virtual ~Fake_DOMMediaStream() {
     // Note: memory leak
     mMediaStream->Stop();
   }
 
 public:
   explicit Fake_DOMMediaStream(Fake_MediaStream *stream = nullptr)
-    : mMediaStream(stream? stream : new Fake_MediaStream())
-    , mVideoTrack(new Fake_MediaStreamTrack(true))
-    , mAudioTrack(new Fake_MediaStreamTrack(false)) {}
+    : mMediaStream(stream ? stream : new Fake_MediaStream())
+    , mVideoTrack(new Fake_MediaStreamTrack(true, this))
+    , mAudioTrack(new Fake_MediaStreamTrack(false, this)) {}
 
   NS_DECL_THREADSAFE_ISUPPORTS
 
   static already_AddRefed<Fake_DOMMediaStream>
   CreateSourceStream(nsIDOMWindow* aWindow, uint32_t aHintContents) {
     Fake_SourceMediaStream *source = new Fake_SourceMediaStream();
 
     nsRefPtr<Fake_DOMMediaStream> ds = new Fake_DOMMediaStream(source);
--- a/media/webrtc/signaling/test/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/test/jsep_session_unittest.cpp
@@ -23,16 +23,18 @@
 #include "signaling/src/sdp/SdpMediaSection.h"
 #include "signaling/src/sdp/SipccSdpParser.h"
 #include "signaling/src/jsep/JsepCodecDescription.h"
 #include "signaling/src/jsep/JsepTrack.h"
 #include "signaling/src/jsep/JsepSession.h"
 #include "signaling/src/jsep/JsepSessionImpl.h"
 #include "signaling/src/jsep/JsepTrack.h"
 
+#include "TestHarness.h"
+
 namespace mozilla {
 static const char* kCandidates[] = {
   "0 1 UDP 9999 192.168.0.1 2000 typ host",
   "0 1 UDP 9999 192.168.0.1 2001 typ host",
   "0 1 UDP 9999 192.168.0.2 2002 typ srflx raddr 10.252.34.97 rport 53594",
   // Mix up order
   "0 1 UDP 9999 192.168.1.2 2012 typ srflx raddr 10.252.34.97 rport 53594",
   "0 1 UDP 9999 192.168.1.1 2010 typ host",
@@ -188,16 +190,132 @@ protected:
     for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) {
       ASSERT_TRUE(uuid_gen.Generate(&track_id));
 
       RefPtr<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
       side->AddTrack(mst);
     }
   }
 
+  RefPtr<JsepTrack> GetTrack(JsepSessionImpl& side,
+                             SdpMediaSection::MediaType type,
+                             size_t index) const {
+    auto tracks = side.GetLocalTracks();
+
+    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
+      if ((*i)->GetMediaType() != type) {
+        continue;
+      }
+
+      if (index != 0) {
+        --index;
+        continue;
+      }
+
+      return *i;
+    }
+
+    return RefPtr<JsepTrack>(nullptr);
+  }
+
+  RefPtr<JsepTrack> GetTrackOff(size_t index,
+                                SdpMediaSection::MediaType type) {
+    return GetTrack(mSessionOff, type, index);
+  }
+
+  RefPtr<JsepTrack> GetTrackAns(size_t index,
+                                SdpMediaSection::MediaType type) {
+    return GetTrack(mSessionAns, type, index);
+  }
+
+  class ComparePairsByLevel {
+    public:
+      bool operator()(const JsepTrackPair& lhs,
+                      const JsepTrackPair& rhs) const {
+        return lhs.mLevel < rhs.mLevel;
+      }
+  };
+
+  std::vector<JsepTrackPair> GetTrackPairsByLevel(JsepSessionImpl& side) const {
+    auto pairs = side.GetNegotiatedTrackPairs();
+    std::sort(pairs.begin(), pairs.end(), ComparePairsByLevel());
+    return pairs;
+  }
+
+  bool Equals(const JsepTrackPair& p1,
+              const JsepTrackPair& p2) const {
+    if (p1.mLevel != p2.mLevel) {
+      return false;
+    }
+
+    if (p1.mBundleLevel.isSome() != p2.mBundleLevel.isSome()) {
+      return false;
+    }
+
+    if (p1.mBundleLevel.isSome() &&
+        *p1.mBundleLevel != *p2.mBundleLevel) {
+      return false;
+    }
+
+    if (p1.mSending.get() != p2.mSending.get()) {
+      return false;
+    }
+
+    if (p1.mReceiving.get() != p2.mReceiving.get()) {
+      return false;
+    }
+
+    if (p1.mRtpTransport.get() != p2.mRtpTransport.get()) {
+      return false;
+    }
+
+    if (p1.mRtcpTransport.get() != p2.mRtcpTransport.get()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  size_t GetTrackCount(JsepSessionImpl& side,
+                       SdpMediaSection::MediaType type) const {
+    auto tracks = side.GetLocalTracks();
+    size_t result = 0;
+    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
+      if ((*i)->GetMediaType() == type) {
+        ++result;
+      }
+    }
+    return result;
+  }
+
+  UniquePtr<Sdp> GetParsedLocalDescription(const JsepSessionImpl& side) const {
+    SipccSdpParser parser;
+    return mozilla::Move(parser.Parse(side.GetLocalDescription()));
+  }
+
+  SdpMediaSection* GetMsection(Sdp& sdp,
+                               SdpMediaSection::MediaType type,
+                               size_t index) const {
+    for (size_t i = 0; i < sdp.GetMediaSectionCount(); ++i) {
+      auto& msection = sdp.GetMediaSection(i);
+      if (msection.GetMediaType() != type) {
+        continue;
+      }
+
+      if (index) {
+        --index;
+        continue;
+      }
+
+      return &msection;
+    }
+
+    return nullptr;
+  }
+
   void
   EnsureNegotiationFailure(SdpMediaSection::MediaType type,
                            const std::string& codecName)
   {
     for (auto i = mSessionOff.Codecs().begin(); i != mSessionOff.Codecs().end();
          ++i) {
       auto* codec = *i;
       if (codec->mType == type && codec->mName != codecName) {
@@ -229,16 +347,26 @@ protected:
     return answer;
   }
 
   static const uint32_t NO_CHECKS = 0;
   static const uint32_t CHECK_SUCCESS = 1;
   static const uint32_t CHECK_TRACKS = 1 << 2;
   static const uint32_t ALL_CHECKS = CHECK_SUCCESS | CHECK_TRACKS;
 
+  void OfferAnswer(uint32_t checkFlags = ALL_CHECKS) {
+    std::string offer = CreateOffer();
+    SetLocalOffer(offer, checkFlags);
+    SetRemoteOffer(offer, checkFlags);
+
+    std::string answer = CreateAnswer();
+    SetLocalAnswer(answer, checkFlags);
+    SetRemoteAnswer(answer, checkFlags);
+  }
+
   void
   SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
   {
     nsresult rv = mSessionOff.SetLocalDescription(kJsepSdpOffer, offer);
 
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
@@ -486,16 +614,55 @@ protected:
 
   void
   ValidateAnswererCandidates()
   {
     ValidateCandidates(mSessionAns, false);
   }
 
   void
+  DisableMsid(std::string* sdp) const {
+    size_t pos = sdp->find("a=msid-semantic");
+    ASSERT_NE(std::string::npos, pos);
+    (*sdp)[pos + 2] = 'X'; // garble, a=Xsid-semantic
+  }
+
+  void
+  DisableBundle(std::string* sdp) const {
+    size_t pos = sdp->find("a=group:BUNDLE");
+    ASSERT_NE(std::string::npos, pos);
+    (*sdp)[pos + 11] = 'G'; // garble, a=group:BUNGLE
+  }
+
+  void
+  DisableMsection(std::string* sdp, size_t level) const {
+    SipccSdpParser parser;
+    UniquePtr<Sdp> parsed = parser.Parse(*sdp);
+    ASSERT_TRUE(parsed.get());
+    ASSERT_LT(level, parsed->GetMediaSectionCount());
+    parsed->GetMediaSection(level).SetPort(0);
+
+    auto& attrs = parsed->GetMediaSection(level).GetAttributeList();
+
+    ASSERT_TRUE(attrs.HasAttribute(SdpAttribute::kMidAttribute));
+    std::string mid = attrs.GetMid();
+
+    attrs.Clear();
+
+    ASSERT_TRUE(
+        parsed->GetAttributeList().HasAttribute(SdpAttribute::kGroupAttribute));
+
+    SdpGroupAttributeList* newGroupAttr(new SdpGroupAttributeList(
+          parsed->GetAttributeList().GetGroup()));
+    newGroupAttr->RemoveMid(mid);
+    parsed->GetAttributeList().SetAttribute(newGroupAttr);
+    (*sdp) = parsed->ToString();
+  }
+
+  void
   DumpTrack(const JsepTrack& track)
   {
     std::cerr << "  type=" << track.GetMediaType() << std::endl;
     std::cerr << "  protocol=" << track.GetNegotiatedDetails()->GetProtocol()
               << std::endl;
     std::cerr << "  codecs=" << std::endl;
     size_t num_codecs = track.GetNegotiatedDetails()->GetCodecCount();
     for (size_t i = 0; i < num_codecs; ++i) {
@@ -626,46 +793,988 @@ TEST_P(JsepSessionTest, FullCall)
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   AddTracks(&mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 }
 
+TEST_P(JsepSessionTest, RenegotiationNoChange)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(types.size(), added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer);
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(types.size(), added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  std::string reoffer = CreateOffer();
+  SetLocalOffer(reoffer);
+  SetRemoteOffer(reoffer);
+
+  added = mSessionAns.GetRemoteTracksAdded();
+  removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  std::string reanswer = CreateAnswer();
+  SetLocalAnswer(reanswer);
+  SetRemoteAnswer(reanswer);
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  std::vector<SdpMediaSection::MediaType> extraTypes;
+  extraTypes.push_back(SdpMediaSection::kAudio);
+  extraTypes.push_back(SdpMediaSection::kVideo);
+  AddTracks(&mSessionOff, extraTypes);
+  types.insert(types.end(), extraTypes.begin(), extraTypes.end());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(2U, added.size());
+  ASSERT_EQ(0U, removed.size());
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  std::vector<SdpMediaSection::MediaType> extraTypes;
+  extraTypes.push_back(SdpMediaSection::kAudio);
+  extraTypes.push_back(SdpMediaSection::kVideo);
+  AddTracks(&mSessionAns, extraTypes);
+  types.insert(types.end(), extraTypes.begin(), extraTypes.end());
+
+  // We need to add a recvonly m-section to the offer for this to work
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio =
+    Some(GetTrackCount(mSessionOff, SdpMediaSection::kAudio) + 1);
+  options.mOfferToReceiveVideo =
+    Some(GetTrackCount(mSessionOff, SdpMediaSection::kVideo) + 1);
+
+  std::string offer = CreateOffer(Some(options));
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer, CHECK_SUCCESS);
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(2U, added.size());
+  ASSERT_EQ(0U, removed.size());
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationBothAddTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  std::vector<SdpMediaSection::MediaType> extraTypes;
+  extraTypes.push_back(SdpMediaSection::kAudio);
+  extraTypes.push_back(SdpMediaSection::kVideo);
+  AddTracks(&mSessionAns, extraTypes);
+  AddTracks(&mSessionOff, extraTypes);
+  types.insert(types.end(), extraTypes.begin(), extraTypes.end());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(2U, added.size());
+  ASSERT_EQ(0U, removed.size());
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(2U, added.size());
+  ASSERT_EQ(0U, removed.size());
+  ASSERT_EQ(SdpMediaSection::kAudio, added[0]->GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1]->GetMediaType());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size() + 2, newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationOffererRemovesTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
+  ASSERT_TRUE(removedTrack);
+  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(),
+                                           removedTrack->GetTrackId()));
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  // First m-section should be recvonly
+  auto offer = GetParsedLocalDescription(mSessionOff);
+  auto* msection = GetMsection(*offer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  // First audio m-section should be sendonly
+  auto answer = GetParsedLocalDescription(mSessionAns);
+  msection = GetMsection(*answer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_FALSE(msection->IsReceiving());
+  ASSERT_TRUE(msection->IsSending());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  // Will be the same size since we still have a track on one side.
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+
+  // This should be the only difference.
+  ASSERT_TRUE(offererPairs[0].mSending);
+  ASSERT_FALSE(newOffererPairs[0].mSending);
+
+  // Remove this difference, let loop below take care of the rest
+  offererPairs[0].mSending = nullptr;
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  // Will be the same size since we still have a track on one side.
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
+
+  // This should be the only difference.
+  ASSERT_TRUE(answererPairs[0].mReceiving);
+  ASSERT_FALSE(newAnswererPairs[0].mReceiving);
+
+  // Remove this difference, let loop below take care of the rest
+  answererPairs[0].mReceiving = nullptr;
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererRemovesTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  RefPtr<JsepTrack> removedTrack = GetTrackAns(0, types.front());
+  ASSERT_TRUE(removedTrack);
+  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrack->GetStreamId(),
+                                           removedTrack->GetTrackId()));
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
+
+  // First m-section should be sendrecv
+  auto offer = GetParsedLocalDescription(mSessionOff);
+  auto* msection = GetMsection(*offer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_TRUE(msection->IsSending());
+
+  // First audio m-section should be recvonly
+  auto answer = GetParsedLocalDescription(mSessionAns);
+  msection = GetMsection(*answer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  // Will be the same size since we still have a track on one side.
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+
+  // This should be the only difference.
+  ASSERT_TRUE(offererPairs[0].mReceiving);
+  ASSERT_FALSE(newOffererPairs[0].mReceiving);
+
+  // Remove this difference, let loop below take care of the rest
+  offererPairs[0].mReceiving = nullptr;
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  // Will be the same size since we still have a track on one side.
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
+
+  // This should be the only difference.
+  ASSERT_TRUE(answererPairs[0].mSending);
+  ASSERT_FALSE(newAnswererPairs[0].mSending);
+
+  // Remove this difference, let loop below take care of the rest
+  answererPairs[0].mSending = nullptr;
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationBothRemoveTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
+  ASSERT_TRUE(removedTrackAnswer);
+  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
+                                           removedTrackAnswer->GetTrackId()));
+
+  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, types.front());
+  ASSERT_TRUE(removedTrackOffer);
+  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(),
+                                           removedTrackOffer->GetTrackId()));
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());
+
+  // First m-section should be recvonly
+  auto offer = GetParsedLocalDescription(mSessionOff);
+  auto* msection = GetMsection(*offer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  // First m-section should be inactive
+  auto answer = GetParsedLocalDescription(mSessionAns);
+  msection = GetMsection(*answer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_FALSE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
+
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i + 1], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
+
+  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i + 1], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationBothRemoveTrackDifferentMsection)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  if (types.size() < 2 || types[0] != types[1]) {
+    // For simplicity, just run in cases where we have two of the same type
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
+  ASSERT_TRUE(removedTrackAnswer);
+  ASSERT_EQ(NS_OK, mSessionAns.RemoveTrack(removedTrackAnswer->GetStreamId(),
+                                           removedTrackAnswer->GetTrackId()));
+
+  // Second instance of the same type
+  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(1, types.front());
+  ASSERT_TRUE(removedTrackOffer);
+  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrackOffer->GetStreamId(),
+                                           removedTrackOffer->GetTrackId()));
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());
+
+  // Second m-section should be recvonly
+  auto offer = GetParsedLocalDescription(mSessionOff);
+  auto* msection = GetMsection(*offer, types.front(), 1);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  // First m-section should be recvonly
+  auto answer = GetParsedLocalDescription(mSessionAns);
+  msection = GetMsection(*answer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+
+  // This should be the only difference.
+  ASSERT_TRUE(offererPairs[0].mReceiving);
+  ASSERT_FALSE(newOffererPairs[0].mReceiving);
+
+  // Remove this difference, let loop below take care of the rest
+  offererPairs[0].mReceiving = nullptr;
+
+  // This should be the only difference.
+  ASSERT_TRUE(offererPairs[1].mSending);
+  ASSERT_FALSE(newOffererPairs[1].mSending);
+
+  // Remove this difference, let loop below take care of the rest
+  offererPairs[1].mSending = nullptr;
+
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
+
+  // This should be the only difference.
+  ASSERT_TRUE(answererPairs[0].mSending);
+  ASSERT_FALSE(newAnswererPairs[0].mSending);
+
+  // Remove this difference, let loop below take care of the rest
+  answererPairs[0].mSending = nullptr;
+
+  // This should be the only difference.
+  ASSERT_TRUE(answererPairs[1].mReceiving);
+  ASSERT_FALSE(newAnswererPairs[1].mReceiving);
+
+  // Remove this difference, let loop below take care of the rest
+  answererPairs[1].mReceiving = nullptr;
+
+  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationOffererReplacesTrack)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
+  ASSERT_TRUE(removedTrack);
+  ASSERT_EQ(NS_OK, mSessionOff.RemoveTrack(removedTrack->GetStreamId(),
+                                           removedTrack->GetTrackId()));
+  RefPtr<JsepTrack> addedTrack(
+      new JsepTrack(types.front(), "newstream", "newtrack"));
+  ASSERT_EQ(NS_OK, mSessionOff.AddTrack(addedTrack));
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto added = mSessionAns.GetRemoteTracksAdded();
+  auto removed = mSessionAns.GetRemoteTracksRemoved();
+  ASSERT_EQ(1U, added.size());
+  ASSERT_EQ(1U, removed.size());
+
+  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
+  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
+  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
+
+  ASSERT_EQ(addedTrack->GetMediaType(), added[0]->GetMediaType());
+  ASSERT_EQ(addedTrack->GetStreamId(), added[0]->GetStreamId());
+  ASSERT_EQ(addedTrack->GetTrackId(), added[0]->GetTrackId());
+
+  added = mSessionOff.GetRemoteTracksAdded();
+  removed = mSessionOff.GetRemoteTracksRemoved();
+  ASSERT_EQ(0U, added.size());
+  ASSERT_EQ(0U, removed.size());
+
+  // First audio m-section should be sendrecv
+  auto offer = GetParsedLocalDescription(mSessionOff);
+  auto* msection = GetMsection(*offer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_TRUE(msection->IsSending());
+
+  // First audio m-section should be sendrecv
+  auto answer = GetParsedLocalDescription(mSessionAns);
+  msection = GetMsection(*answer, types.front(), 0);
+  ASSERT_TRUE(msection);
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_TRUE(msection->IsSending());
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+
+  ASSERT_NE(offererPairs[0].mSending->GetStreamId(),
+            newOffererPairs[0].mSending->GetStreamId());
+  ASSERT_NE(offererPairs[0].mSending->GetTrackId(),
+            newOffererPairs[0].mSending->GetTrackId());
+
+  // Skip first pair
+  for (size_t i = 1; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
+
+  ASSERT_NE(answererPairs[0].mReceiving->GetStreamId(),
+            newAnswererPairs[0].mReceiving->GetStreamId());
+  ASSERT_NE(answererPairs[0].mReceiving->GetTrackId(),
+            newAnswererPairs[0].mReceiving->GetTrackId());
+
+  // Skip first pair
+  for (size_t i = 1; i < newAnswererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  }
+}
+
+// Tests whether auto-assigned remote msids (ie; what happens when the other
+// side doesn't use msid attributes) are stable across renegotiation.
+TEST_P(JsepSessionTest, RenegotiationAutoAssignedMsidIsStable)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  DisableMsid(&answer);
+
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+
+  // Make sure that DisableMsid actually worked, since it is kinda hacky
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+  ASSERT_EQ(offererPairs.size(), answererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(offererPairs[i].mReceiving);
+    ASSERT_TRUE(answererPairs[i].mSending);
+    // These should not match since we've monkeyed with the msid
+    ASSERT_NE(offererPairs[i].mReceiving->GetStreamId(),
+              answererPairs[i].mSending->GetStreamId());
+    ASSERT_NE(offererPairs[i].mReceiving->GetTrackId(),
+              answererPairs[i].mSending->GetTrackId());
+  }
+
+  offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  DisableMsid(&answer);
+
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs();
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  }
+}
+
+// Tests behavior when the answerer does not use msid in the initial exchange,
+// but does on renegotiation.
+TEST_P(JsepSessionTest, RenegotiationAnswererEnablesMsid)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  DisableMsid(&answer);
+
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+
+  offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs();
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
+              newOffererPairs[i].mReceiving->GetMediaType());
+
+    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
+    ASSERT_EQ(offererPairs[i].mRtpTransport, newOffererPairs[i].mRtpTransport);
+    ASSERT_EQ(offererPairs[i].mRtcpTransport, newOffererPairs[i].mRtcpTransport);
+
+    if (offererPairs[i].mReceiving->GetMediaType() ==
+        SdpMediaSection::kApplication) {
+      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+    } else {
+      // This should be the only difference
+      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+    }
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererDisablesMsid)
+{
+  AddTracks(&mSessionOff);
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+
+  offer = CreateOffer();
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  AddTracks(&mSessionAns);
+  answer = CreateAnswer();
+  SetLocalAnswer(answer);
+
+  DisableMsid(&answer);
+
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  auto newOffererPairs = mSessionOff.GetNegotiatedTrackPairs();
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
+              newOffererPairs[i].mReceiving->GetMediaType());
+
+    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
+    ASSERT_EQ(offererPairs[i].mRtpTransport, newOffererPairs[i].mRtpTransport);
+    ASSERT_EQ(offererPairs[i].mRtcpTransport, newOffererPairs[i].mRtcpTransport);
+
+    if (offererPairs[i].mReceiving->GetMediaType() ==
+        SdpMediaSection::kApplication) {
+      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+    } else {
+      // This should be the only difference
+      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+    }
+  }
+}
+
+// Tests behavior when offerer does not use bundle on the initial offer/answer,
+// but does on renegotiation.
+TEST_P(JsepSessionTest, RenegotiationOffererEnablesBundle)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+  std::string offer = CreateOffer();
+
+  DisableBundle(&offer);
+
+  SetLocalOffer(offer);
+  SetRemoteOffer(offer);
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer);
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  OfferAnswer();
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
+
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    // No bundle initially
+    ASSERT_FALSE(offererPairs[i].mBundleLevel.isSome());
+    ASSERT_FALSE(answererPairs[i].mBundleLevel.isSome());
+    if (i != 0) {
+      ASSERT_NE(offererPairs[0].mRtpTransport.get(),
+                offererPairs[i].mRtpTransport.get());
+      if (offererPairs[0].mRtcpTransport) {
+        ASSERT_NE(offererPairs[0].mRtcpTransport.get(),
+                  offererPairs[i].mRtcpTransport.get());
+      }
+      ASSERT_NE(answererPairs[0].mRtpTransport.get(),
+                answererPairs[i].mRtpTransport.get());
+      if (answererPairs[0].mRtcpTransport) {
+        ASSERT_NE(answererPairs[0].mRtcpTransport.get(),
+                  answererPairs[i].mRtcpTransport.get());
+      }
+    }
+
+    // Verify that bundle worked after renegotiation
+    ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome());
+    ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome());
+    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
+              newOffererPairs[i].mRtpTransport.get());
+    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
+              newOffererPairs[i].mRtcpTransport.get());
+    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
+              newAnswererPairs[i].mRtpTransport.get());
+    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
+              newAnswererPairs[i].mRtcpTransport.get());
+  }
+}
+
+TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  if (types.size() < 2) {
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  std::string reoffer = CreateOffer();
+
+  DisableMsection(&reoffer, 0);
+
+  SetLocalOffer(reoffer, CHECK_SUCCESS);
+  SetRemoteOffer(reoffer, CHECK_SUCCESS);
+  std::string reanswer = CreateAnswer();
+  SetLocalAnswer(reanswer, CHECK_SUCCESS);
+  SetRemoteAnswer(reanswer, CHECK_SUCCESS);
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
+
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome());
+    ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome());
+    ASSERT_EQ(1U, *newOffererPairs[i].mBundleLevel);
+    ASSERT_EQ(1U, *newAnswererPairs[i].mBundleLevel);
+    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
+              newOffererPairs[i].mRtpTransport.get());
+    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
+              newOffererPairs[i].mRtcpTransport.get());
+    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
+              newAnswererPairs[i].mRtpTransport.get());
+    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
+              newAnswererPairs[i].mRtcpTransport.get());
+  }
+
+  ASSERT_NE(newOffererPairs[0].mRtpTransport.get(),
+            offererPairs[0].mRtpTransport.get());
+  ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(),
+            answererPairs[0].mRtpTransport.get());
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport)
+{
+  AddTracks(&mSessionOff);
+  AddTracks(&mSessionAns);
+
+  if (types.size() < 2) {
+    return;
+  }
+
+  OfferAnswer();
+
+  auto offererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  std::string reoffer = CreateOffer();
+  SetLocalOffer(reoffer, CHECK_SUCCESS);
+  SetRemoteOffer(reoffer, CHECK_SUCCESS);
+  std::string reanswer = CreateAnswer();
+
+  DisableMsection(&reanswer, 0);
+
+  SetLocalAnswer(reanswer, CHECK_SUCCESS);
+  SetRemoteAnswer(reanswer, CHECK_SUCCESS);
+
+  auto newOffererPairs = GetTrackPairsByLevel(mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(mSessionAns);
+
+  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
+
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    ASSERT_TRUE(newOffererPairs[i].mBundleLevel.isSome());
+    ASSERT_TRUE(newAnswererPairs[i].mBundleLevel.isSome());
+    ASSERT_EQ(1U, *newOffererPairs[i].mBundleLevel);
+    ASSERT_EQ(1U, *newAnswererPairs[i].mBundleLevel);
+    ASSERT_EQ(newOffererPairs[0].mRtpTransport.get(),
+              newOffererPairs[i].mRtpTransport.get());
+    ASSERT_EQ(newOffererPairs[0].mRtcpTransport.get(),
+              newOffererPairs[i].mRtcpTransport.get());
+    ASSERT_EQ(newAnswererPairs[0].mRtpTransport.get(),
+              newAnswererPairs[i].mRtpTransport.get());
+    ASSERT_EQ(newAnswererPairs[0].mRtcpTransport.get(),
+              newAnswererPairs[i].mRtcpTransport.get());
+  }
+
+  ASSERT_NE(newOffererPairs[0].mRtpTransport.get(),
+            offererPairs[0].mRtpTransport.get());
+  ASSERT_NE(newAnswererPairs[0].mRtpTransport.get(),
+            answererPairs[0].mRtpTransport.get());
+}
+
 TEST_P(JsepSessionTest, FullCallWithCandidates)
 {
   AddTracks(&mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   GatherOffererCandidates();
   ValidateOffererCandidates();
   SetRemoteOffer(offer);
   TrickleOffererCandidates();
   ValidateAnswererCandidates();
   AddTracks(&mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 }
 
-INSTANTIATE_TEST_CASE_P(Variants, JsepSessionTest,
-                        ::testing::Values("audio",
-                                          "video",
-                                          "datachannel",
-                                          "audio,video",
-                                          "video,audio",
-                                          "audio,datachannel",
-                                          "video,datachannel",
-                                          "video,audio,datachannel",
-                                          "audio,video,datachannel",
-                                          "datachannel,audio",
-                                          "datachannel,video",
-                                          "datachannel,audio,video",
-                                          "datachannel,video,audio"));
+INSTANTIATE_TEST_CASE_P(
+    Variants,
+    JsepSessionTest,
+    ::testing::Values("audio",
+                      "video",
+                      "datachannel",
+                      "audio,video",
+                      "video,audio",
+                      "audio,datachannel",
+                      "video,datachannel",
+                      "video,audio,datachannel",
+                      "audio,video,datachannel",
+                      "datachannel,audio",
+                      "datachannel,video",
+                      "datachannel,audio,video",
+                      "datachannel,video,audio",
+                      "audio,datachannel,video",
+                      "video,datachannel,audio",
+                      "audio,audio",
+                      "video,video",
+                      "audio,audio,video",
+                      "audio,video,video",
+                      "audio,audio,video,video",
+                      "audio,audio,video,video,datachannel"));
 
 // offerToReceiveXxx variants
 
 TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines)
 {
   JsepOfferOptions options;
   options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
   options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
@@ -1108,22 +2217,25 @@ TEST_P(JsepSessionTest, TestRejectMline)
   ASSERT_TRUE(failed_section) << "Failed type was entirely absent from SDP";
   auto& failed_attrs = failed_section->GetAttributeList();
   ASSERT_EQ(SdpDirectionAttribute::kInactive, failed_attrs.GetDirection());
   ASSERT_EQ(0U, failed_section->GetPort());
 
   mSessionAns.SetLocalDescription(kJsepSdpAnswer, answer);
   mSessionOff.SetRemoteDescription(kJsepSdpAnswer, answer);
 
-  ASSERT_EQ(types.size() - 1, mSessionOff.GetNegotiatedTrackPairs().size());
-  ASSERT_EQ(types.size() - 1, mSessionAns.GetNegotiatedTrackPairs().size());
+  size_t numRejected = std::count(types.begin(), types.end(), types.front());
+  size_t numAccepted = types.size() - numRejected;
+
+  ASSERT_EQ(numAccepted, mSessionOff.GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(numAccepted, mSessionAns.GetNegotiatedTrackPairs().size());
 
   ASSERT_EQ(types.size(), mSessionOff.GetTransports().size());
   ASSERT_EQ(types.size(), mSessionOff.GetLocalTracks().size());
-  ASSERT_EQ(types.size() - 1, mSessionOff.GetRemoteTracks().size());
+  ASSERT_EQ(numAccepted, mSessionOff.GetRemoteTracks().size());
 
   ASSERT_EQ(types.size(), mSessionAns.GetTransports().size());
   ASSERT_EQ(types.size(), mSessionAns.GetLocalTracks().size());
   ASSERT_EQ(types.size(), mSessionAns.GetRemoteTracks().size());
 }
 
 TEST_F(JsepSessionTest, CreateOfferNoMlines)
 {
@@ -1308,14 +2420,17 @@ TEST_F(JsepSessionTest, TestUniquePayloa
       GetUniquePayloadTypes().size());
 }
 
 } // namespace mozilla
 
 int
 main(int argc, char** argv)
 {
+  // Prevents some log spew
+  ScopedXPCOM xpcom("jsep_session_unittest");
+
   NSS_NoDB_Init(nullptr);
   NSS_SetDomesticPolicy();
 
   ::testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
 }
--- a/media/webrtc/signaling/test/signaling_unittests.cpp
+++ b/media/webrtc/signaling/test/signaling_unittests.cpp
@@ -224,21 +224,20 @@ enum offerAnswerFlags
   ANSWER_VIDEO = (1<<9),
 
   OFFER_AV = OFFER_AUDIO | OFFER_VIDEO,
   ANSWER_AV = ANSWER_AUDIO | ANSWER_VIDEO
 };
 
 enum mediaPipelineFlags
 {
-  PIPELINE_LOCAL = (1<<0),
-  PIPELINE_RTCP_MUX = (1<<1),
-  PIPELINE_SEND = (1<<2),
-  PIPELINE_VIDEO = (1<<3),
-  PIPELINE_RTCP_NACK = (1<<4)
+  PIPELINE_RTCP_MUX = (1<<0),
+  PIPELINE_SEND = (1<<1),
+  PIPELINE_VIDEO = (1<<2),
+  PIPELINE_RTCP_NACK = (1<<3)
 };
 
 
  typedef enum {
    NO_TRICKLE = 0,
    OFFERER_TRICKLES = 1,
    ANSWERER_TRICKLES = 2,
    BOTH_TRICKLE = OFFERER_TRICKLES | ANSWERER_TRICKLES
@@ -643,17 +642,21 @@ class ParsedSDP {
   }
 
   //Convert Internal SDP representation into String representation
   std::string getSdp() const
   {
     std::string sdp;
 
     for (auto it = sdp_lines_.begin(); it != sdp_lines_.end(); ++it) {
-      sdp += it->first + ' ' + it->second;
+      sdp += it->first;
+      if (it->second != "\r\n") {
+        sdp += " ";
+      }
+      sdp += it->second;
     }
 
     return sdp;
   }
 
   void IncorporateCandidate(uint16_t level, const std::string &candidate)
   {
     std::string candidate_attribute("a=" + candidate + "\r\n");
@@ -1077,28 +1080,44 @@ class SignalingAgent {
     nsRefPtr<DOMMediaStream> domMediaStream = new DOMMediaStream(stream);
     domMediaStream->SetHintContents(hint);
 
     nsTArray<nsRefPtr<MediaStreamTrack>> tracks;
     domMediaStream->GetTracks(tracks);
     for (uint32_t i = 0; i < tracks.Length(); i++) {
       ASSERT_EQ(pc->AddTrack(tracks[i], domMediaStream), NS_OK);
     }
-    domMediaStream_ = domMediaStream;
+    domMediaStreams_.push_back(domMediaStream);
   }
 
-  // Removes a stream from the PeerConnection. If the stream
-  // parameter is absent, removes the stream that was most
-  // recently added to the PeerConnection.
-  void RemoveLastStreamAdded() {
+  void RemoveTrack(size_t streamIndex, bool videoTrack = false)
+  {
+    ASSERT_LT(streamIndex, domMediaStreams_.size());
     nsTArray<nsRefPtr<MediaStreamTrack>> tracks;
-    domMediaStream_->GetTracks(tracks);
+    domMediaStreams_[streamIndex]->GetTracks(tracks);
+    for (size_t i = 0; i < tracks.Length(); ++i) {
+      if (!!tracks[i]->AsVideoStreamTrack() == videoTrack) {
+        ASSERT_EQ(pc->RemoveTrack(tracks[i]), NS_OK);
+      }
+    }
+  }
+
+  void RemoveStream(size_t index) {
+    nsTArray<nsRefPtr<MediaStreamTrack>> tracks;
+    domMediaStreams_[index]->GetTracks(tracks);
     for (uint32_t i = 0; i < tracks.Length(); i++) {
       ASSERT_EQ(pc->RemoveTrack(tracks[i]), NS_OK);
     }
+    domMediaStreams_.erase(domMediaStreams_.begin() + index);
+  }
+
+  // Removes the stream that was most recently added to the PeerConnection.
+  void RemoveLastStreamAdded() {
+    ASSERT_FALSE(domMediaStreams_.empty());
+    RemoveStream(domMediaStreams_.size() - 1);
   }
 
   void CreateOffer(OfferOptions& options,
                    uint32_t offerFlags, uint32_t sdpCheck,
                    PCImplSignalingState endState =
                      PCImplSignalingState::SignalingStable) {
 
     // Create a media stream as if it came from GUM
@@ -1145,25 +1164,35 @@ class SignalingAgent {
   }
 
   void CreateAnswer(uint32_t offerAnswerFlags,
                     uint32_t sdpCheck = DONT_CHECK_AUDIO|
                                         DONT_CHECK_VIDEO|
                                         DONT_CHECK_DATA,
                     PCImplSignalingState endState =
                     PCImplSignalingState::SignalingHaveRemoteOffer) {
+    // Create a media stream as if it came from GUM
+    Fake_AudioStreamSource *audio_stream =
+      new Fake_AudioStreamSource();
+
+    nsresult ret;
+    mozilla::SyncRunnable::DispatchToThread(
+      test_utils->sts_target(),
+      WrapRunnableRet(audio_stream, &Fake_MediaStream::Start, &ret));
+
+    ASSERT_TRUE(NS_SUCCEEDED(ret));
 
     uint32_t aHintContents = 0;
     if (offerAnswerFlags & ANSWER_AUDIO) {
       aHintContents |= DOMMediaStream::HINT_CONTENTS_AUDIO;
     }
     if (offerAnswerFlags & ANSWER_VIDEO) {
       aHintContents |= DOMMediaStream::HINT_CONTENTS_VIDEO;
     }
-    AddStream(aHintContents);
+    AddStream(aHintContents, audio_stream);
 
     // Decide if streams are disabled for offer or answer
     // then perform SDP checking based on which stream disabled
     pObserver->state = TestObserver::stateNoResponse;
     ASSERT_EQ(pc->CreateAnswer(), NS_OK);
     ASSERT_EQ(pObserver->state, TestObserver::stateSuccess);
     SDPSanityCheck(pObserver->lastString, sdpCheck, false);
     ASSERT_EQ(signaling_state(), endState);
@@ -1179,31 +1208,21 @@ class SignalingAgent {
   void UpdateAnswer(uint32_t sdpCheck) {
     answer_ = getLocalDescription();
     SDPSanityCheck(answer_, sdpCheck, false);
     if (!mBundleEnabled) {
       answer_ = RemoveBundle(answer_);
     }
   }
 
-  // At present, we use the hints field in a stream to find and
-  // remove it. This only works if the specified hints flags are
-  // unique among all streams in the PeerConnection. This is not
-  // generally true, and will need significant revision once
-  // multiple streams are supported.
-  void CreateOfferRemoveStream(OfferOptions& options,
-                               uint32_t hints, uint32_t sdpCheck) {
-
-    domMediaStream_->SetHintContents(hints);
-
-    // This currently "removes" a stream that has the same audio/video
-    // hints as were passed in.
-    // When complete RemoveStream will remove and entire stream and its tracks
-    // not just disable a track as this is currently doing
-    RemoveLastStreamAdded();
+  void CreateOfferRemoveTrack(OfferOptions& options,
+                              bool videoTrack,
+                              uint32_t sdpCheck) {
+
+    RemoveTrack(0, videoTrack);
 
     // Now call CreateOffer as JS would
     pObserver->state = TestObserver::stateNoResponse;
     ASSERT_EQ(pc->CreateOffer(options), NS_OK);
     ASSERT_TRUE(pObserver->state == TestObserver::stateSuccess);
     SDPSanityCheck(pObserver->lastString, sdpCheck, true);
     offer_ = pObserver->lastString;
     if (!mBundleEnabled) {
@@ -1289,36 +1308,43 @@ class SignalingAgent {
                 expectSuccess ? TestObserver::stateSuccess :
                                 TestObserver::stateError
                );
 
     // Verify that adding ICE candidates does not change the signaling state
     ASSERT_EQ(signaling_state(), endState);
   }
 
-  int GetPacketsReceived(int stream) {
+  int GetPacketsReceived(size_t stream) {
     std::vector<DOMMediaStream *> streams = pObserver->GetStreams();
 
-    if ((int) streams.size() <= stream) {
+    if (streams.size() <= stream) {
+      EXPECT_TRUE(false);
       return 0;
     }
 
     return streams[stream]->GetStream()->AsSourceStream()->GetSegmentsAdded();
   }
 
-  int GetPacketsSent(int stream) {
+  int GetPacketsSent(size_t stream) {
+    if (stream >= domMediaStreams_.size()) {
+      EXPECT_TRUE(false);
+      return 0;
+    }
+
     return static_cast<Fake_MediaStreamBase *>(
-        domMediaStream_->GetStream())->GetSegmentsAdded();
+        domMediaStreams_[stream]->GetStream())->GetSegmentsAdded();
   }
 
   //Stops generating new audio data for transmission.
   //Should be called before Cleanup of the peer connection.
   void CloseSendStreams() {
-    static_cast<Fake_MediaStream*>(
-        domMediaStream_->GetStream())->StopStream();
+    for (auto i = domMediaStreams_.begin(); i != domMediaStreams_.end(); ++i) {
+      static_cast<Fake_MediaStream*>((*i)->GetStream())->StopStream();
+    }
   }
 
   //Stops pulling audio data off the receivers.
   //Should be called before Cleanup of the peer connection.
   void CloseReceiveStreams() {
     std::vector<DOMMediaStream *> streams =
                             pObserver->GetStreams();
     for (size_t i = 0; i < streams.size(); i++) {
@@ -1362,27 +1388,26 @@ class SignalingAgent {
     return nullptr;
   }
 
   void CheckMediaPipeline(int stream, bool video, uint32_t flags,
     VideoSessionConduit::FrameRequestType frameRequestMethod =
       VideoSessionConduit::FrameRequestNone) {
 
     std::cout << name << ": Checking media pipeline settings for "
-              << ((flags & PIPELINE_LOCAL) ? "local " : "remote ")
               << ((flags & PIPELINE_SEND) ? "sending " : "receiving ")
               << ((flags & PIPELINE_VIDEO) ? "video" : "audio")
               << " pipeline (stream " << stream
               << ", track " << video << "); expect "
               << ((flags & PIPELINE_RTCP_MUX) ? "MUX, " : "no MUX, ")
               << ((flags & PIPELINE_RTCP_NACK) ? "NACK." : "no NACK.")
               << std::endl;
 
     mozilla::RefPtr<mozilla::MediaPipeline> pipeline =
-      GetMediaPipeline((flags & PIPELINE_LOCAL), stream, video);
+      GetMediaPipeline((flags & PIPELINE_SEND), stream, video);
     ASSERT_TRUE(pipeline);
     ASSERT_EQ(pipeline->IsDoingRtcpMux(), !!(flags & PIPELINE_RTCP_MUX));
     // We cannot yet test send/recv with video.
     if (!(flags & PIPELINE_VIDEO)) {
       if (flags & PIPELINE_SEND) {
         ASSERT_TRUE_WAIT(pipeline->rtp_packets_sent() >= 40 &&
                          pipeline->rtcp_packets_received() >= 1,
                          kDefaultTimeout);
@@ -1415,17 +1440,17 @@ class SignalingAgent {
     pObserver->peerAgent = peer;
   }
 
 public:
   nsRefPtr<PCDispatchWrapper> pc;
   nsRefPtr<TestObserver> pObserver;
   std::string offer_;
   std::string answer_;
-  nsRefPtr<DOMMediaStream> domMediaStream_;
+  std::vector<nsRefPtr<DOMMediaStream>> domMediaStreams_;
   IceConfiguration cfg_;
   const std::string name;
   bool mBundleEnabled;
 
   typedef struct {
     std::string candidate;
     std::string mid;
     uint16_t level;
@@ -1840,24 +1865,24 @@ public:
                                 uint32_t offerSdpCheck,
                                 uint32_t answerSdpCheck) {
     EnsureInit();
     Offer(options, offerAnswerFlags, offerSdpCheck);
     Answer(options, offerAnswerFlags, answerSdpCheck);
     WaitForCompleted();
   }
 
-  void CreateOfferRemoveStream(OfferOptions& options,
-                               uint32_t hints, uint32_t sdpCheck) {
+  void CreateOfferRemoveTrack(OfferOptions& options,
+                              bool videoTrack, uint32_t sdpCheck) {
     EnsureInit();
     OfferOptions aoptions;
     aoptions.setInt32Option("OfferToReceiveAudio", 1);
     aoptions.setInt32Option("OfferToReceiveVideo", 1);
     a1_->CreateOffer(aoptions, OFFER_AV, SHOULD_SENDRECV_AV );
-    a1_->CreateOfferRemoveStream(options, hints, sdpCheck);
+    a1_->CreateOfferRemoveTrack(options, videoTrack, sdpCheck);
   }
 
   void CreateOfferAudioOnly(OfferOptions& options,
                             uint32_t sdpCheck) {
     EnsureInit();
     a1_->CreateOffer(options, OFFER_AUDIO, sdpCheck);
   }
 
@@ -1870,16 +1895,47 @@ public:
   }
 
   void AddIceCandidateEarly(const std::string& candidate, const std::string& mid,
                             unsigned short level) {
     EnsureInit();
     a1_->AddIceCandidate(candidate, mid, level, false);
   }
 
+  std::string SwapMsids(const std::string& sdp, bool swapVideo) const
+  {
+    SipccSdpParser parser;
+    UniquePtr<Sdp> parsed = parser.Parse(sdp);
+
+    SdpMediaSection* previousMsection = nullptr;
+    bool swapped = false;
+    for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+      SdpMediaSection* currentMsection = &parsed->GetMediaSection(i);
+      bool isVideo = currentMsection->GetMediaType() == SdpMediaSection::kVideo;
+      if (swapVideo == isVideo) {
+        if (previousMsection) {
+          UniquePtr<SdpMsidAttributeList> prevMsid(
+            new SdpMsidAttributeList(
+                previousMsection->GetAttributeList().GetMsid()));
+          UniquePtr<SdpMsidAttributeList> currMsid(
+            new SdpMsidAttributeList(
+                currentMsection->GetAttributeList().GetMsid()));
+          previousMsection->GetAttributeList().SetAttribute(currMsid.release());
+          currentMsection->GetAttributeList().SetAttribute(prevMsid.release());
+          swapped = true;
+        }
+        previousMsection = currentMsection;
+      }
+    }
+
+    EXPECT_TRUE(swapped);
+
+    return parsed->ToString();
+  }
+
   void CheckRtcpFbSdp(const std::string &sdp,
                       const std::set<std::string>& expected) {
 
     std::set<std::string>::const_iterator it;
 
     // Iterate through the list of expected feedback types and ensure
     // that none of them are missing.
     for (it = expected.begin(); it != expected.end(); ++it) {
@@ -1943,31 +1999,27 @@ public:
     a2_->SetLocal(TestObserver::ANSWER, a2_->answer());
 
     std::string modifiedAnswer(HardcodeRtcpFb(a2_->answer(), feedback));
 
     a1_->SetRemote(TestObserver::ANSWER, modifiedAnswer);
 
     WaitForCompleted();
 
-    a1_->CloseSendStreams();
-    a1_->CloseReceiveStreams();
-    a2_->CloseSendStreams();
-    a2_->CloseReceiveStreams();
+    CloseStreams();
 
     // Check caller video settings for remote pipeline
     a1_->CheckMediaPipeline(0, true, (fRtcpMux ? PIPELINE_RTCP_MUX : 0) |
       PIPELINE_VIDEO | rtcpFbFlags, frameRequestMethod);
 
     // Check caller video settings for remote pipeline
     // (Should use pli and nack, regardless of what was in the offer)
     a2_->CheckMediaPipeline(0, true,
                             (fRtcpMux ? PIPELINE_RTCP_MUX : 0) |
                             PIPELINE_VIDEO |
-                            PIPELINE_SEND |
                             PIPELINE_RTCP_NACK,
                             VideoSessionConduit::FrameRequestPli);
   }
 
   void TestRtcpFbOffer(
       const std::set<std::string>& feedback,
       uint32_t rtcpFbFlags,
       VideoSessionConduit::FrameRequestType frameRequestMethod) {
@@ -1983,31 +2035,27 @@ public:
     a2_->SetRemote(TestObserver::OFFER, modifiedOffer);
     a2_->CreateAnswer(OFFER_AV | ANSWER_AV);
 
     a2_->SetLocal(TestObserver::ANSWER, a2_->answer());
     a1_->SetRemote(TestObserver::ANSWER, a2_->answer());
 
     WaitForCompleted();
 
-    a1_->CloseSendStreams();
-    a1_->CloseReceiveStreams();
-    a2_->CloseSendStreams();
-    a2_->CloseReceiveStreams();
+    CloseStreams();
 
     // Check callee video settings for remote pipeline
     a2_->CheckMediaPipeline(0, true, (fRtcpMux ? PIPELINE_RTCP_MUX : 0) |
       PIPELINE_VIDEO | rtcpFbFlags, frameRequestMethod);
 
     // Check caller video settings for remote pipeline
     // (Should use pli and nack, regardless of what was in the offer)
     a1_->CheckMediaPipeline(0, true,
                             (fRtcpMux ? PIPELINE_RTCP_MUX : 0) |
                             PIPELINE_VIDEO |
-                            PIPELINE_SEND |
                             PIPELINE_RTCP_NACK,
                             VideoSessionConduit::FrameRequestPli);
   }
 
   void SetTestStunServer() {
     stun_addr_ = TestStunServer::GetInstance()->addr();
     stun_port_ = TestStunServer::GetInstance()->port();
 
@@ -2055,16 +2103,24 @@ public:
     // Check max-fr value
     if (max_fr > 0) {
       std::stringstream ss;
       ss << "max-fr=" << max_fr;
       ASSERT_NE(line.find(ss.str()), std::string::npos);
     }
   }
 
+  void CloseStreams()
+  {
+    a1_->CloseSendStreams();
+    a2_->CloseSendStreams();
+    a1_->CloseReceiveStreams();
+    a2_->CloseReceiveStreams();
+  }
+
  protected:
   bool init_;
   ScopedDeletePtr<SignalingAgent> a1_;  // Canonically "caller"
   ScopedDeletePtr<SignalingAgent> a2_;  // Canonically "callee"
   std::string stun_addr_;
   uint16_t stun_port_;
 };
 
@@ -2183,44 +2239,40 @@ TEST_P(SignalingTest, CreateOfferDontRec
 {
   OfferOptions options;
   options.setInt32Option("OfferToReceiveAudio", 1);
   options.setInt32Option("OfferToReceiveVideo", 0);
   CreateOffer(options, OFFER_AV,
               SHOULD_SENDRECV_AUDIO | SHOULD_SEND_VIDEO);
 }
 
-// XXX Disabled pending resolution of Bug 840728
-TEST_P(SignalingTest, DISABLED_CreateOfferRemoveAudioStream)
+TEST_P(SignalingTest, CreateOfferRemoveAudioTrack)
 {
   OfferOptions options;
   options.setInt32Option("OfferToReceiveAudio", 1);
   options.setInt32Option("OfferToReceiveVideo", 1);
-  CreateOfferRemoveStream(options, DOMMediaStream::HINT_CONTENTS_AUDIO,
-              SHOULD_RECV_AUDIO | SHOULD_SENDRECV_VIDEO);
+  CreateOfferRemoveTrack(options, false,
+                         SHOULD_RECV_AUDIO | SHOULD_SENDRECV_VIDEO);
 }
 
-// XXX Disabled pending resolution of Bug 840728
-TEST_P(SignalingTest, DISABLED_CreateOfferDontReceiveAudioRemoveAudioStream)
+TEST_P(SignalingTest, CreateOfferDontReceiveAudioRemoveAudioTrack)
 {
   OfferOptions options;
   options.setInt32Option("OfferToReceiveAudio", 0);
   options.setInt32Option("OfferToReceiveVideo", 1);
-  CreateOfferRemoveStream(options, DOMMediaStream::HINT_CONTENTS_AUDIO,
-              SHOULD_SENDRECV_VIDEO);
+  CreateOfferRemoveTrack(options, false, SHOULD_SENDRECV_VIDEO | SHOULD_OMIT_AUDIO);
 }
 
-// XXX Disabled pending resolution of Bug 840728
-TEST_P(SignalingTest, DISABLED_CreateOfferDontReceiveVideoRemoveVideoStream)
+TEST_P(SignalingTest, CreateOfferDontReceiveVideoRemoveVideoTrack)
 {
   OfferOptions options;
   options.setInt32Option("OfferToReceiveAudio", 1);
   options.setInt32Option("OfferToReceiveVideo", 0);
-  CreateOfferRemoveStream(options, DOMMediaStream::HINT_CONTENTS_VIDEO,
-              SHOULD_SENDRECV_AUDIO);
+  CreateOfferRemoveTrack(options, true,
+                         SHOULD_SENDRECV_AUDIO);
 }
 
 TEST_P(SignalingTest, OfferAnswerNothingDisabled)
 {
   OfferOptions options;
   OfferAnswer(options, OFFER_AV | ANSWER_AV,
               SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
 }
@@ -2354,18 +2406,17 @@ TEST_P(SignalingTest, OfferAnswerVideoIn
   OfferAnswer(options, OFFER_AUDIO | ANSWER_AUDIO,
               SHOULD_SENDRECV_AUDIO | SHOULD_RECV_VIDEO,
               SHOULD_SENDRECV_AUDIO | SHOULD_INACTIVE_VIDEO);
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   // Check that we wrote a bunch of data
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   //ASSERT_GE(a2_->GetPacketsSent(0), 40);
   //ASSERT_GE(a1_->GetPacketsReceived(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 TEST_P(SignalingTest, OfferAnswerBothInactive)
@@ -2559,47 +2610,376 @@ TEST_P(SignalingTest, FullCall)
   OfferOptions options;
   OfferAnswer(options, OFFER_AV | ANSWER_AV,
               SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= 40 &&
+                   a1_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+
+  CloseStreams();
   // Check that we wrote a bunch of data
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
-  //ASSERT_GE(a2_->GetPacketsSent(0), 40);
-  //ASSERT_GE(a1_->GetPacketsReceived(0), 40);
+  ASSERT_GE(a2_->GetPacketsSent(0), 40);
+  ASSERT_GE(a1_->GetPacketsReceived(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 
   // Check the low-level media pipeline
   // for RTP and RTCP flows
   // The first Local pipeline gets stored at 0
   a1_->CheckMediaPipeline(0, false, fRtcpMux ?
-    PIPELINE_LOCAL | PIPELINE_RTCP_MUX | PIPELINE_SEND :
-    PIPELINE_LOCAL | PIPELINE_SEND);
+    PIPELINE_RTCP_MUX | PIPELINE_SEND :
+    PIPELINE_SEND);
+
+  // The first Remote pipeline gets stored at 0
+  a2_->CheckMediaPipeline(0, false, (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+}
+
+TEST_P(SignalingTest, RenegotiationOffererAddsTracks)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40, kDefaultTimeout * 2);
+
+  // OFFER_AV causes a new stream + tracks to be added
+  OfferAnswer(options, OFFER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some more data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(1) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(1) >= 40, kDefaultTimeout * 2);
+
+  CloseStreams();
+
+  // Check the low-level media pipeline
+  // for RTP and RTCP flows
+  for (size_t i = 0; i < 2; ++i) {
+    a2_->CheckMediaPipeline(i,
+                            false,
+                            (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+    a1_->CheckMediaPipeline(i,
+                            false,
+                            (fRtcpMux ?  PIPELINE_RTCP_MUX : 0) |
+                            PIPELINE_SEND);
+  }
+
+  a1_->CheckMediaPipeline(0,
+                          false,
+                          (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+  a2_->CheckMediaPipeline(0,
+                          false,
+                          (fRtcpMux ?  PIPELINE_RTCP_MUX : 0) |
+                          PIPELINE_SEND);
+}
+
+TEST_P(SignalingTest, RenegotiationOffererRemovesTrack)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get received
+  ASSERT_TRUE_WAIT(a1_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= 40, kDefaultTimeout * 2);
+
+  int a2PacketsSent = a2_->GetPacketsSent(0);
+  int a1PacketsReceived = a1_->GetPacketsReceived(0);
+
+  a1_->RemoveTrack(0, false);
+
+  OfferAnswer(options, OFFER_NONE,
+              SHOULD_RECV_AUDIO | SHOULD_SENDRECV_VIDEO,
+              SHOULD_SEND_AUDIO | SHOULD_SENDRECV_VIDEO);
+
+  ASSERT_TRUE_WAIT(a1_->GetPacketsReceived(0) >= a1PacketsReceived + 40,
+                   kDefaultTimeout * 2);
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= a2PacketsSent + 40,
+                   kDefaultTimeout * 2);
+
+  CloseStreams();
+}
+
+TEST_P(SignalingTest, RenegotiationOffererReplacesTrack)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40, kDefaultTimeout * 2);
+
+  a1_->RemoveTrack(0, false);
+
+  // OFFER_AUDIO causes a new audio track to be added on both sides
+  OfferAnswer(options, OFFER_AUDIO,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some more data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(1) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(1) >= 40, kDefaultTimeout * 2);
+
+  CloseStreams();
+
+  // Check the low-level media pipeline
+  // for RTP and RTCP flows
+  a1_->CheckMediaPipeline(1, false, fRtcpMux ?
+    PIPELINE_RTCP_MUX | PIPELINE_SEND :
+    PIPELINE_SEND);
+
+  a2_->CheckMediaPipeline(1, false, (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+}
+
+TEST_P(SignalingTest, RenegotiationOffererSwapsMsids)
+{
+  OfferOptions options;
+
+  EnsureInit();
+  // Create a media stream as if it came from GUM
+  Fake_AudioStreamSource *audio_stream =
+    new Fake_AudioStreamSource();
+
+  nsresult ret;
+  mozilla::SyncRunnable::DispatchToThread(
+    test_utils->sts_target(),
+    WrapRunnableRet(audio_stream, &Fake_MediaStream::Start, &ret));
+
+  ASSERT_TRUE(NS_SUCCEEDED(ret));
+
+  a1_->AddStream(DOMMediaStream::HINT_CONTENTS_AUDIO |
+                 DOMMediaStream::HINT_CONTENTS_VIDEO, audio_stream);
+
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40, kDefaultTimeout * 2);
+
+  a1_->CreateOffer(options, OFFER_NONE, SHOULD_SENDRECV_AV);
+  a1_->SetLocal(TestObserver::OFFER, a1_->offer());
+  std::string audioSwapped = SwapMsids(a1_->offer(), false);
+  std::string audioAndVideoSwapped = SwapMsids(audioSwapped, true);
+  std::cout << "Msids swapped: " << std::endl << audioAndVideoSwapped << std::endl;
+  a2_->SetRemote(TestObserver::OFFER, audioAndVideoSwapped);
+  Answer(options, OFFER_NONE, SHOULD_SENDRECV_AV, BOTH_TRICKLE);
+  WaitForCompleted();
+
+  // Wait for some more data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(1) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(1) >= 40, kDefaultTimeout * 2);
+
+  CloseStreams();
+
+  for (size_t i = 0; i < 2; ++i) {
+    // Check the low-level media pipeline
+    // for RTP and RTCP flows
+    a1_->CheckMediaPipeline(i, false, fRtcpMux ?
+      PIPELINE_RTCP_MUX | PIPELINE_SEND :
+      PIPELINE_SEND);
+
+    a2_->CheckMediaPipeline(i, false, (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+  }
+}
+
+TEST_P(SignalingTest, RenegotiationAnswererAddsTracks)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get received
+  ASSERT_TRUE_WAIT(a1_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= 40, kDefaultTimeout * 2);
+
+  options.setInt32Option("OfferToReceiveAudio", 2);
+  options.setInt32Option("OfferToReceiveVideo", 2);
+
+  // ANSWER_AV causes a new stream + tracks to be added
+  OfferAnswer(options, ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  ASSERT_TRUE_WAIT(a1_->GetPacketsReceived(1) >= 40, kDefaultTimeout * 2);
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(1) >= 40, kDefaultTimeout * 2);
+
+  CloseStreams();
+
+  // Check the low-level media pipeline
+  // for RTP and RTCP flows
+  for (size_t i = 0; i < 2; ++i) {
+    a1_->CheckMediaPipeline(i,
+                            false,
+                            (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+    a2_->CheckMediaPipeline(i,
+                            false,
+                            (fRtcpMux ?  PIPELINE_RTCP_MUX : 0) |
+                            PIPELINE_SEND);
+  }
+
+  a2_->CheckMediaPipeline(0,
+                          false,
+                          (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+  a1_->CheckMediaPipeline(0,
+                          false,
+                          (fRtcpMux ?  PIPELINE_RTCP_MUX : 0) |
+                          PIPELINE_SEND);
+}
+
+TEST_P(SignalingTest, RenegotiationAnswererRemovesTrack)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get received
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+  // Not really packets, but audio segments, happens later
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40, kDefaultTimeout * 2);
+
+  int a1PacketsSent = a1_->GetPacketsSent(0);
+  int a2PacketsReceived = a2_->GetPacketsReceived(0);
+
+  a2_->RemoveTrack(0, false);
+
+  OfferAnswer(options, OFFER_NONE,
+              SHOULD_SENDRECV_AUDIO | SHOULD_SENDRECV_VIDEO,
+              SHOULD_RECV_AUDIO | SHOULD_SENDRECV_VIDEO);
+
+  ASSERT_TRUE_WAIT(a2_->GetPacketsReceived(0) >= a2PacketsReceived + 40,
+                   kDefaultTimeout * 2);
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= a1PacketsSent + 40,
+                   kDefaultTimeout * 2);
+
+  CloseStreams();
+}
+
+TEST_P(SignalingTest, RenegotiationAnswererReplacesTrack)
+{
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get written
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
+                   a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= 40 &&
+                   a1_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+
+  int a1PacketsSent = a1_->GetPacketsSent(0);
+  int a2PacketsReceived = a2_->GetPacketsReceived(0);
+
+  a2_->RemoveTrack(0, false);
+
+  // ANSWER_AUDIO causes a new audio track to be added
+  OfferAnswer(options, ANSWER_AUDIO,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some more data to get written
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= a1PacketsSent + 40 &&
+                   a2_->GetPacketsReceived(0) >= a2PacketsReceived + 40,
+                   kDefaultTimeout * 2);
+
+  // The other direction is going to start over
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= 40 &&
+                   a1_->GetPacketsReceived(0) >= 40,
+                   kDefaultTimeout * 2);
+
+  CloseStreams();
+
+  // Check the low-level media pipeline
+  // for RTP and RTCP flows
+  a1_->CheckMediaPipeline(0, false, fRtcpMux ?
+    PIPELINE_RTCP_MUX | PIPELINE_SEND :
+    PIPELINE_SEND);
+
+  a2_->CheckMediaPipeline(0, false, (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
+}
+
+TEST_P(SignalingTest, BundleRenegotiation)
+{
+  if (GetParam() == "bundle") {
+    // We don't support ICE restart, which is a prereq for renegotiating bundle
+    // off.
+    return;
+  }
+
+  OfferOptions options;
+  OfferAnswer(options, OFFER_AV | ANSWER_AV,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some data to get written
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
+                   a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= 40 &&
+                   a1_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
+
+  int a1PacketsSent = a1_->GetPacketsSent(0);
+  int a2PacketsSent = a2_->GetPacketsSent(0);
+  int a1PacketsReceived = a1_->GetPacketsReceived(0);
+  int a2PacketsReceived = a2_->GetPacketsReceived(0);
+
+  // If we did bundle before, turn it off, if not, turn it on
+  if (a1_->mBundleEnabled && a2_->mBundleEnabled) {
+    a1_->SetBundleEnabled(false);
+  } else {
+    a1_->SetBundleEnabled(true);
+    a2_->SetBundleEnabled(true);
+  }
+
+  OfferAnswer(options, OFFER_NONE,
+              SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
+
+  // Wait for some more data to get written
+  ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= a1PacketsSent + 40 &&
+                   a2_->GetPacketsReceived(0) >= a2PacketsReceived + 40,
+                   kDefaultTimeout * 2);
+
+  ASSERT_TRUE_WAIT(a2_->GetPacketsSent(0) >= a2PacketsSent + 40 &&
+                   a1_->GetPacketsReceived(0) >= a1PacketsReceived + 40,
+                   kDefaultTimeout * 2);
+
+  // Check the low-level media pipeline
+  // for RTP and RTCP flows
+  // The first Local pipeline gets stored at 0
+  a1_->CheckMediaPipeline(0, false, fRtcpMux ?
+    PIPELINE_RTCP_MUX | PIPELINE_SEND :
+    PIPELINE_SEND);
 
   // The first Remote pipeline gets stored at 0
   a2_->CheckMediaPipeline(0, false, (fRtcpMux ?  PIPELINE_RTCP_MUX : 0));
 }
 
 TEST_P(SignalingTest, FullCallAudioOnly)
 {
   OfferOptions options;
   OfferAnswer(options, OFFER_AUDIO | ANSWER_AUDIO,
               SHOULD_SENDRECV_AUDIO, SHOULD_SENDRECV_AUDIO);
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   // Check that we wrote a bunch of data
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   //ASSERT_GE(a2_->GetPacketsSent(0), 40);
   //ASSERT_GE(a1_->GetPacketsReceived(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // FIXME -- reject offered stream by .stop()ing the MST that was offered instead,
@@ -2612,18 +2992,17 @@ TEST_P(SignalingTest, DISABLED_FullCallA
   answeroptions.setInt32Option("offerToReceiveVideo", 0);
   OfferAnswer(offeroptions, OFFER_AV | ANSWER_AUDIO,
               SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AUDIO);
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   // Check that we wrote a bunch of data
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   //ASSERT_GE(a2_->GetPacketsSent(0), 40);
   //ASSERT_GE(a1_->GetPacketsReceived(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 TEST_P(SignalingTest, FullCallVideoOnly)
@@ -2633,18 +3012,17 @@ TEST_P(SignalingTest, FullCallVideoOnly)
               SHOULD_SENDRECV_VIDEO | SHOULD_OMIT_AUDIO,
               SHOULD_SENDRECV_VIDEO | SHOULD_OMIT_AUDIO);
 
   // If we could check for video packets, we would wait for some to be written
   // here. Since we can't, we don't.
   // ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
   //                 a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   // FIXME -- Ideally we would check that packets were sent
   // and received; however, the test driver setup does not
   // currently support sending/receiving with Fake_VideoStreamSource.
   //
   // Check that we wrote a bunch of data
   // ASSERT_GE(a1_->GetPacketsSent(0), 40);
   //ASSERT_GE(a2_->GetPacketsSent(0), 40);
@@ -2665,36 +3043,34 @@ TEST_P(SignalingTest, OfferAndAnswerWith
   sdpWrapper.AddLine("a=rtpmap:8 PCMA/8000\r\n");
   std::cout << "Modified SDP " << std::endl
             << indent(sdpWrapper.getSdp()) << std::endl;
 
   a1_->SetRemote(TestObserver::ANSWER, sdpWrapper.getSdp());
 
   WaitForCompleted();
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, FullCallTrickle)
 {
   OfferOptions options;
   OfferAnswer(options,
               OFFER_AV | ANSWER_AV,
               SHOULD_SENDRECV_AV,
               SHOULD_SENDRECV_AV);
 
   std::cerr << "ICE handshake completed" << std::endl;
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // Offer answer with trickle but with chrome-style candidates
 TEST_P(SignalingTest, DISABLED_FullCallTrickleChrome)
 {
   OfferOptions options;
@@ -2703,18 +3079,17 @@ TEST_P(SignalingTest, DISABLED_FullCallT
                            SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
 
   std::cerr << "ICE handshake completed" << std::endl;
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 TEST_P(SignalingTest, FullCallTrickleBeforeSetLocal)
 {
   OfferOptions options;
   Offer(options, OFFER_AV | ANSWER_AV, SHOULD_SENDRECV_AV);
@@ -2729,18 +3104,17 @@ TEST_P(SignalingTest, FullCallTrickleBef
   WaitForCompleted();
 
   std::cerr << "ICE handshake completed" << std::endl;
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // This test comes from Bug 810220
 TEST_P(SignalingTest, AudioOnlyG711Call)
 {
   EnsureInit();
@@ -3399,26 +3773,25 @@ TEST_P(SignalingTest, AudioOnlyCalleeNoR
             std::string::npos) << "SDP was: " << a2_->getLocalDescription();
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 
   // Check the low-level media pipeline
   // for RTP and RTCP flows
   // The first Local pipeline gets stored at 0
-  a1_->CheckMediaPipeline(0, false, PIPELINE_LOCAL | PIPELINE_SEND);
+  a1_->CheckMediaPipeline(0, false, PIPELINE_SEND);
   a2_->CheckMediaPipeline(0, false, 0);
 }
 
 
 
 TEST_P(SignalingTest, AudioOnlyG722Only)
 {
   EnsureInit();
@@ -3440,18 +3813,17 @@ TEST_P(SignalingTest, AudioOnlyG722Only)
   ASSERT_NE(a2_->getLocalDescription().find("a=rtpmap:9 G722/8000"), std::string::npos);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 TEST_P(SignalingTest, AudioOnlyG722MostPreferred)
 {
   EnsureInit();
@@ -3467,18 +3839,17 @@ TEST_P(SignalingTest, AudioOnlyG722MostP
             << indent(sdpWrapper.getSdp()) << std::endl;
   a2_->SetRemote(TestObserver::OFFER, sdpWrapper.getSdp(), false);
   a2_->CreateAnswer(OFFER_AUDIO | ANSWER_AUDIO);
   a2_->SetLocal(TestObserver::ANSWER, a2_->answer(), false);
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
   ASSERT_NE(a2_->getLocalDescription().find("RTP/SAVPF 9"), std::string::npos);
   ASSERT_NE(a2_->getLocalDescription().find("a=rtpmap:9 G722/8000"), std::string::npos);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, AudioOnlyG722Rejected)
 {
   EnsureInit();
 
   OfferOptions options;
 
@@ -3497,18 +3868,17 @@ TEST_P(SignalingTest, AudioOnlyG722Rejec
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
   // TODO(bug 1099351): Use commented out code instead.
   ASSERT_NE(a2_->getLocalDescription().find("RTP/SAVPF 0\r"), std::string::npos);
   // ASSERT_NE(a2_->getLocalDescription().find("RTP/SAVPF 0 8\r"), std::string::npos);
   ASSERT_NE(a2_->getLocalDescription().find("a=rtpmap:0 PCMU/8000"), std::string::npos);
   ASSERT_EQ(a2_->getLocalDescription().find("a=rtpmap:109 opus/48000/2"), std::string::npos);
   ASSERT_EQ(a2_->getLocalDescription().find("a=rtpmap:9 G722/8000"), std::string::npos);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, FullCallAudioNoMuxVideoMux)
 {
   if (GetParam() == "bundle") {
     // This test doesn't make sense for bundle
     return;
   }
@@ -3537,30 +3907,29 @@ TEST_P(SignalingTest, FullCallAudioNoMux
   ASSERT_EQ(match, std::string::npos);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 
   // Check the low-level media pipeline
   // for RTP and RTCP flows
   // The first Local pipeline gets stored at 0
-  a1_->CheckMediaPipeline(0, false, PIPELINE_LOCAL | PIPELINE_SEND);
+  a1_->CheckMediaPipeline(0, false, PIPELINE_SEND);
 
   // Now check video mux.
   a1_->CheckMediaPipeline(0, true,
-    PIPELINE_LOCAL | (fRtcpMux ? PIPELINE_RTCP_MUX : 0) | PIPELINE_SEND |
+    (fRtcpMux ? PIPELINE_RTCP_MUX : 0) | PIPELINE_SEND |
     PIPELINE_VIDEO);
 
   // The first Remote pipeline gets stored at 0
   a2_->CheckMediaPipeline(0, false, 0);
 
   // Now check video mux.
   a2_->CheckMediaPipeline(0, true, (fRtcpMux ?  PIPELINE_RTCP_MUX : 0) |
     PIPELINE_VIDEO | PIPELINE_RTCP_NACK, VideoSessionConduit::FrameRequestPli);
@@ -3740,18 +4109,17 @@ TEST_P(SignalingTest, AudioCallForceDtls
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // In this test we will change the offer SDP's a=setup value
 // from actpass to active.  This will make the answer do passive
 TEST_P(SignalingTest, AudioCallReverseDtlsRoles)
@@ -3789,18 +4157,17 @@ TEST_P(SignalingTest, AudioCallReverseDt
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // In this test we will change the answer SDP's a=setup value
 // from active to passive.  This will make both sides do
 // active and should not connect.
@@ -3837,18 +4204,17 @@ TEST_P(SignalingTest, AudioCallMismatchD
   // This should setup the DTLS with both sides playing active
   a1_->SetRemote(TestObserver::ANSWER, answer, false);
 
   WaitForCompleted();
 
   // Not using ASSERT_TRUE_WAIT here because we expect failure
   PR_Sleep(kDefaultTimeout * 2); // Wait for some data to get written
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   // In this case we should receive nothing.
   ASSERT_EQ(a2_->GetPacketsReceived(0), 0);
 }
 
 // In this test we will change the offer SDP's a=setup value
 // from actpass to garbage.  It should ignore the garbage value
@@ -3887,18 +4253,17 @@ TEST_P(SignalingTest, AudioCallGarbageSe
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // In this test we will change the offer SDP to remove the
 // a=setup line.  Answer should respond with a=setup:active.
 TEST_P(SignalingTest, AudioCallOfferNoSetupOrConnection)
@@ -3935,18 +4300,17 @@ TEST_P(SignalingTest, AudioCallOfferNoSe
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 // In this test we will change the answer SDP to remove the
 // a=setup line.  ICE should still connect since active will
 // be assumed.
@@ -3983,36 +4347,34 @@ TEST_P(SignalingTest, AudioCallAnswerNoS
   a1_->SetRemote(TestObserver::ANSWER, answer, false);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 
 TEST_P(SignalingTest, FullCallRealTrickle)
 {
   OfferOptions options;
   OfferAnswer(options, OFFER_AV | ANSWER_AV,
               SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 TEST_P(SignalingTest, FullCallRealTrickleTestServer)
 {
   SetTestStunServer();
 
@@ -4021,18 +4383,17 @@ TEST_P(SignalingTest, FullCallRealTrickl
               SHOULD_SENDRECV_AV, SHOULD_SENDRECV_AV);
 
   TestStunServer::GetInstance()->SetActive(true);
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
   ASSERT_GE(a1_->GetPacketsSent(0), 40);
   ASSERT_GE(a2_->GetPacketsReceived(0), 40);
 }
 
 TEST_P(SignalingTest, hugeSdp)
 {
   EnsureInit();
 
@@ -4528,18 +4889,17 @@ TEST_P(SignalingTest, AnswerWithoutVP8)
 
   a1_->SetRemote(TestObserver::ANSWER, answer, false);
 
   ASSERT_EQ(a1_->pObserver->lastStatusCode,
             PeerConnectionImpl::kNoError);
 
   WaitForCompleted();
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 // Test using a non preferred dynamic video payload type on answer negotiation
 TEST_P(SignalingTest, UseNonPrefferedPayloadTypeOnAnswer)
 {
   EnsureInit();
 
   OfferOptions options;
@@ -4597,18 +4957,17 @@ TEST_P(SignalingTest, UseNonPrefferedPay
             PeerConnectionImpl::kNoError);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 40 &&
                    a2_->GetPacketsReceived(0) >= 40, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, VideoNegotiationFails)
 {
   EnsureInit();
 
   OfferOptions options;
 
@@ -4641,18 +5000,17 @@ TEST_P(SignalingTest, VideoNegotiationFa
             PeerConnectionImpl::kNoError);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 10 &&
                    a2_->GetPacketsReceived(0) >= 10, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, AudioNegotiationFails)
 {
   EnsureInit();
 
   OfferOptions options;
 
@@ -4676,18 +5034,17 @@ TEST_P(SignalingTest, AudioNegotiationFa
 
   a1_->SetRemote(TestObserver::ANSWER, a2_->answer(), false);
 
   ASSERT_EQ(a1_->pObserver->lastStatusCode,
             PeerConnectionImpl::kNoError);
 
   WaitForCompleted();
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, BundleStreamCorrelationBySsrc)
 {
   if (GetParam() != "bundle") {
     return;
   }
 
@@ -4734,18 +5091,17 @@ TEST_P(SignalingTest, BundleStreamCorrel
             PeerConnectionImpl::kNoError);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 10 &&
                    a2_->GetPacketsReceived(0) >= 10, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 TEST_P(SignalingTest, BundleStreamCorrelationByUniquePt)
 {
   if (GetParam() != "bundle") {
     return;
   }
 
@@ -4789,18 +5145,17 @@ TEST_P(SignalingTest, BundleStreamCorrel
             PeerConnectionImpl::kNoError);
 
   WaitForCompleted();
 
   // Wait for some data to get written
   ASSERT_TRUE_WAIT(a1_->GetPacketsSent(0) >= 10 &&
                    a2_->GetPacketsReceived(0) >= 10, kDefaultTimeout * 2);
 
-  a1_->CloseSendStreams();
-  a2_->CloseReceiveStreams();
+  CloseStreams();
 }
 
 INSTANTIATE_TEST_CASE_P(Variants, SignalingTest,
                         ::testing::Values("bundle",
                                           "no_bundle",
                                           "reject_bundle"));
 
 } // End namespace test.