Backed out 8 changesets (bug 1290948) for failing mochitest dom/tests/mochitest/general/test_interfaces.html r=backout on a CLOSED TREE
authorAndreea Pavel <apavel@mozilla.com>
Tue, 14 Nov 2017 21:02:11 +0200
changeset 446042 e077a6e6e8425ca49a7992e3ee0a589ebe050184
parent 446041 9f310ebf0488fce55e5618a31497c8b1460699c7
child 446043 edadec9ac2051856ab1f3e06ba9367fa7f65a115
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1290948
milestone59.0a1
backs out97a271bf671eb1e8adb9378ee4052e0d93a40a80
8ff38e646037f6857357efe95370b725d070fdf4
314675023cd5dd745f7f1b9cccb537b762b495f2
1a5f090502b0a27641a0866513a82aa8c545fc14
ffb6e6da955fa81fc4faca06210aa2e5764ed205
56c169018cebfab1e4bb3a34b1aeddba2fec3d66
49878c508ce6741eceb48ffe337796d5e7897497
bbe53fb92e21bfd5ec6ace0a854a483dbf4514ab
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
Backed out 8 changesets (bug 1290948) for failing mochitest dom/tests/mochitest/general/test_interfaces.html r=backout on a CLOSED TREE Backed out changeset 97a271bf671e (bug 1290948) Backed out changeset 8ff38e646037 (bug 1290948) Backed out changeset 314675023cd5 (bug 1290948) Backed out changeset 1a5f090502b0 (bug 1290948) Backed out changeset ffb6e6da955f (bug 1290948) Backed out changeset 56c169018ceb (bug 1290948) Backed out changeset 49878c508ce6 (bug 1290948) Backed out changeset bbe53fb92e21 (bug 1290948)
dom/bindings/Bindings.conf
dom/media/PeerConnection.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_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_addtrack_removetrack_events.html
dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_basicAudioVideoTransceivers.html
dom/media/tests/mochitest/test_peerConnection_bug1064223.html
dom/media/tests/mochitest/test_peerConnection_constructedStream.html
dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html
dom/media/tests/mochitest/test_peerConnection_localRollback.html
dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.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
dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
dom/media/tests/mochitest/test_peerConnection_setParameters.html
dom/media/tests/mochitest/test_peerConnection_transceivers.html
dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html
dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
dom/webidl/MediaStream.webidl
dom/webidl/MediaStreamList.webidl
dom/webidl/PeerConnectionImpl.webidl
dom/webidl/PeerConnectionObserver.webidl
dom/webidl/RTCPeerConnection.webidl
dom/webidl/RTCRtpSender.webidl
dom/webidl/RTCRtpTransceiver.webidl
dom/webidl/RTCTrackEvent.webidl
dom/webidl/TransceiverImpl.webidl
dom/webidl/moz.build
media/mtransport/nricectx.cpp
media/mtransport/nricectx.h
media/mtransport/test/transport_unittests.cpp
media/mtransport/transportlayerice.cpp
media/mtransport/transportlayerice.h
media/webrtc/signaling/gtest/jsep_session_unittest.cpp
media/webrtc/signaling/gtest/jsep_track_unittest.cpp
media/webrtc/signaling/gtest/mediapipeline_unittest.cpp
media/webrtc/signaling/src/jsep/JsepSession.h
media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
media/webrtc/signaling/src/jsep/JsepSessionImpl.h
media/webrtc/signaling/src/jsep/JsepTrack.cpp
media/webrtc/signaling/src/jsep/JsepTrack.h
media/webrtc/signaling/src/jsep/JsepTrackEncoding.h
media/webrtc/signaling/src/jsep/JsepTransceiver.h
media/webrtc/signaling/src/jsep/JsepTransport.h
media/webrtc/signaling/src/jsep/SsrcGenerator.cpp
media/webrtc/signaling/src/jsep/SsrcGenerator.h
media/webrtc/signaling/src/jsep/moz.build
media/webrtc/signaling/src/mediapipeline/MediaPipeline.cpp
media/webrtc/signaling/src/mediapipeline/MediaPipeline.h
media/webrtc/signaling/src/mediapipeline/MediaPipelineFilter.cpp
media/webrtc/signaling/src/mediapipeline/moz.build
media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.h
media/webrtc/signaling/src/peerconnection/MediaStreamList.cpp
media/webrtc/signaling/src/peerconnection/MediaStreamList.h
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
media/webrtc/signaling/src/peerconnection/RemoteTrackSource.h
media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
media/webrtc/signaling/src/peerconnection/moz.build
media/webrtc/signaling/src/sdp/SdpAttribute.h
media/webrtc/signaling/src/sdp/SdpHelper.cpp
media/webrtc/signaling/src/sdp/SdpHelper.h
media/webrtc/signaling/src/sdp/SdpMediaSection.h
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -691,22 +691,16 @@ DOMInterfaces = {
 },
 
 'PeerConnectionImpl': {
     'nativeType': 'mozilla::PeerConnectionImpl',
     'headerFile': 'PeerConnectionImpl.h',
     'wrapperCache': False
 },
 
-'TransceiverImpl': {
-    'nativeType': 'mozilla::TransceiverImpl',
-    'headerFile': 'TransceiverImpl.h',
-    'wrapperCache': False
-},
-
 'Plugin': {
     'headerFile' : 'nsPluginArray.h',
     'nativeType': 'nsPluginElement',
 },
 
 'PluginArray': {
     'nativeType': 'nsPluginArray',
 },
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -20,30 +20,28 @@ const PC_CONTRACT = "@mozilla.org/dom/pe
 const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
 const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1";
 const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
 const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
 const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1";
 const PC_RECEIVER_CONTRACT = "@mozilla.org/dom/rtpreceiver;1";
-const PC_TRANSCEIVER_CONTRACT = "@mozilla.org/dom/rtptransceiver;1";
 const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
 const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1";
 
 const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
 const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
 const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
 const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
 const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
 const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}");
 const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
 const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}");
 const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}");
-const PC_TRANSCEIVER_CID = Components.ID("{09475754-103a-41f5-a2d0-e1f27eb0b537}");
 const PC_COREQUEST_CID = Components.ID("{74b2122d-65a8-4824-aa9e-3d664cb75dc2}");
 const PC_DTMF_SENDER_CID = Components.ID("{3610C242-654E-11E6-8EC0-6D1BE389A607}");
 
 function logMsg(msg, file, line, flag, winID) {
   let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
   let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
   scriptError.initWithWindowID(msg, file, null, line, 0, flag,
                                "content javascript", winID);
@@ -145,18 +143,19 @@ class GlobalPCList {
       this.handleGMPCrash(data);
     }
   }
 
   observe(subject, topic, data) {
     let cleanupPcRef = function(pcref) {
       let pc = pcref.get();
       if (pc) {
-        pc._suppressEvents = true;
-        pc.close();
+        pc._pc.close();
+        delete pc._observer;
+        pc._pc = null;
       }
     };
 
     let cleanupWinId = function(list, winID) {
       if (list.hasOwnProperty(winID)) {
         list[winID].forEach(cleanupPcRef);
         delete list[winID];
       }
@@ -343,18 +342,18 @@ setupPrototype(RTCStatsReport, {
         "candidate-pair": "candidatepair",
         "local-candidate": "localcandidate",
         "remote-candidate": "remotecandidate"
   }
 });
 
 class RTCPeerConnection {
   constructor() {
-    this._receiveStreams = new Map();
-    this._transceivers = [];
+    this._senders = [];
+    this._receivers = [];
 
     this._pc = null;
     this._closed = false;
 
     this._localType = null;
     this._remoteType = null;
     // http://rtcweb-wg.github.io/jsep/#rfc.section.4.1.9
     // canTrickle == null means unknown; when a remote description is received it
@@ -585,28 +584,16 @@ class RTCPeerConnection {
 
     try {
       wrapCallback(onSucc)(await func());
     } catch (e) {
       wrapCallback(onErr)(e);
     }
   }
 
-  // This implements the fairly common "Queue a task" logic
-  async _queueTaskWithClosedCheck(func) {
-    return new this._win.Promise(resolve => {
-      Services.tm.dispatchToMainThread({ run() {
-        if (!this._closed) {
-          func();
-          resolve();
-        }
-      }});
-    });
-  }
-
   /**
    * An RTCConfiguration may look like this:
    *
    * { "iceServers": [ { urls: "stun:stun.example.org", },
    *                   { url: "stun:stun.example.org", }, // deprecated version
    *                   { urls: ["turn:turn1.x.org", "turn:turn2.x.org"],
    *                     username:"jib", credential:"mypass"} ] }
    *
@@ -698,17 +685,17 @@ class RTCPeerConnection {
       throw new this._win.DOMException("Peer connection is closed",
                                        "InvalidStateError");
     }
   }
 
   dispatchEvent(event) {
     // PC can close while events are firing if there is an async dispatch
     // in c++ land. But let through "closed" signaling and ice connection events.
-    if (!this._suppressEvents) {
+    if (!this._closed || this._inClose) {
       this.__DOM_IMPL__.dispatchEvent(event);
     }
   }
 
   // Log error message to web console and window.onerror, if present.
   logErrorAndCallOnError(e) {
     this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.exceptionFlag);
 
@@ -765,70 +752,21 @@ class RTCPeerConnection {
                             set(h) {
                               this.logWarning(name + " is deprecated! " + msg);
                               return this.setEH(name, h);
                             }
                           });
   }
 
   createOffer(optionsOrOnSucc, onErr, options) {
-    let onSuccess = null;
+    // This entry-point handles both new and legacy call sig. Decipher which one
     if (typeof optionsOrOnSucc == "function") {
-      onSuccess = optionsOrOnSucc;
-    } else {
-      options = optionsOrOnSucc;
-    }
-
-    // Spec language implies that this needs to happen as if it were called
-    // before createOffer, so we do this as early as possible.
-    this._ensureTransceiversForOfferToReceive(options);
-
-    // This entry-point handles both new and legacy call sig. Decipher which one
-    if (onSuccess) {
-      return this._legacy(onSuccess, onErr, () => this._createOffer(options));
+      return this._legacy(optionsOrOnSucc, onErr, () => this._createOffer(options));
     }
-
-    return this._async(() => this._createOffer(options));
-  }
-
-  // Ensures that we have at least one transceiver of |kind| that is
-  // configured to receive. It will create one if necessary.
-  _ensureOfferToReceive(kind) {
-    let hasRecv = this._transceivers.some(
-      transceiver =>
-        transceiver.getKind() == kind &&
-        (transceiver.direction == "sendrecv" || transceiver.direction == "recvonly") &&
-        !transceiver.stopped);
-
-    if (!hasRecv) {
-      this._addTransceiverNoEvents(kind, {direction: "recvonly"});
-    }
-  }
-
-  // Handles offerToReceiveAudio/Video
-  _ensureTransceiversForOfferToReceive(options) {
-    if (options.offerToReceiveVideo) {
-      this._ensureOfferToReceive("video");
-    }
-
-    if (options.offerToReceiveVideo === false) {
-      this.logWarning("offerToReceiveVideo: false is ignored now. If you " +
-                      "want to disallow a recv track, use " +
-                      "RTCRtpTransceiver.direction");
-    }
-
-    if (options.offerToReceiveAudio) {
-      this._ensureOfferToReceive("audio");
-    }
-
-    if (options.offerToReceiveAudio === false) {
-      this.logWarning("offerToReceiveAudio: false is ignored now. If you " +
-                      "want to disallow a recv track, use " +
-                      "RTCRtpTransceiver.direction");
-    }
+    return this._async(() => this._createOffer(optionsOrOnSucc));
   }
 
   async _createOffer(options) {
     this._checkClosed();
     let origin = Cu.getWebIDLCallerPrincipal().origin;
     return this._chain(async () => {
       let haveAssertion;
       if (this._localIdp.enabled) {
@@ -1123,209 +1061,118 @@ class RTCPeerConnection {
     stream.getTracks().forEach(track => this.addTrack(track, stream));
   }
 
   addTrack(track, stream) {
     if (stream.currentTime === undefined) {
       throw new this._win.DOMException("invalid stream.", "InvalidParameterError");
     }
     this._checkClosed();
-
-    if (this._transceivers.some(
-          transceiver => transceiver.sender.track == track)) {
-      throw new this._win.DOMException("This track is already set on a sender.",
-                                       "InvalidAccessError");
-    }
-
-    let transceiver = this._transceivers.find(transceiver => {
-      return transceiver.sender.track == null &&
-             transceiver.getKind() == track.kind &&
-             !transceiver.stopped &&
-             !transceiver.hasBeenUsedToSend();
+    this._senders.forEach(sender => {
+      if (sender.track == track) {
+        throw new this._win.DOMException("already added.",
+                                         "InvalidParameterError");
+      }
     });
-
-    if (transceiver) {
-      transceiver.sender.setTrack(track);
-      transceiver.sender.setStreams([stream]);
-      if (transceiver.direction == "recvonly") {
-        transceiver.setDirectionInternal("sendrecv");
-      } else if (transceiver.direction == "inactive") {
-        transceiver.setDirectionInternal("sendonly");
-      }
-    } else {
-      transceiver = this._addTransceiverNoEvents(track, {
-        streams: [stream],
-        direction: "sendrecv"
-      });
-    }
-
-    transceiver.setAddTrackMagic();
-    transceiver.sync();
-    this.updateNegotiationNeeded();
-    return transceiver.sender;
+    this._impl.addTrack(track, stream);
+    let sender = this._win.RTCRtpSender._create(this._win,
+                                                new RTCRtpSender(this, track,
+                                                                 stream));
+    this._senders.push(sender);
+    return sender;
   }
 
   removeTrack(sender) {
     this._checkClosed();
-
-    sender.checkWasCreatedByPc(this.__DOM_IMPL__);
-
-    let transceiver =
-      this._transceivers.find(transceiver => transceiver.sender == sender);
-
-    // If the transceiver was removed due to rollback, let it slide.
-    if (!transceiver || !sender.track) {
-      return;
-    }
-
-    // TODO(bug 1401983): Move to TransceiverImpl?
-    this._impl.removeTrack(sender.track);
-
-    sender.setTrack(null);
-    if (transceiver.direction == "sendrecv") {
-      transceiver.setDirectionInternal("recvonly");
-    } else if (transceiver.direction == "sendonly") {
-      transceiver.setDirectionInternal("inactive");
+    var i = this._senders.indexOf(sender);
+    if (i >= 0) {
+      this._senders.splice(i, 1);
+      this._impl.removeTrack(sender.track); // fires negotiation needed
     }
-
-    transceiver.sync();
-    this.updateNegotiationNeeded();
-  }
-
-  _addTransceiverNoEvents(sendTrackOrKind, init) {
-    let sendTrack = null;
-    let kind;
-    if (typeof(sendTrackOrKind) == "string") {
-      kind = sendTrackOrKind;
-      switch (kind) {
-        case "audio":
-        case "video":
-          break;
-        default:
-          throw new this._win.TypeError("Invalid media kind");
-      }
-    } else {
-      sendTrack = sendTrackOrKind;
-      kind = sendTrack.kind;
-    }
-
-    let transceiverImpl = this._impl.createTransceiverImpl(kind, sendTrack);
-    let transceiver = this._win.RTCRtpTransceiver._create(
-        this._win,
-        new RTCRtpTransceiver(this, transceiverImpl, init, kind, sendTrack));
-    transceiver.sync();
-    this._transceivers.push(transceiver);
-    return transceiver;
   }
 
-  _onTransceiverNeeded(kind, transceiverImpl) {
-    let init = {direction: "recvonly"};
-    let transceiver = this._win.RTCRtpTransceiver._create(
-        this._win,
-        new RTCRtpTransceiver(this, transceiverImpl, init, kind, null));
-    transceiver.sync();
-    this._transceivers.push(transceiver);
-  }
-
-  addTransceiver(sendTrackOrKind, init) {
-    let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init);
-    this.updateNegotiationNeeded();
-    return transceiver;
-  }
-
-  _syncTransceivers() {
-    this._transceivers.forEach(transceiver => transceiver.sync());
-  }
-
-  updateNegotiationNeeded() {
-    if (this._closed || this.signalingState != "stable") {
-      return;
-    }
-
-    let negotiationNeeded = this._impl.checkNegotiationNeeded();
-    if (!negotiationNeeded) {
-      this._negotiationNeeded = false;
-      return;
-    }
-
-    if (this._negotiationNeeded) {
-      return;
-    }
-
-    this._negotiationNeeded = true;
-
-    this._queueTaskWithClosedCheck(() => {
-      if (this._negotiationNeeded) {
-        this.dispatchEvent(new this._win.Event("negotiationneeded"));
-      }
-    });
-  }
-
-  _getOrCreateStream(id) {
-    if (!this._receiveStreams.has(id)) {
-      let stream = new this._win.MediaStream();
-      stream.assignId(id);
-      // Legacy event, remove eventually
-      let ev = new this._win.MediaStreamEvent("addstream", { stream });
-      this.dispatchEvent(ev);
-      this._receiveStreams.set(id, stream);
-    }
-
-    return this._receiveStreams.get(id);
-  }
-
-  _insertDTMF(transceiverImpl, tones, duration, interToneGap) {
-    return this._impl.insertDTMF(transceiverImpl, tones, duration, interToneGap);
+  _insertDTMF(sender, tones, duration, interToneGap) {
+    return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
   }
 
   _getDTMFToneBuffer(sender) {
     return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
   }
 
-  _replaceTrack(transceiverImpl, withTrack) {
+  async _replaceTrack(sender, withTrack) {
     this._checkClosed();
-    this._impl.replaceTrackNoRenegotiation(transceiverImpl, withTrack);
+    return this._chain(() => new Promise((resolve, reject) => {
+      this._onReplaceTrackSender = sender;
+      this._onReplaceTrackWithTrack = withTrack;
+      this._onReplaceTrackSuccess = resolve;
+      this._onReplaceTrackFailure = reject;
+      this._impl.replaceTrack(sender.track, withTrack);
+    }));
+  }
+
+  _setParameters({ track }, parameters) {
+    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
+      return;
+    }
+    // validate parameters input
+    var encodings = parameters.encodings || [];
+
+    encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
+      if (scaleResolutionDownBy < 1.0) {
+        throw new this._win.RangeError("scaleResolutionDownBy must be >= 1.0");
+      }
+      if (!rid && encodings.length > 1) {
+        throw new this._win.DOMException("Missing rid", "TypeError");
+      }
+      if (uniqueRids[rid]) {
+        throw new this._win.DOMException("Duplicate rid", "TypeError");
+      }
+      uniqueRids[rid] = true;
+      return uniqueRids;
+    }, {});
+
+    this._impl.setParameters(track, parameters);
+  }
+
+  _getParameters({ track }) {
+    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
+      return null;
+    }
+    return this._impl.getParameters(track);
   }
 
   close() {
     if (this._closed) {
       return;
     }
     this._closed = true;
+    this._inClose = true;
     this.changeIceConnectionState("closed");
     this._localIdp.close();
     this._remoteIdp.close();
     this._impl.close();
-    this._suppressEvents = true;
-    delete this._pc;
-    delete this._observer;
+    this._inClose = false;
   }
 
   getLocalStreams() {
     this._checkClosed();
-    let localStreams = new Set();
-    this._transceivers.forEach(transceiver => {
-      transceiver.sender.getStreams().forEach(stream => {
-        localStreams.add(stream);
-      });
-    });
-    return [...localStreams.values()];
+    return this._impl.getLocalStreams();
   }
 
   getRemoteStreams() {
     this._checkClosed();
-    return [...this._receiveStreams.values()];
+    return this._impl.getRemoteStreams();
   }
 
   getSenders() {
-    return this.getTransceivers().map(transceiver => transceiver.sender);
+    return this._senders;
   }
 
   getReceivers() {
-    return this.getTransceivers().map(transceiver => transceiver.receiver);
+    return this._receivers;
   }
 
   mozAddRIDExtension(receiver, extensionId) {
     this._impl.addRIDExtension(receiver.track, extensionId);
   }
 
   mozAddRIDFilter(receiver, rid) {
     this._impl.addRIDFilter(receiver.track, rid);
@@ -1338,20 +1185,16 @@ class RTCPeerConnection {
   mozEnablePacketDump(level, type, sending) {
     this._impl.enablePacketDump(level, type, sending);
   }
 
   mozDisablePacketDump(level, type, sending) {
     this._impl.disablePacketDump(level, type, sending);
   }
 
-  getTransceivers() {
-    return this._transceivers;
-  }
-
   get localDescription() {
     this._checkClosed();
     let sdp = this._impl.localDescription;
     if (sdp.length == 0) {
       return null;
     }
     return new this._win.RTCSessionDescription({ type: this._localType, sdp });
   }
@@ -1479,27 +1322,19 @@ class RTCPeerConnection {
     if (maxPacketLifeTime) {
       type = Ci.IPeerConnection.kDataChannelPartialReliableTimed;
     } else if (maxRetransmits) {
       type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit;
     } else {
       type = Ci.IPeerConnection.kDataChannelReliable;
     }
     // Synchronous since it doesn't block.
-    let dataChannel =
-      this._impl.createDataChannel(label, protocol, type, ordered,
-                                   maxPacketLifeTime, maxRetransmits,
-                                   negotiated, id);
-
-    // Spec says to only do this if this is the first DataChannel created,
-    // but the c++ code that does the "is negotiation needed" checking will
-    // only ever return true on the first one.
-    this.updateNegotiationNeeded();
-
-    return dataChannel;
+    return this._impl.createDataChannel(label, protocol, type, ordered,
+                                        maxPacketLifeTime, maxRetransmits,
+                                        negotiated, id);
   }
 }
 setupPrototype(RTCPeerConnection, {
   classID: PC_CID,
   contractID: PC_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer]),
   _actions: {
@@ -1555,26 +1390,20 @@ class PeerConnectionObserver {
     this._dompc._onCreateAnswerSuccess(sdp);
   }
 
   onCreateAnswerError(code, message) {
     this._dompc._onCreateAnswerFailure(this.newError(message, code));
   }
 
   onSetLocalDescriptionSuccess() {
-    this._dompc._syncTransceivers();
-    this._negotiationNeeded = false;
-    this._dompc.updateNegotiationNeeded();
     this._dompc._onSetLocalDescriptionSuccess();
   }
 
   onSetRemoteDescriptionSuccess() {
-    this._dompc._syncTransceivers();
-    this._negotiationNeeded = false;
-    this._dompc.updateNegotiationNeeded();
     this._dompc._onSetRemoteDescriptionSuccess();
   }
 
   onSetLocalDescriptionError(code, message) {
     this._localType = null;
     this._dompc._onSetLocalDescriptionFailure(this.newError(message, code));
   }
 
@@ -1601,16 +1430,20 @@ class PeerConnectionObserver {
     } else {
       candidate = null;
 
     }
     this.dispatchEvent(new win.RTCPeerConnectionIceEvent("icecandidate",
                                                          { candidate }));
   }
 
+  onNegotiationNeeded() {
+    this.dispatchEvent(new this._win.Event("negotiationneeded"));
+  }
+
   // This method is primarily responsible for updating iceConnectionState.
   // This state is defined in the WebRTC specification as follows:
   //
   // iceConnectionState:
   // -------------------
   //   new           Any of the RTCIceTransports are in the new state and none
   //                 of them are in the checking, failed or disconnected state.
   //
@@ -1720,85 +1553,81 @@ class PeerConnectionObserver {
                               pc._onGetStatsIsLegacy);
     pc._onGetStatsSuccess(webidlobj);
   }
 
   onGetStatsError(code, message) {
     this._dompc._onGetStatsFailure(this.newError(message, code));
   }
 
+  onAddStream(stream) {
+    let ev = new this._dompc._win.MediaStreamEvent("addstream", { stream });
+    this.dispatchEvent(ev);
+  }
+
   onRemoveStream(stream) {
     this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream",
                                                              { stream }));
   }
 
-  _getTransceiverWithRecvTrack(webrtcTrackId) {
-    return this._dompc.getTransceivers().find(
-        transceiver => transceiver.remoteTrackId == webrtcTrackId);
-  }
-
-  onTrack(webrtcTrackId, streamIds) {
+  onAddTrack(track, streams) {
     let pc = this._dompc;
-    let matchingTransceiver = this._getTransceiverWithRecvTrack(webrtcTrackId);
-
-    // Get or create MediaStreams, and add the new track to them.
-    let streams = streamIds.map(id => this._dompc._getOrCreateStream(id));
-
-    streams.forEach(stream => {
-      stream.addTrack(matchingTransceiver.receiver.track);
-      // Adding tracks from JS does not result in the stream getting
-      // onaddtrack, so we need to do that here. The mediacapture spec says
-      // this needs to be queued, also.
-      pc._queueTaskWithClosedCheck(() => {
-        stream.dispatchEvent(
-            new pc._win.MediaStreamTrackEvent(
-              "addtrack", { track: matchingTransceiver.receiver.track }));
-      });
-    });
-
-
-    let ev = new pc._win.RTCTrackEvent("track", {
-      receiver: matchingTransceiver.receiver,
-      track: matchingTransceiver.receiver.track,
-      streams,
-      transceiver: matchingTransceiver });
+    let receiver = pc._win.RTCRtpReceiver._create(pc._win,
+                                                  new RTCRtpReceiver(pc,
+                                                                     track));
+    pc._receivers.push(receiver);
+    let ev = new pc._win.RTCTrackEvent("track", { receiver, track, streams });
     this.dispatchEvent(ev);
 
     // Fire legacy event as well for a little bit.
-    ev = new pc._win.MediaStreamTrackEvent("addtrack",
-        { track: matchingTransceiver.receiver.track });
+    ev = new pc._win.MediaStreamTrackEvent("addtrack", { track });
     this.dispatchEvent(ev);
   }
 
-  onTransceiverNeeded(kind, transceiverImpl) {
-    this._dompc._onTransceiverNeeded(kind, transceiverImpl);
+  onRemoveTrack(track) {
+    let pc = this._dompc;
+    let i = pc._receivers.findIndex(receiver => receiver.track == track);
+    if (i >= 0) {
+      pc._receivers.splice(i, 1);
+    }
+  }
+
+  onReplaceTrackSuccess() {
+    var pc = this._dompc;
+    pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack;
+    pc._onReplaceTrackWithTrack = null;
+    pc._onReplaceTrackSender = null;
+    pc._onReplaceTrackSuccess();
+  }
+
+  onReplaceTrackError(code, message) {
+    var pc = this._dompc;
+    pc._onReplaceTrackWithTrack = null;
+    pc._onReplaceTrackSender = null;
+    pc._onReplaceTrackFailure(this.newError(message, code));
   }
 
   notifyDataChannel(channel) {
     this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
                                                                 { channel }));
   }
 
-  onDTMFToneChange(track, tone) {
+  onDTMFToneChange(trackId, tone) {
     var pc = this._dompc;
-    var sender = pc.getSenders().find(sender => sender.track == track);
+    var sender = pc._senders.find(({track}) => track.id == trackId);
     sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
                                                                  { tone }));
   }
 
   onPacket(level, type, sending, packet) {
     var pc = this._dompc;
     if (pc._onPacket) {
       pc._onPacket(level, type, sending, packet);
     }
   }
-
-  syncTransceivers() {
-    this._dompc._syncTransceivers();
-  }
 }
 setupPrototype(PeerConnectionObserver, {
   classID: PC_OBS_CID,
   contractID: PC_OBS_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer])
 });
 
@@ -1833,344 +1662,87 @@ class RTCDTMFSender {
   }
 
   set ontonechange(handler) {
     this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
   }
 
   insertDTMF(tones, duration, interToneGap) {
     this._sender._pc._checkClosed();
-    this._sender._transceiver.insertDTMF(tones, duration, interToneGap);
+
+    if (this._sender._pc._senders.indexOf(this._sender.__DOM_IMPL__) == -1) {
+      throw new this._sender._pc._win.DOMException("RTCRtpSender is stopped",
+                                                   "InvalidStateError");
+    }
+
+    duration = Math.max(40, Math.min(duration, 6000));
+    if (interToneGap < 30) interToneGap = 30;
+
+    tones = tones.toUpperCase();
+
+    if (tones.match(/[^0-9A-D#*,]/)) {
+      throw new this._sender._pc._win.DOMException("Invalid DTMF characters",
+                                                   "InvalidCharacterError");
+    }
+
+    this._sender._pc._insertDTMF(this._sender, tones, duration, interToneGap);
   }
 }
 setupPrototype(RTCDTMFSender, {
   classID: PC_DTMF_SENDER_CID,
   contractID: PC_DTMF_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpSender {
-  constructor(pc, transceiverImpl, transceiver, track, streams) {
-    let dtmf = pc._win.RTCDTMFSender._create(
-        pc._win, new RTCDTMFSender(this));
-
-    Object.assign(this, {
-      _pc: pc,
-      _transceiverImpl: transceiverImpl,
-      _transceiver: transceiver,
-      track,
-      _streams: streams,
-      dtmf });
+  constructor(pc, track, stream) {
+    let dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
+    Object.assign(this, { _pc: pc, track, _stream: stream, dtmf });
   }
 
   replaceTrack(withTrack) {
-    // async functions in here return a chrome promise, which is not something
-    // content can use. This wraps that promise in something content can use.
-    return this._pc._win.Promise.resolve(this._replaceTrack(withTrack));
-  }
-
-  async _replaceTrack(withTrack) {
-    this._pc._checkClosed();
-
-    if (this._transceiver.stopped) {
-      throw new this._pc._win.DOMException(
-          "Cannot call replaceTrack when transceiver is stopped",
-          "InvalidStateError");
-    }
-
-    if (withTrack && (withTrack.kind != this._transceiver.getKind())) {
-      throw new this._pc._win.DOMException(
-          "Cannot replaceTrack with a different kind!",
-          "TypeError");
-    }
-
-    // Updates the track on the MediaPipeline; this is needed whether or not
-    // we've associated this transceiver, the spec language notwithstanding.
-    // Synchronous, and will throw on failure.
-    this._pc._replaceTrack(this._transceiverImpl, withTrack);
-
-    let setTrack = () => {
-      this.track = withTrack;
-      this._transceiver.sync();
-    }
-
-    // Spec is a little weird here; we only queue if the transceiver was
-    // associated, otherwise we update the track synchronously.
-    if (this._transceiver.mid == null) {
-      setTrack();
-    } else {
-      // We're supposed to queue a task if the transceiver is associated
-      await this._pc._queueTaskWithClosedCheck(setTrack);
-    }
+    return this._pc._async(() => this._pc._replaceTrack(this, withTrack));
   }
 
   setParameters(parameters) {
-    return this._pc._win.Promise.resolve(this._setParameters(parameters));
-  }
-
-  async _setParameters(parameters) {
-    this._pc._checkClosed();
-
-    if (this._transceiver.stopped) {
-      throw new this._pc._win.DOMException(
-          "This sender's transceiver is stopped", "InvalidStateError");
-    }
-
-    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
-      return;
-    }
-
-    parameters.encodings = parameters.encodings || [];
-
-    parameters.encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
-      if (scaleResolutionDownBy < 1.0) {
-        throw new this._pc._win.RangeError("scaleResolutionDownBy must be >= 1.0");
-      }
-      if (!rid && parameters.encodings.length > 1) {
-        throw new this._pc._win.DOMException("Missing rid", "TypeError");
-      }
-      if (uniqueRids[rid]) {
-        throw new this._pc._win.DOMException("Duplicate rid", "TypeError");
-      }
-      uniqueRids[rid] = true;
-      return uniqueRids;
-    }, {});
-
-    // TODO(bug 1401592): transaction ids, timing changes
-
-    await this._pc._queueTaskWithClosedCheck(() => {
-      this.parameters = parameters;
-      this._transceiver.sync();
-    });
+    return this._pc._win.Promise.resolve()
+      .then(() => this._pc._setParameters(this, parameters));
   }
 
   getParameters() {
-    // TODO(bug 1401592): transaction ids
-
-    // All the other stuff that the spec says to update is handled when
-    // transceivers are synced.
-    return this.parameters;
-  }
-
-  setStreams(streams) {
-    this._streams = streams;
-  }
-
-  getStreams() {
-    return this._streams;
-  }
-
-  setTrack(track) {
-    this.track = track;
+    return this._pc._getParameters(this);
   }
 
   getStats() {
     return this._pc._async(
       async () => this._pc._getStats(this.track));
   }
-
-  checkWasCreatedByPc(pc) {
-    if (pc != this._pc.__DOM_IMPL__) {
-      throw new this._pc._win.DOMException(
-          "This sender was not created by this PeerConnection",
-          "InvalidAccessError");
-    }
-  }
 }
 setupPrototype(RTCRtpSender, {
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpReceiver {
-  constructor(pc, transceiverImpl) {
-    // We do not set the track here; that is done when _transceiverImpl is set
-    Object.assign(this,
-        {
-          _pc: pc,
-          _transceiverImpl: transceiverImpl,
-          track: transceiverImpl.getReceiveTrack()
-        });
+  constructor(pc, track) {
+    Object.assign(this, { _pc: pc, track });
   }
 
-  // TODO(bug 1401983): Create a getStats binding on TransceiverImpl, and use
-  // that here.
   getStats() {
     return this._pc._async(
       async () => this._pc.getStats(this.track));
   }
 }
 setupPrototype(RTCRtpReceiver, {
   classID: PC_RECEIVER_CID,
   contractID: PC_RECEIVER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
-class RTCRtpTransceiver {
-  constructor(pc, transceiverImpl, init, kind, sendTrack) {
-    let receiver = pc._win.RTCRtpReceiver._create(
-        pc._win, new RTCRtpReceiver(pc, transceiverImpl, kind));
-    let streams = (init && init.streams) || [];
-    let sender = pc._win.RTCRtpSender._create(
-        pc._win, new RTCRtpSender(pc, transceiverImpl, this, sendTrack, streams));
-
-    let direction = (init && init.direction) || "sendrecv";
-    Object.assign(this,
-        {
-          _pc: pc,
-          mid: null,
-          sender,
-          receiver,
-          stopped: false,
-          _direction: direction,
-          currentDirection: null,
-          remoteTrackId: null,
-          addTrackMagic: false,
-          _hasBeenUsedToSend: false,
-          // the receiver starts out without a track, so record this here
-          _kind: kind,
-          _transceiverImpl: transceiverImpl
-        });
-  }
-
-  set direction(direction) {
-    this._pc._checkClosed();
-
-    if (this.stopped) {
-      throw new this._pc._win.DOMException("Transceiver is stopped!",
-                                           "InvalidStateError");
-    }
-
-    if (this._direction == direction) {
-      return;
-    }
-
-    this._direction = direction;
-    this.sync();
-    this._pc.updateNegotiationNeeded();
-  }
-
-  get direction() {
-    return this._direction;
-  }
-
-  setDirectionInternal(direction) {
-    this._direction = direction;
-  }
-
-  stop() {
-    if (this.stopped) {
-      return;
-    }
-
-    this._pc._checkClosed();
-
-    this.setStopped();
-    this.sync();
-    this._pc.updateNegotiationNeeded();
-  }
-
-  setStopped() {
-    this.stopped = true;
-    this.currentDirection = null;
-  }
-
-  remove() {
-    var index = this._pc._transceivers.indexOf(this.__DOM_IMPL__);
-    if (index != -1) {
-      this._pc._transceivers.splice(index, 1);
-    }
-  }
-
-  getKind() {
-    return this._kind;
-  }
-
-  hasBeenUsedToSend() {
-    return this._hasBeenUsedToSend;
-  }
-
-  setRemoteTrackId(webrtcTrackId) {
-    this.remoteTrackId = webrtcTrackId;
-  }
-
-  setAddTrackMagic() {
-    this.addTrackMagic = true;
-  }
-
-  sync() {
-    if (this._syncing) {
-      throw new DOMException("Reentrant sync! This is a bug!", "InternalError");
-    }
-    this._syncing = true;
-    this._transceiverImpl.syncWithJS(this.__DOM_IMPL__);
-    this._syncing = false;
-  }
-
-  // Used by _transceiverImpl.syncWithJS, don't call sync again!
-  setCurrentDirection(direction) {
-    if (this.stopped) {
-      return;
-    }
-
-    switch (direction) {
-      case "sendrecv":
-      case "sendonly":
-        this._hasBeenUsedToSend = true;
-        break;
-      default:
-    }
-
-    this.currentDirection = direction;
-  }
-
-  // Used by _transceiverImpl.syncWithJS, don't call sync again!
-  setMid(mid) {
-    this.mid = mid;
-  }
-
-  // Used by _transceiverImpl.syncWithJS, don't call sync again!
-  unsetMid() {
-    this.mid = null;
-  }
-
-  insertDTMF(tones, duration, interToneGap) {
-    if (this.stopped) {
-      throw new this._pc._win.DOMException("Transceiver is stopped!",
-                                           "InvalidStateError");
-    }
-
-    if (!this.sender.track) {
-      throw new this._pc._win.DOMException("RTCRtpSender has no track",
-                                           "InvalidStateError");
-    }
-
-    duration = Math.max(40, Math.min(duration, 6000));
-    if (interToneGap < 30) interToneGap = 30;
-
-    tones = tones.toUpperCase();
-
-    if (tones.match(/[^0-9A-D#*,]/)) {
-      throw new this._pc._win.DOMException("Invalid DTMF characters",
-                                           "InvalidCharacterError");
-    }
-
-    // TODO (bug 1401983): Move this API to TransceiverImpl so we don't need the
-    // extra hops through RTCPeerConnection and PeerConnectionImpl
-    this._pc._insertDTMF(this._transceiverImpl, tones, duration, interToneGap);
-  }
-}
-
-setupPrototype(RTCRtpTransceiver, {
-  classID: PC_TRANSCEIVER_CID,
-  contractID: PC_TRANSCEIVER_CONTRACT,
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
-});
-
 class CreateOfferRequest {
   constructor(windowID, innerWindowID, callID, isSecure) {
     Object.assign(this, { windowID, innerWindowID, callID, isSecure });
   }
 }
 setupPrototype(CreateOfferRequest, {
   classID: PC_COREQUEST_CID,
   contractID: PC_COREQUEST_CONTRACT,
@@ -2181,13 +1753,12 @@ this.NSGetFactory = XPCOMUtils.generateN
   [GlobalPCList,
    RTCDTMFSender,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCPeerConnectionStatic,
    RTCRtpReceiver,
    RTCRtpSender,
-   RTCRtpTransceiver,
    RTCStatsReport,
    PeerConnectionObserver,
    CreateOfferRequest]
 );
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -314,17 +314,16 @@ function setupEnvironment() {
 
   var defaultMochitestPrefs = {
     'set': [
       ['media.peerconnection.enabled', true],
       ['media.peerconnection.identity.enabled', true],
       ['media.peerconnection.identity.timeout', 120000],
       ['media.peerconnection.ice.stun_client_maximum_transmits', 14],
       ['media.peerconnection.ice.trickle_grace_period', 30000],
-      ['media.peerconnection.remoteTrackId.enabled', true],
       ['media.navigator.permission.disabled', true],
       ['media.navigator.streams.fake', FAKE_ENABLED],
       ['media.getusermedia.screensharing.enabled', true],
       ['media.getusermedia.screensharing.allowed_domains', "mochi.test"],
       ['media.getusermedia.audiocapture.enabled', true],
       ['media.recorder.audio_node.enabled', true]
     ]
   };
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -93,17 +93,16 @@ skip-if = toolkit == 'android' # no scre
 [test_getUserMedia_trackCloneCleanup.html]
 [test_getUserMedia_trackEnded.html]
 [test_getUserMedia_peerIdentity.html]
 [test_peerConnection_addIceCandidate.html]
 [test_peerConnection_addtrack_removetrack_events.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_audioCodecs.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
-[test_peerConnection_transceivers.html]
 [test_peerConnection_basicAudio.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_checkPacketDumpHook.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicAudioNATSrflx.html]
 skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
 [test_peerConnection_basicAudioNATRelay.html]
 skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
@@ -122,18 +121,16 @@ skip-if = (android_version == '18') # an
 [test_peerConnection_basicAudioVideoCombined.html]
 skip-if = toolkit == 'android'  # Bug 1189784
 [test_peerConnection_basicAudioVideoNoBundle.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicAudioVideoNoRtcpMux.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
-[test_peerConnection_basicAudioVideoTransceivers.html]
-skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicVideo.html]
 skip-if = (android_version == '18' && debug) # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicScreenshare.html]
 # frequent timeouts/crashes on e10s (bug 1048455)
 skip-if = toolkit == 'android' # no screenshare on android
 [test_peerConnection_basicWindowshare.html]
 # frequent timeouts/crashes on e10s (bug 1048455)
 skip-if = toolkit == 'android' # no screenshare on android
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -145,17 +145,17 @@ PeerConnectionTest.prototype.closePC = f
       Promise.all(pc._pc.getReceivers()
         .filter(receiver => receiver.track.readyState == "live")
         .map(receiver => {
           info("Waiting for track " + receiver.track.id + " (" +
                receiver.track.kind + ") to end.");
           return haveEvent(receiver.track, "ended", wait(50000))
             .then(event => {
               is(event.target, receiver.track, "Event target should be the correct track");
-              info(pc + " ended fired for track " + receiver.track.id);
+              info("ended fired for track " + receiver.track.id);
             }, e => e ? Promise.reject(e)
                       : ok(false, "ended never fired for track " +
                                     receiver.track.id));
         }))
     ]);
     pc.close();
     return promise;
   };
@@ -756,20 +756,18 @@ function PeerConnectionWrapper(label, co
   this._local_ice_candidates = [];
   this._remote_ice_candidates = [];
   this.localRequiresTrickleIce = false;
   this.remoteRequiresTrickleIce = false;
   this.localMediaElements = [];
   this.remoteMediaElements = [];
   this.audioElementsOnly = false;
 
-  this._sendStreams = [];
-
   this.expectedLocalTrackInfoById = {};
-  this.expectedSignalledTrackInfoById = {};
+  this.expectedRemoteTrackInfoById = {};
   this.observedRemoteTrackInfoById = {};
 
   this.disableRtpCountChecking = false;
 
   this.iceConnectedResolve;
   this.iceConnectedReject;
   this.iceConnected = new Promise((resolve, reject) => {
     this.iceConnectedResolve = resolve;
@@ -872,70 +870,36 @@ PeerConnectionWrapper.prototype = {
   get iceConnectionState() {
     return this._pc.iceConnectionState;
   },
 
   setIdentityProvider: function(provider, protocol, identity) {
     this._pc.setIdentityProvider(provider, protocol, identity);
   },
 
-  elementPrefix : direction =>
-  {
-    return [this.label, direction].join('_');
-  },
-
-  getMediaElementForTrack : function (track, direction)
-  {
-    var prefix = this.elementPrefix(direction);
-    return getMediaElementForTrack(track, prefix);
-  },
+  ensureMediaElement : function(track, direction) {
+    const idPrefix = [this.label, direction].join('_');
+    var element = getMediaElementForTrack(track, idPrefix);
 
-  createMediaElementForTrack : function(track, direction)
-  {
-    var prefix = this.elementPrefix(direction);
-    return createMediaElementForTrack(track, prefix);
-  },
-
-  ensureMediaElement : function(track, direction) {
-    var prefix = this.elementPrefix(direction);
-    var element = this.getMediaElementForTrack(track, direction);
     if (!element) {
-      element = this.createMediaElementForTrack(track, direction);
+      element = createMediaElementForTrack(track, idPrefix);
       if (direction == "local") {
         this.localMediaElements.push(element);
       } else if (direction == "remote") {
         this.remoteMediaElements.push(element);
       }
     }
 
     // We do this regardless, because sometimes we end up with a new stream with
     // an old id (ie; the rollback tests cause the same stream to be added
     // twice)
     element.srcObject = new MediaStream([track]);
     element.play();
   },
 
-  addSendStream : function(stream)
-  {
-    // The PeerConnection will not necessarily know about this stream
-    // automatically, because replaceTrack is not told about any streams the
-    // new track might be associated with. Only content really knows.
-    this._sendStreams.push(stream);
-  },
-
-  getStreamForSendTrack : function(track)
-  {
-    return this._sendStreams.find(str => str.getTrackById(track.id));
-  },
-
-  getStreamForRecvTrack : function(track)
-  {
-    return this._pc.getRemoteStreams().find(s => !!s.getTrackById(track.id));
-  },
-
   /**
    * Attaches a local track to this RTCPeerConnection using
    * RTCPeerConnection.addTrack().
    *
    * Also creates a media element playing a MediaStream containing all
    * tracks that have been added to `stream` using `attachLocalTrack()`.
    *
    * @param {MediaStreamTrack} track
@@ -952,147 +916,110 @@ PeerConnectionWrapper.prototype = {
 
     ok(track.id, "track has id");
     ok(track.kind, "track has kind");
     ok(stream.id, "stream has id");
     this.expectedLocalTrackInfoById[track.id] = {
       type: track.kind,
       streamId: stream.id,
     };
-    this.expectedSignalledTrackInfoById[track.id] =
-      this.expectedLocalTrackInfoById[track.id];
-
-    this.addSendStream(stream);
 
     // This will create one media element per track, which might not be how
     // we set up things with the RTCPeerConnection. It's the only way
     // we can ensure all sent tracks are flowing however.
     this.ensureMediaElement(track, "local");
 
     return this.observedNegotiationNeeded;
   },
 
   /**
    * Callback when we get local media. Also an appropriate HTML media element
    * will be created and added to the content node.
    *
    * @param {MediaStream} stream
    *        Media stream to handle
    */
-  attachLocalStream : function(stream, useAddTransceiver) {
+  attachLocalStream : function(stream) {
     info("Got local media stream: (" + stream.id + ")");
 
     this.expectNegotiationNeeded();
-    if (useAddTransceiver) {
-      info("Using addTransceiver (on PC).");
-      stream.getTracks().forEach(track => {
-        var transceiver = this._pc.addTransceiver(track, {streams: [stream]});
-        is(transceiver.sender.track, track, "addTransceiver returns sender");
-      });
-    }
     // In order to test both the addStream and addTrack APIs, we do half one
     // way, half the other, at random.
-    else if (Math.random() < 0.5) {
+    if (Math.random() < 0.5) {
       info("Using addStream.");
       this._pc.addStream(stream);
       ok(this._pc.getSenders().find(sender => sender.track == stream.getTracks()[0]),
          "addStream returns sender");
     } else {
       info("Using addTrack (on PC).");
       stream.getTracks().forEach(track => {
         var sender = this._pc.addTrack(track, stream);
         is(sender.track, track, "addTrack returns sender");
       });
     }
 
-    this.addSendStream(stream);
-
     stream.getTracks().forEach(track => {
       ok(track.id, "track has id");
       ok(track.kind, "track has kind");
       this.expectedLocalTrackInfoById[track.id] = {
           type: track.kind,
           streamId: stream.id
         };
-      this.expectedSignalledTrackInfoById[track.id] =
-        this.expectedLocalTrackInfoById[track.id];
       this.ensureMediaElement(track, "local");
     });
-
-    return this.observedNegotiationNeeded;
   },
 
   removeSender : function(index) {
     var sender = this._pc.getSenders()[index];
     delete this.expectedLocalTrackInfoById[sender.track.id];
     this.expectNegotiationNeeded();
     this._pc.removeTrack(sender);
     return this.observedNegotiationNeeded;
   },
 
-  senderReplaceTrack : function(sender, withTrack, stream) {
+  senderReplaceTrack : function(index, withTrack, withStreamId) {
+    var sender = this._pc.getSenders()[index];
     delete this.expectedLocalTrackInfoById[sender.track.id];
     this.expectedLocalTrackInfoById[withTrack.id] = {
         type: withTrack.kind,
-        streamId: stream.id
+        streamId: withStreamId
       };
-    this.addSendStream(stream);
-    this.ensureMediaElement(withTrack, 'local');
     return sender.replaceTrack(withTrack);
   },
 
-  getUserMedia : async function(constraints) {
-    var stream = await getUserMedia(constraints);
-    if (constraints.audio) {
-      stream.getAudioTracks().forEach(track => {
-        info(this + " gUM local stream " + stream.id +
-          " with audio track " + track.id);
-      });
-    }
-    if (constraints.video) {
-      stream.getVideoTracks().forEach(track => {
-        info(this + " gUM local stream " + stream.id +
-          " with video track " + track.id);
-      });
-    }
-    return stream;
-  },
-
   /**
    * 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) {
       info("Skipping GUM: no UserMedia requested");
       return Promise.resolve();
     }
 
     info("Get " + constraintsList.length + " local streams");
-    return Promise.all(
-      constraintsList.map(constraints => this.getUserMedia(constraints))
-    );
-  },
-
-  getAllUserMediaAndAddStreams : async function(constraintsList) {
-    var streams = await this.getAllUserMedia(constraintsList);
-    if (!streams) {
-      return;
-    }
-    return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
-  },
-
-  getAllUserMediaAndAddTransceivers : async function(constraintsList) {
-    var streams = await this.getAllUserMedia(constraintsList);
-    if (!streams) {
-      return;
-    }
-    return Promise.all(streams.map(stream => this.attachLocalStream(stream, true)));
+    return Promise.all(constraintsList.map(constraints => {
+      return getUserMedia(constraints).then(stream => {
+        if (constraints.audio) {
+          stream.getAudioTracks().forEach(track => {
+            info(this + " gUM local stream " + stream.id +
+              " with audio track " + track.id);
+          });
+        }
+        if (constraints.video) {
+          stream.getVideoTracks().forEach(track => {
+            info(this + " gUM local stream " + stream.id +
+              " with video track " + track.id);
+          });
+        }
+        return this.attachLocalStream(stream);
+      });
+    }));
   },
 
   /**
    * Create a new data channel instance.  Also creates a promise called
    * `this.nextDataChannel` that resolves when the next data channel arrives.
    */
   expectDataChannel: function(message) {
     this.nextDataChannel = new Promise(resolve => {
@@ -1232,65 +1159,44 @@ PeerConnectionWrapper.prototype = {
     });
   },
 
   /**
    * Checks whether a given track is expected, has not been observed yet, and
    * is of the correct type. Then, moves the track from
    * |expectedTrackInfoById| to |observedTrackInfoById|.
    */
-  checkTrackIsExpected : function(trackId,
-                                  kind,
+  checkTrackIsExpected : function(track,
                                   expectedTrackInfoById,
                                   observedTrackInfoById) {
-    ok(expectedTrackInfoById[trackId], "track id " + trackId + " was expected");
-    ok(!observedTrackInfoById[trackId], "track id " + trackId + " was not yet observed");
-    var observedKind = kind;
-    var expectedKind = expectedTrackInfoById[trackId].type;
+    ok(expectedTrackInfoById[track.id], "track id " + track.id + " was expected");
+    ok(!observedTrackInfoById[track.id], "track id " + track.id + " was not yet observed");
+    var observedKind = track.kind;
+    var expectedKind = expectedTrackInfoById[track.id].type;
     is(observedKind, expectedKind,
-        "track id " + trackId + " was of kind " +
+        "track id " + track.id + " was of kind " +
         observedKind + ", which matches " + expectedKind);
-    observedTrackInfoById[trackId] = expectedTrackInfoById[trackId];
+    observedTrackInfoById[track.id] = expectedTrackInfoById[track.id];
   },
 
   isTrackOnPC: function(track) {
-    return !!this.getStreamForRecvTrack(track);
+    return this._pc.getRemoteStreams().some(s => !!s.getTrackById(track.id));
   },
 
   allExpectedTracksAreObserved: function(expected, observed) {
     return Object.keys(expected).every(trackId => observed[trackId]);
   },
 
-  getWebrtcTrackId: function(receiveTrack) {
-    let matchingTransceiver = this._pc.getTransceivers().find(
-        transceiver => transceiver.receiver.track == receiveTrack);
-    if (!matchingTransceiver) {
-      return null;
-    }
-
-    return matchingTransceiver.remoteTrackId;
-  },
-
   setupTrackEventHandler: function() {
     this._pc.addEventListener('track', event => {
-      info(this + ": 'ontrack' event fired for " + event.track.id +
-                  "(SDP msid is " + this.getWebrtcTrackId(event.track) +
-                  ")");
+      info(this + ": 'ontrack' event fired for " + JSON.stringify(event.track));
 
-      // TODO(bug 1403238): Checking for remote tracks needs to be completely
-      // reworked, because with the latest spec the identifiers aren't the same
-      // as they are on the other end. Ultimately, what we need to check is
-      // whether the _transceivers_ are in line with what is expected, and
-      // whether the callbacks are consistent with the transceivers.
-      let trackId = this.getWebrtcTrackId(event.track);
-      ok(!this.observedRemoteTrackInfoById[trackId],
-         "track id " + trackId + " was not yet observed");
-      this.observedRemoteTrackInfoById[trackId] = {
-        type: event.track.kind
-      };
+      this.checkTrackIsExpected(event.track,
+                                this.expectedRemoteTrackInfoById,
+                                this.observedRemoteTrackInfoById);
       ok(this.isTrackOnPC(event.track), "Found track " + event.track.id);
 
       this.ensureMediaElement(event.track, 'remote');
     });
   },
 
   /**
    * Either adds a given ICE candidate right away or stores it to be added
@@ -1413,47 +1319,53 @@ PeerConnectionWrapper.prototype = {
       candidateHandler(this.label, anEvent.candidate);
     };
   },
 
   checkLocalMediaTracks : function() {
     var observed = {};
     info(this + " Checking local tracks " + JSON.stringify(this.expectedLocalTrackInfoById));
     this._pc.getSenders().forEach(sender => {
-      if (sender.track) {
-        this.checkTrackIsExpected(sender.track.id,
-                                  sender.track.kind,
-                                  this.expectedLocalTrackInfoById,
-                                  observed);
-      }
+      this.checkTrackIsExpected(sender.track, this.expectedLocalTrackInfoById, observed);
     });
 
     Object.keys(this.expectedLocalTrackInfoById).forEach(
         id => ok(observed[id], this + " local id " + id + " was observed"));
   },
 
   /**
    * Checks that we are getting the media tracks we expect.
    */
   checkMediaTracks : function() {
     this.checkLocalMediaTracks();
+
+    info(this + " Checking remote tracks " +
+         JSON.stringify(this.expectedRemoteTrackInfoById));
+
+    ok(this.allExpectedTracksAreObserved(this.expectedRemoteTrackInfoById,
+                                         this.observedRemoteTrackInfoById),
+       "All expected tracks have been observed"
+       + "\nexpected: " + JSON.stringify(this.expectedRemoteTrackInfoById)
+       + "\nobserved: " + JSON.stringify(this.observedRemoteTrackInfoById));
   },
 
   checkMsids: function() {
     var checkSdpForMsids = (desc, expectedTrackInfo, side) => {
       Object.keys(expectedTrackInfo).forEach(trackId => {
         var streamId = expectedTrackInfo[trackId].streamId;
         ok(desc.sdp.match(new RegExp("a=msid:" + streamId + " " + trackId)),
            this + ": " + side + " SDP contains stream " + streamId +
            " and track " + trackId );
       });
     };
 
-    checkSdpForMsids(this.localDescription, this.expectedSignalledTrackInfoById,
+    checkSdpForMsids(this.localDescription, this.expectedLocalTrackInfoById,
                      "local");
+    checkSdpForMsids(this.remoteDescription, this.expectedRemoteTrackInfoById,
+                     "remote");
   },
 
   markRemoteTracksAsNegotiated: function() {
     Object.values(this.observedRemoteTrackInfoById).forEach(
         trackInfo => trackInfo.negotiated = true);
   },
 
   rollbackRemoteTracksIfNotNegotiated: function() {
@@ -1544,66 +1456,32 @@ PeerConnectionWrapper.prototype = {
         return stats;
       }
       await wait(retryInterval);
     }
     throw new Error("Timeout checking for stats for track " + track.id
                     + " after at least" + timeout + "ms");
   },
 
-  getExpectedActiveReceiveTracks : function() {
-    return this._pc.getTransceivers()
-      .filter(t => {
-        return !t.stopped &&
-               t.currentDirection &&
-               (t.currentDirection != "inactive") &&
-               (t.currentDirection != "sendonly");
-      })
-      .map(t => {
-        info("Found transceiver that should be receiving RTP: mid=" + t.mid +
-             " currentDirection=" + t.currentDirection + " kind=" +
-             t.receiver.track.kind + " track-id=" + t.receiver.track.id);
-        return t.receiver.track;
-      });
-  },
-
-  getExpectedSendTracks : function() {
-    return Object.keys(this.expectedLocalTrackInfoById)
-              .map(id => this.findSendTrackByWebrtcId(id));
-  },
-
-  findReceiveTrackByWebrtcId : function(webrtcId) {
-    return this._pc.getReceivers().map(receiver => receiver.track)
-              .find(track => this.getWebrtcTrackId(track) == webrtcId);
-  },
-
-  // Send tracks use the same identifiers that go in the signaling
-  findSendTrackByWebrtcId : function(webrtcId) {
-    return this._pc.getSenders().map(sender => sender.track)
-              .filter(track => track) // strip out null
-              .find(track => track.id == webrtcId);
-  },
-
   /**
    * Wait for presence of video flow on all media elements and rtp flow on
    * all sending and receiving track involved in this test.
    *
    * @returns {Promise}
    *        A promise that resolves when media flows for all elements and tracks
    */
   waitForMediaFlow : function() {
     return Promise.all([].concat(
       this.localMediaElements.map(element => this.waitForMediaElementFlow(element)),
-      this.remoteMediaElements.filter(elem =>
-          this.getExpectedActiveReceiveTracks()
-            .some(track => elem.srcObject.getTracks().some(t => t == track))
-        )
-        .map(elem => this.waitForMediaElementFlow(elem)),
-      this.getExpectedActiveReceiveTracks().map(track => this.waitForRtpFlow(track)),
-      this.getExpectedSendTracks().map(track => this.waitForRtpFlow(track))));
+      Object.keys(this.expectedRemoteTrackInfoById)
+          .map(id => this.remoteMediaElements
+              .find(e => e.srcObject.getTracks().some(t => t.id == id)))
+          .map(e => this.waitForMediaElementFlow(e)),
+      this._pc.getSenders().map(sender => this.waitForRtpFlow(sender.track)),
+      this._pc.getReceivers().map(receiver => this.waitForRtpFlow(receiver.track))));
   },
 
   async waitForSyncedRtcp() {
     // Ensures that RTCP is present
     let ensureSyncedRtcp = async () => {
       let report = await this._pc.getStats();
       for (let [k, v] of report) {
         if (v.type.endsWith("bound-rtp") && !v.remoteId) {
@@ -1639,100 +1517,69 @@ PeerConnectionWrapper.prototype = {
       await wait(waitPeriod);
     }
     throw Error("Waiting for synced RTCP timed out after at least "
                 + maxTime + "ms");
   },
 
   /**
    * Check that correct audio (typically a flat tone) is flowing to this
-   * PeerConnection for each transceiver that should be receiving. Uses
-   * WebAudio AnalyserNodes to compare input and output audio data in the
-   * frequency domain.
+   * PeerConnection. Uses WebAudio AnalyserNodes to compare input and output
+   * audio data in the frequency domain.
    *
    * @param {object} from
    *        A PeerConnectionWrapper whose audio RTPSender we use as source for
    *        the audio flow check.
    * @returns {Promise}
-   *        A promise that resolves when we're receiving the tone/s from |from|.
+   *        A promise that resolves when we're receiving the tone from |from|.
    */
   checkReceivingToneFrom : async function(audiocontext, from,
       cancel = wait(60000, new Error("Tone not detected"))) {
-    let localTransceivers = this._pc.getTransceivers()
-      .filter(t => t.mid)
-      .filter(t => t.receiver.track.kind == "audio")
-      .sort((t1, t2) => t1.mid < t2.mid);
-    let remoteTransceivers = from._pc.getTransceivers()
-      .filter(t => t.mid)
-      .filter(t => t.receiver.track.kind == "audio")
-      .sort((t1, t2) => t1.mid < t2.mid);
+    let inputElem = from.localMediaElements[0];
 
-    is(localTransceivers.length, remoteTransceivers.length,
-       "Same number of associated audio transceivers on remote and local.");
+    // As input we use the stream of |from|'s first available audio sender.
+    let inputSenderTracks = from._pc.getSenders().map(sn => sn.track);
+    let inputAudioStream = from._pc.getLocalStreams()
+      .find(s => inputSenderTracks.some(t => t.kind == "audio" && s.getTrackById(t.id)));
+    let inputAnalyser = new AudioStreamAnalyser(audiocontext, inputAudioStream);
 
-    for (let i = 0; i < localTransceivers.length; i++) {
-      is(localTransceivers[i].mid, remoteTransceivers[i].mid,
-         "Transceivers at index " + i + " have the same mid.");
+    // It would have been nice to have a working getReceivers() here, but until
+    // we do, let's use what remote streams we have.
+    let outputAudioStream = this._pc.getRemoteStreams()
+      .find(s => s.getAudioTracks().length > 0);
+    let outputAnalyser = new AudioStreamAnalyser(audiocontext, outputAudioStream);
 
-      if (!remoteTransceivers[i].sender.track) {
-        continue;
-      }
+    let error = null;
+    cancel.then(e => error = e);
 
-      if (remoteTransceivers[i].currentDirection == "recvonly" ||
-          remoteTransceivers[i].currentDirection == "inactive") {
-        continue;
+    let indexOfMax = data => 
+      data.reduce((max, val, i) => (val >= data[max]) ? i : max, 0);
+
+    await outputAnalyser.waitForAnalysisSuccess(() => {
+      if (error) {
+        throw error;
       }
 
-      let sendTrack = remoteTransceivers[i].sender.track;
-      let inputElem = from.getMediaElementForTrack(sendTrack, "local");
-      ok(inputElem,
-         "Remote wrapper should have a media element for track id " +
-         sendTrack.id);
-      let inputAudioStream = from.getStreamForSendTrack(sendTrack);
-      ok(inputAudioStream,
-         "Remote wrapper should have a stream for track id " + sendTrack.id);
-      let inputAnalyser =
-        new AudioStreamAnalyser(audiocontext, inputAudioStream);
-
-      let recvTrack = localTransceivers[i].receiver.track;
-      let outputAudioStream = this.getStreamForRecvTrack(recvTrack);
-      ok(outputAudioStream,
-         "Local wrapper should have a stream for track id " + recvTrack.id);
-      let outputAnalyser =
-        new AudioStreamAnalyser(audiocontext, outputAudioStream);
-
-      let error = null;
-      cancel.then(e => error = e);
-
-      let indexOfMax = data =>
-        data.reduce((max, val, i) => (val >= data[max]) ? i : max, 0);
+      let inputData = inputAnalyser.getByteFrequencyData();
+      let outputData = outputAnalyser.getByteFrequencyData();
 
-      await outputAnalyser.waitForAnalysisSuccess(() => {
-        if (error) {
-          throw error;
-        }
-
-        let inputData = inputAnalyser.getByteFrequencyData();
-        let outputData = outputAnalyser.getByteFrequencyData();
+      let inputMax = indexOfMax(inputData);
+      let outputMax = indexOfMax(outputData);
+      info(`Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},`
+        + ` output[${outputMax}] = ${outputData[outputMax]}`);
+      if (!inputData[inputMax] || !outputData[outputMax]) {
+        return false;
+      }
 
-        let inputMax = indexOfMax(inputData);
-        let outputMax = indexOfMax(outputData);
-        info(`Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},`
-          + ` output[${outputMax}] = ${outputData[outputMax]}`);
-        if (!inputData[inputMax] || !outputData[outputMax]) {
-          return false;
-        }
-
-        // When the input and output maxima are within reasonable distance (2% of
-        // total length, which means ~10 for length 512) from each other, we can
-        // be sure that the input tone has made it through the peer connection.
-        info(`input data length: ${inputData.length}`);
-        return Math.abs(inputMax - outputMax) < (inputData.length * 0.02);
-      });
-    }
+      // When the input and output maxima are within reasonable distance (2% of
+      // total length, which means ~10 for length 512) from each other, we can
+      // be sure that the input tone has made it through the peer connection.
+      info(`input data length: ${inputData.length}`);
+      return Math.abs(inputMax - outputMax) < (inputData.length * 0.02);
+    });
   },
 
   /**
    * Get stats from the "legacy" getStats callback interface
    */
   getStatsLegacy : function(selector, onSuccess, onFail) {
     let wrapper = stats => {
       info(this + ": Got legacy stats: " + JSON.stringify(stats));
@@ -1770,17 +1617,16 @@ PeerConnectionWrapper.prototype = {
    *        The stats to check from this PeerConnectionWrapper
    */
   checkStats : function(stats, twoMachines) {
     const isWinXP = navigator.userAgent.indexOf("Windows NT 5.1") != -1;
 
     // Use spec way of enumerating stats
     var counters = {};
     for (let [key, res] of stats) {
-      info("Checking stats for " + key + " : " + res);
       // validate stats
       ok(res.id == key, "Coherent stats id");
       var nowish = Date.now() + 1000;        // TODO: clock drift observed
       var minimum = this.whenCreated - 1000; // on Windows XP (Bug 979649)
       if (isWinXP) {
         todo(false, "Can't reliably test rtcp timestamps on WinXP (Bug 979649)");
 
       } else if (false) { // Bug 1325430 - timestamps aren't working properly in update 49
@@ -1804,27 +1650,21 @@ PeerConnectionWrapper.prototype = {
       if (res.isRemote) {
         continue;
       }
       counters[res.type] = (counters[res.type] || 0) + 1;
 
       switch (res.type) {
         case "inbound-rtp":
         case "outbound-rtp": {
-          // Inbound tracks won't have an ssrc if RTP is not flowing.
-          // (eg; negotiated inactive)
-          ok(res.ssrc || res.type == "inbound-rtp", "Outbound RTP stats has an ssrc.");
-
-          if (res.ssrc) {
-            // ssrc is a 32 bit number returned as a string by spec
-            ok(res.ssrc.length > 0, "Ssrc has length");
-            ok(res.ssrc.length < 11, "Ssrc not lengthy");
-            ok(!/[^0-9]/.test(res.ssrc), "Ssrc numeric");
-            ok(parseInt(res.ssrc) < Math.pow(2,32), "Ssrc within limits");
-          }
+          // ssrc is a 32 bit number returned as a string by spec
+          ok(res.ssrc.length > 0, "Ssrc has length");
+          ok(res.ssrc.length < 11, "Ssrc not lengthy");
+          ok(!/[^0-9]/.test(res.ssrc), "Ssrc numeric");
+          ok(parseInt(res.ssrc) < Math.pow(2,32), "Ssrc within limits");
 
           if (res.type == "outbound-rtp") {
             ok(res.packetsSent !== undefined, "Rtp packetsSent");
             // We assume minimum payload to be 1 byte (guess from RFC 3550)
             ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent");
           } else {
             ok(res.packetsReceived !== undefined, "Rtp packetsReceived");
             ok(res.bytesReceived >= res.packetsReceived, "Rtp bytesReceived");
@@ -1889,22 +1729,17 @@ PeerConnectionWrapper.prototype = {
       var res = stats[key];
       var type = legacyToSpecMapping[res.type] || res.type;
       if (!res.isRemote) {
         counters2[type] = (counters2[type] || 0) + 1;
       }
     }
     is(JSON.stringify(counters), JSON.stringify(counters2),
        "Spec and legacy variant of RTCStatsReport enumeration agree");
-    var nin = this._pc.getTransceivers()
-      .filter(t => {
-        return !t.stopped &&
-               (t.currentDirection != "inactive") &&
-               (t.currentDirection != "sendonly");
-      }).length;
+    var nin = Object.keys(this.expectedRemoteTrackInfoById).length;
     var nout = Object.keys(this.expectedLocalTrackInfoById).length;
     var ndata = this.dataChannels.length;
 
     // TODO(Bug 957145): Restore stronger inbound-rtp test once Bug 948249 is fixed
     //is((counters["inbound-rtp"] || 0), nin, "Have " + nin + " inbound-rtp stat(s)");
     ok((counters["inbound-rtp"] || 0) >= nin, "Have at least " + nin + " inbound-rtp stat(s) *");
 
     is(counters["outbound-rtp"] || 0, nout, "Have " + nout + " outbound-rtp stat(s)");
@@ -1970,46 +1805,49 @@ PeerConnectionWrapper.prototype = {
 
   /**
    * Compares amount of established ICE connection according to ICE candidate
    * pairs in the stats reporting with the expected amount of connection based
    * on the constraints.
    *
    * @param {object} stats
    *        The stats to check for ICE candidate pairs
+   * @param {object} counters
+   *        The counters for media and data tracks based on constraints
    * @param {object} testOptions
    *        The test options object from the PeerConnectionTest
    */
-  checkStatsIceConnections : function(stats, testOptions) {
+  checkStatsIceConnections : function(stats,
+      offerConstraintsList, offerOptions, testOptions) {
     var numIceConnections = 0;
     stats.forEach(stat => {
       if ((stat.type === "candidate-pair") && stat.selected) {
         numIceConnections += 1;
       }
     });
     info("ICE connections according to stats: " + numIceConnections);
     isnot(numIceConnections, 0, "Number of ICE connections according to stats is not zero");
     if (testOptions.bundle) {
       if (testOptions.rtcpmux) {
         is(numIceConnections, 1, "stats reports exactly 1 ICE connection");
       } else {
         is(numIceConnections, 2, "stats report exactly 2 ICE connections for media and RTCP");
       }
     } else {
-      var numAudioTransceivers =
-        this._pc.getTransceivers().filter((transceiver) => {
-          return (!transceiver.stopped) && transceiver.receiver.track.kind == "audio";
-        }).length;
+      // This code assumes that no media sections have been rejected due to
+      // codec mismatch or other unrecoverable negotiation failures.
+      var numAudioTracks =
+          sdputils.countTracksInConstraint('audio', offerConstraintsList) ||
+          ((offerOptions && offerOptions.offerToReceiveAudio) ? 1 : 0);
 
-      var numVideoTransceivers =
-        this._pc.getTransceivers().filter((transceiver) => {
-          return (!transceiver.stopped) && transceiver.receiver.track.kind == "video";
-        }).length;
+      var numVideoTracks =
+          sdputils.countTracksInConstraint('video', offerConstraintsList) ||
+          ((offerOptions && offerOptions.offerToReceiveVideo) ? 1 : 0);
 
-      var numExpectedTransports = numAudioTransceivers + numVideoTransceivers;
+      var numExpectedTransports = numAudioTracks + numVideoTracks;
       if (!testOptions.rtcpmux) {
         numExpectedTransports *= 2;
       }
 
       if (this.dataChannels.length) {
         ++numExpectedTransports;
       }
 
--- a/dom/media/tests/mochitest/templates.js
+++ b/dom/media/tests/mochitest/templates.js
@@ -78,17 +78,18 @@ function waitForAnIceCandidate(pc) {
   }).then(() => {
     ok(pc._local_ice_candidates.length > 0,
        pc + " received local trickle ICE candidates");
     isnot(pc._pc.iceGatheringState, GATH_NEW,
           pc + " ICE gathering state is not 'new'");
   });
 }
 
-function checkTrackStats(pc, track, outbound) {
+function checkTrackStats(pc, rtpSenderOrReceiver, outbound) {
+  var track = rtpSenderOrReceiver.track;
   var audio = (track.kind == "audio");
   var msg = pc + " stats " + (outbound ? "outbound " : "inbound ") +
       (audio ? "audio" : "video") + " rtp track id " + track.id;
   return pc.getStats(track).then(stats => {
     ok(pc.hasStat(stats, {
       type: outbound ? "outbound-rtp" : "inbound-rtp",
       isRemote: false,
       mediaType: audio ? "audio" : "video"
@@ -100,18 +101,18 @@ function checkTrackStats(pc, track, outb
     ok(!pc.hasStat(stats, {
       mediaType: audio ? "video" : "audio"
     }), msg + " - did not find extra stats with wrong media type");
   });
 }
 
 var checkAllTrackStats = pc => {
   return Promise.all([].concat(
-    pc.getExpectedActiveReceiveTracks().map(track => checkTrackStats(pc, track, false)),
-    pc.getExpectedSendTracks().map(track => checkTrackStats(pc, track, true))));
+    pc._pc.getSenders().map(sender => checkTrackStats(pc, sender, true)),
+    pc._pc.getReceivers().map(receiver => checkTrackStats(pc, receiver, false))));
 }
 
 // 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.testOptions.steeplechase) {
       test.setupSignalingClient();
@@ -177,21 +178,21 @@ var commandsPeerConnectionInitial = [
   function PC_REMOTE_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
     is(test.pcRemote._pc.canTrickleIceCandidates, null,
        "Remote trickle status should start out unknown");
   },
 ];
 
 var commandsGetUserMedia = [
   function PC_LOCAL_GUM(test) {
-    return test.pcLocal.getAllUserMediaAndAddStreams(test.pcLocal.constraints);
+    return test.pcLocal.getAllUserMedia(test.pcLocal.constraints);
   },
 
   function PC_REMOTE_GUM(test) {
-    return test.pcRemote.getAllUserMediaAndAddStreams(test.pcRemote.constraints);
+    return test.pcRemote.getAllUserMedia(test.pcRemote.constraints);
   },
 ];
 
 var commandsPeerConnectionOfferAnswer = [
   function PC_LOCAL_SETUP_ICE_HANDLER(test) {
     test.pcLocal.setupIceCandidateHandler(test);
   },
 
@@ -208,16 +209,42 @@ var commandsPeerConnectionOfferAnswer = 
 
   function PC_REMOTE_STEEPLECHASE_SIGNAL_EXPECTED_LOCAL_TRACKS(test) {
     if (test.testOptions.steeplechase) {
       send_message({"type": "remote_expected_tracks",
                     "expected_tracks": test.pcRemote.expectedLocalTrackInfoById});
     }
   },
 
+  function PC_LOCAL_GET_EXPECTED_REMOTE_TRACKS(test) {
+    if (test.testOptions.steeplechase) {
+      return test.getSignalingMessage("remote_expected_tracks").then(
+          message => {
+            test.pcLocal.expectedRemoteTrackInfoById = message.expected_tracks;
+          });
+    }
+
+    // Deep copy, as similar to steeplechase as possible
+    test.pcLocal.expectedRemoteTrackInfoById =
+      JSON.parse(JSON.stringify(test.pcRemote.expectedLocalTrackInfoById));
+  },
+
+  function PC_REMOTE_GET_EXPECTED_REMOTE_TRACKS(test) {
+    if (test.testOptions.steeplechase) {
+      return test.getSignalingMessage("local_expected_tracks").then(
+          message => {
+            test.pcRemote.expectedRemoteTrackInfoById = message.expected_tracks;
+          });
+    }
+
+    // Deep copy, as similar to steeplechase as possible
+    test.pcRemote.expectedRemoteTrackInfoById =
+      JSON.parse(JSON.stringify(test.pcLocal.expectedLocalTrackInfoById));
+  },
+
   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) {
@@ -403,23 +430,29 @@ var commandsPeerConnectionOfferAnswer = 
     return test.pcRemote.getStats().then(stats => {
       test.pcRemote.checkStatsIceConnectionType(stats,
           test.testOptions.expectedRemoteCandidateType);
     });
   },
 
   function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) {
     return test.pcLocal.getStats().then(stats => {
-      test.pcLocal.checkStatsIceConnections(stats, test.testOptions);
+      test.pcLocal.checkStatsIceConnections(stats,
+                                            test._offer_constraints,
+                                            test._offer_options,
+                                            test.testOptions);
     });
   },
 
   function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
     return test.pcRemote.getStats().then(stats => {
-      test.pcRemote.checkStatsIceConnections(stats, test.testOptions);
+      test.pcRemote.checkStatsIceConnections(stats,
+                                             test._offer_constraints,
+                                             test._offer_options,
+                                             test.testOptions);
     });
   },
 
   function PC_LOCAL_CHECK_MSID(test) {
     return test.pcLocal.checkMsids();
   },
   function PC_REMOTE_CHECK_MSID(test) {
     return test.pcRemote.checkMsids();
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
@@ -13,17 +13,17 @@
 
   runNetworkTest(function (options) {
     const test = new PeerConnectionTest(options);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
-          return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
           // We test both tracks to avoid an ordering problem
           is(test.pcRemote._pc.getReceivers().length, 2,
              "pcRemote should have two receivers");
           return Promise.all(test.pcRemote._pc.getReceivers().map(r => {
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
@@ -17,17 +17,17 @@
     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.expectIceChecking();
-          return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
         function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
           test.pcRemote.expectIceChecking();
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
           // We test both tracks to avoid an ordering problem
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
@@ -16,17 +16,17 @@
     const test = new PeerConnectionTest(options);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{video: true}, {video: true}],
                                    [{video: true}]);
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMediaAndAddStreams([{video: true, fake: true}]);
+          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
           const h = new VideoStreamHelper();
           is(test.pcRemote.remoteMediaElements.length, 2,
              "Should have two remote media elements after renegotiation");
           return Promise.all(test.pcRemote.remoteMediaElements.map(video =>
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
@@ -20,17 +20,17 @@
         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.expectIceChecking();
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMediaAndAddStreams([{video: true, fake: true}]);
+          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
         },
         function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
           test.pcRemote.expectIceChecking();
         },
       ],
       [
         function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
           const h = new VideoStreamHelper();
--- a/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
@@ -15,54 +15,53 @@ createHTML({
 
 runNetworkTest(function (options) {
   let test = new PeerConnectionTest(options);
   let eventsPromise;
   addRenegotiation(test.chain,
     [
       function PC_LOCAL_SWAP_VIDEO_TRACKS(test) {
         return getUserMedia({video: true}).then(stream => {
-          var videoTransceiver = test.pcLocal._pc.getTransceivers()[1];
-          is(videoTransceiver.currentDirection, "sendonly",
-             "Video transceiver's current direction is sendonly");
-          is(videoTransceiver.direction, "sendrecv",
-             "Video transceiver's desired direction is sendrecv");
-
           const localStream = test.pcLocal._pc.getLocalStreams()[0];
           ok(localStream, "Should have local stream");
 
           const remoteStream = test.pcRemote._pc.getRemoteStreams()[0];
           ok(remoteStream, "Should have remote stream");
 
           const newTrack = stream.getTracks()[0];
 
           const videoSenderIndex =
             test.pcLocal._pc.getSenders().findIndex(s => s.track.kind == "video");
           isnot(videoSenderIndex, -1, "Should have video sender");
 
           test.pcLocal.removeSender(videoSenderIndex);
-          is(videoTransceiver.direction, "recvonly",
-             "Video transceiver should be recvonly after removeTrack");
           test.pcLocal.attachLocalTrack(stream.getTracks()[0], localStream);
-          is(videoTransceiver.direction, "recvonly",
-             "Video transceiver should be recvonly after addTrack");
 
-          eventsPromise = haveEvent(remoteStream, "addtrack",
-              wait(50000, new Error("No addtrack event for " + newTrack.id)))
+          const addTrackPromise = haveEvent(remoteStream, "addtrack",
+              wait(50000, new Error("No addtrack event")))
             .then(trackEvent => {
               ok(trackEvent instanceof MediaStreamTrackEvent,
                  "Expected event to be instance of MediaStreamTrackEvent");
               is(trackEvent.type, "addtrack",
                  "Expected addtrack event type");
-              is(test.pcRemote.getWebrtcTrackId(trackEvent.track), newTrack.id, "Expected track in event");
+              is(trackEvent.track.id, newTrack.id, "Expected track in event");
               is(trackEvent.track.readyState, "live",
                  "added track should be live");
             })
             .then(() => haveNoEvent(remoteStream, "addtrack"));
 
+          const remoteTrack = test.pcRemote._pc.getReceivers()
+              .map(r => r.track)
+              .find(t => t.kind == "video");
+          ok(remoteTrack, "Should have received remote track");
+          const endedPromise = haveEvent(remoteTrack, "ended",
+              wait(50000, new Error("No ended event")));
+
+          eventsPromise = Promise.all([addTrackPromise, endedPromise]);
+
           remoteStream.addEventListener("removetrack",
                                         function onRemovetrack(trackEvent) {
             ok(false, "UA shouldn't raise 'removetrack' when receiving peer connection");
           })
         });
       },
     ],
     [
--- a/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
@@ -14,17 +14,17 @@
   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.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ]
     );
 
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
   });
 </script>
deleted file mode 100644
--- a/dom/media/tests/mochitest/test_peerConnection_basicAudioVideoTransceivers.html
+++ /dev/null
@@ -1,31 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <script type="application/javascript" src="pc.js"></script>
-</head>
-<body>
-<pre id="test">
-<script type="application/javascript">
-  createHTML({
-    bug: "1290948",
-    title: "Basic audio/video with addTransceiver"
-  });
-
-  var test;
-  runNetworkTest(function (options) {
-    test = new PeerConnectionTest(options);
-    test.setMediaConstraints([{audio: true}, {video: true}],
-                             [{audio: true}, {video: true}]);
-    test.chain.replace("PC_LOCAL_GUM",
-      [
-        function PC_LOCAL_GUM_TRANSCEIVERS(test) {
-          return test.pcLocal.getAllUserMediaAndAddTransceivers(test.pcLocal.constraints);
-        }
-      ]);
-
-    test.run();
-  });
-</script>
-</pre>
-</body>
-</html>
--- a/dom/media/tests/mochitest/test_peerConnection_bug1064223.html
+++ b/dom/media/tests/mochitest/test_peerConnection_bug1064223.html
@@ -11,17 +11,17 @@
     title: "CreateOffer fails without streams or modern RTCOfferOptions"
   });
 
   runNetworkTest(function () {
     var pc = new mozRTCPeerConnection();
     var options = { mandatory: { OfferToReceiveVideo: true } }; // obsolete
 
     pc.createOffer(options).then(() => ok(false, "createOffer must fail"),
-                                 e => is(e.name, "InvalidStateError",
+                                 e => is(e.name, "InternalError",
                                          "createOffer must fail"))
     .catch(e => ok(false, e.message))
     .then(() => {
       pc.close();
       networkTestFinished();
     })
     .catch(e => ok(false, e.message));
   });
--- a/dom/media/tests/mochitest/test_peerConnection_constructedStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_constructedStream.html
@@ -45,17 +45,17 @@ runNetworkTest(() => {
     ok(receivedStream, "We should receive a stream with with the sent stream's id (" + sentStreamId + ")");
     if (!receivedStream) {
       return;
     }
 
     is(receivedStream.getTracks().length, sentTracks.length,
        "Should receive same number of tracks as were sent");
     sentTracks.forEach(t =>
-      ok(receivedStream.getTracks().find(t2 => t.id == test.pcRemote.getWebrtcTrackId(t2)),
+      ok(receivedStream.getTracks().find(t2 => t.id == t2.id),
          "The sent track (" + t.id + ") should exist on the receive side"));
   };
 
   test.chain.append([
     function PC_REMOTE_CHECK_RECEIVED_CONSTRUCTED_STREAM() {
       checkSentTracksReceived(constructedStream.id, constructedStream.getTracks());
     },
     function PC_REMOTE_CHECK_RECEIVED_DUMMY_STREAM() {
--- a/dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html
@@ -13,17 +13,17 @@
 
   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}]);
-          return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
 
         function PC_REMOTE_SETUP_ICE_HANDLER(test) {
           test.pcRemote.setupIceCandidateHandler(test);
           if (test.testOptions.steeplechase) {
             test.pcRemote.endOfTrickleIce.then(() => {
               send_message({"type": "end_of_trickle_ice"});
             });
@@ -32,19 +32,16 @@
 
         function PC_REMOTE_CREATE_AND_SET_OFFER(test) {
           return test.createOffer(test.pcRemote).then(offer => {
             return test.setLocalDescription(test.pcRemote, offer, HAVE_LOCAL_OFFER);
           });
         },
 
         function PC_REMOTE_ROLLBACK(test) {
-          // the negotiationNeeded slot should have been true both before and
-          // after this SLD, so the event should fire again.
-          test.pcRemote.expectNegotiationNeeded();
           return test.setLocalDescription(test.pcRemote,
                                           { type: "rollback", sdp: "" },
                                           STABLE);
         },
 
         // Rolling back should shut down gathering
         function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
           return test.pcRemote.endOfTrickleIce;
--- a/dom/media/tests/mochitest/test_peerConnection_localRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_localRollback.html
@@ -18,19 +18,16 @@
     test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
         function PC_REMOTE_CREATE_AND_SET_OFFER(test) {
           return test.createOffer(test.pcRemote).then(offer => {
             return test.setLocalDescription(test.pcRemote, offer, HAVE_LOCAL_OFFER);
           });
         },
 
         function PC_REMOTE_ROLLBACK(test) {
-          // the negotiationNeeded slot should have been true both before and
-          // after this SLD, so the event should fire again.
-          test.pcRemote.expectNegotiationNeeded();
           return test.setLocalDescription(test.pcRemote,
                                           { type: "rollback", sdp: "" },
                                           STABLE);
         },
 
         // Rolling back should shut down gathering
         function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
           return test.pcRemote.endOfTrickleIce;
--- a/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
@@ -14,17 +14,17 @@
   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}]);
-          return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ]
     );
     test.chain.replaceAfter('PC_REMOTE_SET_REMOTE_DESCRIPTION',
       [
         function PC_LOCAL_SETUP_ICE_HANDLER(test) {
           test.pcLocal.setupIceCandidateHandler(test);
           if (test.testOptions.steeplechase) {
--- a/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
@@ -32,21 +32,20 @@
         },
         function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
           test.setOfferOptions({ offerToReceiveAudio: true });
           return test.pcLocal.removeSender(0);
         },
       ],
       [
         function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
-          // Simply removing a track is not enough to cause it to be
-          // signaled as ended. Spec may change though.
-          // TODO: One last check of the spec is in order
-          is(receivedTrack.readyState, "live",
-             "The received track should not have ended");
+          is(test.pcRemote._pc.getReceivers().length, 0,
+             "pcRemote should have no more receivers");
+          is(receivedTrack.readyState, "ended",
+             "The received track should have ended");
 
           return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
         },
       ]
     );
 
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html
@@ -24,40 +24,31 @@
         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.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+          is(test.pcRemote._pc.getReceivers().length, 1,
+              "pcRemote should still have one receiver");
+          const track = test.pcRemote._pc.getReceivers()[0].track;
+          isnot(originalTrack.id, track.id, "Receiver should have changed");
 
           const analyser = new AudioStreamAnalyser(
               new AudioContext(), new MediaStream([track]));
           const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
           return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
         },
-        function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
-
-          const analyser = new AudioStreamAnalyser(
-              new AudioContext(), new MediaStream([track]));
-          const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
-          return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
-        }
       ]
     );
 
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
   });
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html
@@ -7,18 +7,16 @@
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1017888",
     title: "Renegotiation: remove then add audio track"
   });
 
   runNetworkTest(function (options) {
-    options = options || { };
-    options.bundle = false;
     const test = new PeerConnectionTest(options);
     let originalTrack;
     addRenegotiation(test.chain,
       [
         function PC_REMOTE_FIND_RECEIVER(test) {
           is(test.pcRemote._pc.getReceivers().length, 1,
              "pcRemote should have one receiver");
           originalTrack = test.pcRemote._pc.getReceivers()[0].track;
@@ -26,46 +24,31 @@
         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.getAllUserMediaAndAddStreams([{audio: true}]);
-        },
-        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
-          test.pcLocal.expectIceChecking();
-        },
-        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
-          test.pcRemote.expectIceChecking();
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+          is(test.pcRemote._pc.getReceivers().length, 1,
+              "pcRemote should still have one receiver");
+          const track = test.pcRemote._pc.getReceivers()[0].track;
+          isnot(originalTrack.id, track.id, "Receiver should have changed");
 
           const analyser = new AudioStreamAnalyser(
               new AudioContext(), new MediaStream([track]));
           const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
           return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
         },
-        function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
-
-          const analyser = new AudioStreamAnalyser(
-              new AudioContext(), new MediaStream([track]));
-          const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
-          return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
-        }
       ]
     );
 
     test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
                                PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
 
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html
@@ -28,38 +28,33 @@
           // 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_VIDEO_TRACK(test) {
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMediaAndAddStreams([{video: true, fake: true}]);
+          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+          is(test.pcRemote._pc.getReceivers().length, 1,
+              "pcRemote should still have one receiver");
+          const track = test.pcRemote._pc.getReceivers()[0].track;
+          isnot(originalTrack.id, track.id, "Receiver should have changed");
 
+          const vOriginal = test.pcRemote.remoteMediaElements.find(
+              elem => elem.id.includes(originalTrack.id));
           const vAdded = test.pcRemote.remoteMediaElements.find(
               elem => elem.id.includes(track.id));
+          ok(vOriginal.ended, "Original video element should have ended");
           return helper.checkVideoPlaying(vAdded);
         },
-        function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
-
-          const vAdded = test.pcRemote.remoteMediaElements.find(
-              elem => elem.id.includes(track.id));
-          return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
-        }
       ]
     );
 
     test.setMediaConstraints([{video: true}], [{video: true}]);
     test.run();
   });
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html
@@ -8,18 +8,16 @@
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1017888",
     title: "Renegotiation: remove then add video track, no bundle"
   });
 
   runNetworkTest(function (options) {
-    options = options || { };
-    options.bundle = false;
     const test = new PeerConnectionTest(options);
     const helper = new VideoStreamHelper();
     var originalTrack;
     addRenegotiation(test.chain,
       [
         function PC_REMOTE_FIND_RECEIVER(test) {
           is(test.pcRemote._pc.getReceivers().length, 1,
              "pcRemote should have one receiver");
@@ -30,44 +28,33 @@
           // 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_VIDEO_TRACK(test) {
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMediaAndAddStreams([{video: true, fake: true}]);
-        },
-        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
-          test.pcLocal.expectIceChecking();
-        },
-        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
-          test.pcRemote.expectIceChecking();
+          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+          is(test.pcRemote._pc.getReceivers().length, 1,
+              "pcRemote should still have one receiver");
+          const track = test.pcRemote._pc.getReceivers()[0].track;
+          isnot(originalTrack.id, track.id, "Receiver should have changed");
 
+          const vOriginal = test.pcRemote.remoteMediaElements.find(
+              elem => elem.id.includes(originalTrack.id));
           const vAdded = test.pcRemote.remoteMediaElements.find(
               elem => elem.id.includes(track.id));
+          ok(vOriginal.ended, "Original video element should have ended");
           return helper.checkVideoPlaying(vAdded);
         },
-        function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 2,
-              "pcRemote should have two transceivers");
-          const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
-
-          const vAdded = test.pcRemote.remoteMediaElements.find(
-              elem => elem.id.includes(track.id));
-          return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
-        },
       ]
     );
 
     test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
                                PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
 
     test.setMediaConstraints([{video: true}], [{video: true}]);
     test.run();
--- a/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html
@@ -1,13 +1,12 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <script type="application/javascript" src="pc.js"></script>
-  <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     bug: "1017888",
     title: "Renegotiation: remove video track"
   });
@@ -32,24 +31,22 @@
         function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
           test.setOfferOptions({ offerToReceiveVideo: true });
           test.setMediaConstraints([], [{video: true}]);
           return test.pcLocal.removeSender(0);
         },
       ],
       [
         function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 1,
-              "pcRemote should have one transceiver");
-          const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
-
-          const vAdded = test.pcRemote.remoteMediaElements.find(
-              elem => elem.id.includes(track.id));
-          const helper = new VideoStreamHelper();
-          return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+          is(test.pcRemote._pc.getReceivers().length, 0,
+             "pcRemote should have no more receivers");
+          is(receivedTrack.readyState, "ended",
+             "The received track should have ended");
+          is(element.ended, true,
+             "Element playing the removed track should have ended");
         },
       ]
     );
 
     test.setMediaConstraints([{video: true}], [{video: true}]);
     test.run();
   });
 </script>
--- a/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
@@ -42,31 +42,29 @@
     return navigator.mediaDevices.getUserMedia({video:true, audio:true})
       .then(newStream => {
         window.grip = newStream;
         newTrack = newStream.getVideoTracks()[0];
         audiotrack = newStream.getAudioTracks()[0];
         isnot(newTrack, sender.track, "replacing with a different track");
         ok(!pc.getLocalStreams().some(s => s == newStream),
            "from a different stream");
-        // Use wrapper function, since it updates expected tracks
-        return wrapper.senderReplaceTrack(sender, newTrack, newStream);
+        return sender.replaceTrack(newTrack);
       })
       .then(() => {
         is(pc.getSenders().length, oldSenderCount, "same sender count");
         is(sender.track, newTrack, "sender.track has been replaced");
         ok(!pc.getSenders().map(sn => sn.track).some(t => t == oldTrack),
            "old track not among senders");
-        // Spec does not say we add this new track to any stream
-        ok(!pc.getLocalStreams().some(s => s.getTracks()
+        ok(pc.getLocalStreams().some(s => s.getTracks()
                                            .some(t => t == sender.track)),
-           "track does not exist among pc's local streams");
+           "track exists among pc's local streams");
         return sender.replaceTrack(audiotrack)
           .then(() => ok(false, "replacing with different kind should fail"),
-                e => is(e.name, "TypeError",
+                e => is(e.name, "IncompatibleMediaStreamTrackError",
                         "replacing with different kind should fail"));
       });
   }
 
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.audioCtx = new AudioContext();
     test.setMediaConstraints([{video: true, audio: true}], [{video: true}]);
@@ -127,61 +125,53 @@
         // (440Hz for loopback devices, 1kHz for fake tracks).
         sourceNode.frequency.value = 2000;
         sourceNode.start();
 
         var destNode = test.audioCtx.createMediaStreamDestination();
         sourceNode.connect(destNode);
         var newTrack = destNode.stream.getAudioTracks()[0];
 
-        return test.pcLocal.senderReplaceTrack(
-            sender, newTrack, destNode.stream)
+        return sender.replaceTrack(newTrack)
           .then(() => {
             is(pc.getSenders().length, oldSenderCount, "same sender count");
             ok(!pc.getSenders().some(sn => sn.track == oldTrack),
                "Replaced track should be removed from senders");
-            // TODO: Should PC remove local streams when there are no senders
-            // associated with it? getLocalStreams() isn't in the spec anymore,
-            // so I guess it is pretty arbitrary?
+            ok(allLocalStreamsHaveSender(pc),
+               "Shouldn't have any streams without a corresponding sender");
             is(sender.track, newTrack, "sender.track has been replaced");
-            // Spec does not say we add this new track to any stream
-            ok(!pc.getLocalStreams().some(s => s.getTracks()
+            ok(pc.getLocalStreams().some(s => s.getTracks()
                                                .some(t => t == sender.track)),
                "track exists among pc's local streams");
           });
       }
     ]);
     test.chain.append([
       function PC_LOCAL_CHECK_WEBAUDIO_FLOW_PRESENT(test) {
         return test.pcRemote.checkReceivingToneFrom(test.audioCtx, test.pcLocal);
       }
     ]);
     test.chain.append([
       function PC_LOCAL_INVALID_ADD_VIDEOTRACKS(test) {
-        let videoTransceivers = test.pcLocal._pc.getTransceivers()
-          .filter(transceiver => {
-            return !transceiver.stopped &&
-                   transceiver.receiver.track.kind == "video" &&
-                   transceiver.sender.track;
-          });
-
-        ok(videoTransceivers.length,
-           "There is at least one non-stopped video transceiver with a track.");
-
-        videoTransceivers.forEach(transceiver => {
-            var stream = test.pcLocal._pc.getLocalStreams()[0];;
-            var track = transceiver.sender.track;
-            try {
-              test.pcLocal._pc.addTrack(track, stream);
-              ok(false, "addTrack existing track should fail");
-            } catch (e) {
-              is(e.name, "InvalidAccessError",
-                 "addTrack existing track should fail");
-            }
-          });
+        var stream = test.pcLocal._pc.getLocalStreams()[0];
+        var track = stream.getVideoTracks()[0];
+        try {
+          test.pcLocal._pc.addTrack(track, stream);
+          ok(false, "addTrack existing track should fail");
+        } catch (e) {
+          is(e.name, "InvalidParameterError",
+             "addTrack existing track should fail");
+        }
+        try {
+          test.pcLocal._pc.addTrack(track, stream);
+          ok(false, "addTrack existing track should fail");
+        } catch (e) {
+          is(e.name, "InvalidParameterError",
+             "addTrack existing track should fail");
+        }
       }
     ]);
     test.run();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
+++ b/dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
@@ -31,38 +31,60 @@
     ]);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_REPLACE_VIDEO_TRACK_THEN_ADD_SECOND_STREAM(test) {
           emitter1.stop();
           emitter2.start();
           const newstream = emitter2.stream();
           const newtrack = newstream.getVideoTracks()[0];
-          var sender = test.pcLocal._pc.getSenders()[0];
-          return test.pcLocal.senderReplaceTrack(sender, newtrack, newstream)
+          return test.pcLocal.senderReplaceTrack(0, newtrack, newstream.id)
             .then(() => {
               test.setMediaConstraints([{video: true}, {video: true}],
                                        [{video: true}]);
+              // Use fake:true here since the native fake device on linux
+              // doesn't change color as needed by checkVideoPlaying() below.
+              return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
             });
         },
       ],
       [
-        function PC_REMOTE_CHECK_ORIGINAL_TRACK_NOT_ENDED(test) {
-          is(test.pcRemote._pc.getTransceivers().length, 1,
-              "pcRemote should have one transceiver");
-          const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
-
+        function PC_REMOTE_CHECK_ORIGINAL_TRACK_ENDED(test) {
           const vremote = test.pcRemote.remoteMediaElements.find(
-              elem => elem.id.includes(track.id));
+              elem => elem.id.includes(emitter1.stream().getTracks()[0].id));
+          if (!vremote) {
+            return Promise.reject(new Error("Couldn't find video element"));
+          }
+          ok(vremote.ended, "Original track should have ended after renegotiation");
+        },
+        function PC_REMOTE_CHECK_REPLACED_TRACK_FLOW(test) {
+          const vremote = test.pcRemote.remoteMediaElements.find(
+              elem => elem.id.includes(test.pcLocal._pc.getSenders()[0].track.id));
           if (!vremote) {
             return Promise.reject(new Error("Couldn't find video element"));
           }
-          ok(!vremote.ended, "Original track should not have ended after renegotiation (replaceTrack is not signalled!)");
+          return addFinallyToPromise(helper.checkVideoPlaying(vremote))
+            .finally(() => emitter2.stop())
+            .then(() => {
+              const px = helper._helper.getPixel(vremote, 10, 10);
+              const isBlue = helper._helper.isPixel(
+                  px, CaptureStreamTestHelper.prototype.blue, 5);
+              const isGrey = helper._helper.isPixel(
+                  px, CaptureStreamTestHelper.prototype.grey, 5);
+              ok(isBlue || isGrey, "replaced track should be blue or grey");
+            });
+        },
+        function PC_REMOTE_CHECK_ADDED_TRACK_FLOW(test) {
+          const vremote = test.pcRemote.remoteMediaElements.find(
+              elem => elem.id.includes(test.pcLocal._pc.getSenders()[1].track.id));
+          if (!vremote) {
+            return Promise.reject(new Error("Couldn't find video element"));
+          }
           return helper.checkVideoPlaying(vremote);
-        }
+        },
       ]
     );
 
     test.run();
    });
   });
 
 </script>
--- a/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
+++ b/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
@@ -13,73 +13,73 @@
   });
 
   const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
 
   var mustRejectWith = (msg, reason, f) =>
     f().then(() => ok(false, msg),
              e => is(e.name, reason, msg));
 
-  async function testScale(codec) {
+  function testScale(codec) {
     var pc1 = new RTCPeerConnection();
     var pc2 = new RTCPeerConnection();
 
     var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
     pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
     pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
 
     info("testing scaling with " + codec);
 
-    let stream = await navigator.mediaDevices.getUserMedia({ video: true });
-
-    var v1 = createMediaElement('video', 'v1');
-    var v2 = createMediaElement('video', 'v2');
-
-    var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e));
-    var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve);
-
-    is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+    pc1.onnegotiationneeded = e =>
+      pc1.createOffer()
+      .then(d => pc1.setLocalDescription(codec == "VP8"
+        ? d
+        : (d.sdp = sdputils.removeAllButPayloadType(d.sdp, 126), d)))
+      .then(() => pc2.setRemoteDescription(pc1.localDescription))
+      .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(d))
+      .then(() => pc1.setRemoteDescription(pc2.localDescription))
+      .catch(generateErrorCallback());
 
-    v1.srcObject = stream;
-    var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+    return navigator.mediaDevices.getUserMedia({ video: true })
+    .then(stream => {
+      var v1 = createMediaElement('video', 'v1');
+      var v2 = createMediaElement('video', 'v2');
 
-    await mustRejectWith(
-        "Invalid scaleResolutionDownBy must reject", "RangeError",
-        () => sender.setParameters(
-            { encodings:[{ scaleResolutionDownBy: 0.5 } ] })
-    );
+      is(v2.currentTime, 0, "v2.currentTime is zero at outset");
 
-    await sender.setParameters({ encodings: [{ maxBitrate: 60000,
-                                               scaleResolutionDownBy: 2 }] });
+      v1.srcObject = stream;
+      var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
 
-    let offer = await pc1.createOffer();
-    if (codec == "VP8") {
-      offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126);
-    }
-    await pc1.setLocalDescription(offer);
-    await pc2.setRemoteDescription(pc1.localDescription);
-
-    let answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(pc2.localDescription);
-    let trackevent = await ontrackfired;
-
-    v2.srcObject = trackevent.streams[0];
-
-    await v2loadedmetadata;
-
-    await waitUntil(() => v2.currentTime > 0 && v2.srcObject.currentTime > 0);
-    ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
-
-    ok(v1.videoWidth > 0, "source width is positive");
-    ok(v1.videoHeight > 0, "source height is positive");
-    is(v2.videoWidth, v1.videoWidth / 2, "sink is half the width of source");
-    is(v2.videoHeight, v1.videoHeight / 2, "sink is half the height of source");
-    stream.getTracks().forEach(track => track.stop());
-    v1.srcObject = v2.srcObject = null;
+      return mustRejectWith("Invalid scaleResolutionDownBy must reject", "RangeError",
+                            () => sender.setParameters({ encodings:
+                                                       [{ scaleResolutionDownBy: 0.5 } ] }))
+      .then(() => sender.setParameters({ encodings: [{ maxBitrate: 60000,
+                                                       scaleResolutionDownBy: 2 }] }))
+      .then(() => new Promise(resolve => pc2.ontrack = e => resolve(e)))
+      .then(e => v2.srcObject = e.streams[0])
+      .then(() => new Promise(resolve => v2.onloadedmetadata = resolve))
+      .then(() => waitUntil(() => v2.currentTime > 0 && v2.srcObject.currentTime > 0))
+      .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+      .then(() => wait(3000)) // TODO: Bug 1248154
+      .then(() => {
+        ok(v1.videoWidth > 0, "source width is positive");
+        ok(v1.videoHeight > 0, "source height is positive");
+        if (v2.videoWidth == 640 && v2.videoHeight == 480) { // TODO: Bug 1248154
+          info("Skipping test due to Bug 1248154");
+        } else {
+          is(v2.videoWidth, v1.videoWidth / 2, "sink is half the width of source");
+          is(v2.videoHeight, v1.videoHeight / 2, "sink is half the height of source");
+        }
+      })
+      .then(() => {
+        stream.getTracks().forEach(track => track.stop());
+        v1.srcObject = v2.srcObject = null;
+      })
+    })
+    .catch(generateErrorCallback());
   }
 
   pushPrefs(['media.peerconnection.video.lock_scaling', true]).then(() => {
     if (!navigator.appVersion.includes("Android")) {
       runNetworkTest(() => testScale("VP8").then(() => testScale("H264"))
                     .then(networkTestFinished));
     } else {
       // No support for H.264 on Android in automation, see Bug 1355786
--- a/dom/media/tests/mochitest/test_peerConnection_setParameters.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setParameters.html
@@ -12,21 +12,20 @@ createHTML({
   visible: true
 });
 
 function parameterstest(pc) {
   ok(pc.getSenders().length > 0, "have senders");
   var sender = pc.getSenders()[0];
 
   var testParameters = (params, errorName, errorMsg) => {
-    info("Trying to set " + JSON.stringify(params));
 
     var validateParameters = (a, b) => {
       var validateEncoding = (a, b) => {
-        is(a.rid, b.rid, "same rid");
+        is(a.rid, b.rid || "", "same rid");
         is(a.maxBitrate, b.maxBitrate, "same maxBitrate");
         is(a.scaleResolutionDownBy, b.scaleResolutionDownBy,
            "same scaleResolutionDownBy");
       };
       is(a.encodings.length, (b.encodings || []).length, "same encodings");
       a.encodings.forEach((en, i) => validateEncoding(en, b.encodings[i]));
     };
 
deleted file mode 100644
--- a/dom/media/tests/mochitest/test_peerConnection_transceivers.html
+++ /dev/null
@@ -1,1707 +0,0 @@
-<!DOCTYPE HTML>
-<html>
-<head>
-  <script type="application/javascript" src="pc.js"></script>
-</head>
-<body>
-<pre id="test">
-<script type="application/javascript">
-  createHTML({
-    bug: "1290948",
-    title: "Transceivers API tests"
-  });
-
-  let checkThrows = async (func, exceptionName, description) => {
-    try {
-      await func();
-      ok(false, description + " throws " + exceptionName);
-    } catch (e) {
-      is(e.name, exceptionName, description + " throws " + exceptionName);
-    }
-  };
-
-  let stopTracks = (...streams) => {
-    streams.forEach(stream => stream.getTracks().forEach(track => track.stop()));
-  };
-
-  let setRemoteDescriptionReturnTrackEvents = async (pc, desc) => {
-    let trackEvents = [];
-    let listener = e => trackEvents.push(e);
-    pc.addEventListener("track", listener);
-    await pc.setRemoteDescription(desc);
-    pc.removeEventListener("track", listener);
-
-    // basic sanity-check, simplifies testing elsewhere
-    for (let e of trackEvents) {
-      ok(e.track, "Track is set on event");
-      ok(e.receiver, "Receiver is set on event");
-      ok(e.transceiver, "Transceiver is set on event");
-      ok(e.streams, "Streams is set on event");
-      is(e.receiver, e.transceiver.receiver, "Receiver belongs to transceiver");
-      is(e.track, e.receiver.track, "Track belongs to receiver");
-    }
-
-    return trackEvents;
-  };
-
-  let trickle = (pc1, pc2) => {
-    pc1.onicecandidate = async e => {
-      info("Adding ICE candidate: " + JSON.stringify(e.candidate));
-      try {
-        await pc2.addIceCandidate(e.candidate);
-      } catch(e) {
-        ok(false, "addIceCandidate threw error: " + e.name);
-      }
-    };
-  };
-
-  let iceConnected = pc => {
-    info("Waiting for ICE connected...");
-    return new Promise((resolve, reject) => {
-      let iceCheck = () => {
-        if (pc.iceConnectionState == "connected") {
-          ok(true, "ICE connected");
-          resolve();
-        }
-
-        if (pc.iceConnectionState == "failed") {
-          ok(false, "ICE failed");
-          reject();
-        }
-      };
-
-      iceCheck();
-      pc.oniceconnectionstatechange = iceCheck;
-    });
-  };
-
-  let negotiationNeeded = pc => {
-    return new Promise(resolve => pc.onnegotiationneeded = resolve);
-  };
-
-  let logExpected = expected => {
-    info("(expected " + JSON.stringify(expected) + ")");
-  };
-
-  let hasProps = (observed, expected) => {
-
-    if (observed === expected) {
-      return true;
-    }
-
-    // If we are expecting an array, iterate over it
-    if (Array.isArray(expected)) {
-      if (!Array.isArray(observed)) {
-        ok(false, "Expected an array, but didn't get one.");
-        logExpected(expected);
-        return false;
-      }
-
-      if (observed.length !== expected.length) {
-        ok(false, "Expected array to be " + expected.length + " long, but it was " + observed.length + " long instead");
-        logExpected(expected);
-        return false;
-      }
-
-      for (let i = 0; i < expected.length; i++) {
-        if (!hasProps(observed[i], expected[i])) {
-          logExpected(expected);
-          return false;
-        }
-      }
-
-      return true;
-    }
-
-    // If we are expecting an object, check its props
-    if (typeof expected === "object" && expected !== null) {
-      let propsWeCareAbout = Object.getOwnPropertyNames(expected);
-      for (let i in propsWeCareAbout) {
-        let prop = propsWeCareAbout[i];
-        if (!hasProps(observed[prop], expected[prop])) {
-          logExpected(expected);
-          return false;
-        }
-      }
-
-      return true;
-    }
-
-    ok(false, "Expected (" + JSON.stringify(expected) + ") did not match " +
-              "observed (" + JSON.stringify(observed) + ")");
-    return false;
-  };
-
-  let checkAddTransceiverNoTrack = async () => {
-    let pc = new RTCPeerConnection();
-    hasProps(pc.getTransceivers(), []);
-
-    pc.addTransceiver("audio");
-    pc.addTransceiver("video");
-
-    // NOTE: the w3c spec doesn't say anything about transceiver order, so this
-    // may not necessarily be the same order we see on other browsers.
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio", readyState: "live"}},
-          sender: {track: null},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        },
-        {
-          receiver: {track: {kind: "video", readyState: "live"}},
-          sender: {track: null},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    pc.close();
-  };
-
-  let checkAddTransceiverWithTrack = async () => {
-    let pc = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true, video: true});
-    let audio = stream.getAudioTracks()[0];
-    let video = stream.getVideoTracks()[0];
-
-    pc.addTransceiver(audio);
-    pc.addTransceiver(video);
-
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: audio},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        },
-        {
-          receiver: {track: {kind: "video"}},
-          sender: {track: video},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    pc.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTransceiverWithAddTrack = async () => {
-    let pc = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true, video: true});
-    let audio = stream.getAudioTracks()[0];
-    let video = stream.getVideoTracks()[0];
-
-    pc.addTrack(audio, stream);
-    pc.addTrack(video, stream);
-
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: audio},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        },
-        {
-          receiver: {track: {kind: "video"}},
-          sender: {track: video},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    pc.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTransceiverWithDirection = async () => {
-    let pc = new RTCPeerConnection();
-
-    pc.addTransceiver("audio", {direction: "recvonly"});
-    pc.addTransceiver("video", {direction: "recvonly"});
-
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        },
-        {
-          receiver: {track: {kind: "video"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    pc.close();
-  };
-
-  let checkAddTransceiverWithStream = async () => {
-    let pc = new RTCPeerConnection();
-
-    let audioStream = await getUserMedia({audio: true});
-    let videoStream = await getUserMedia({video: true});
-    let audio = audioStream.getAudioTracks()[0];
-    let video = videoStream.getVideoTracks()[0];
-
-    pc.addTransceiver(audio, {streams: [audioStream]});
-    pc.addTransceiver(video, {streams: [videoStream]});
-
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: audio},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        },
-        {
-          receiver: {track: {kind: "video"}},
-          sender: {track: video},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    let offer = await pc.createOffer();
-    ok(offer.sdp.includes("a=msid:" + audioStream.id + " " + audio.id),
-      "offer contains the expected audio msid");
-    ok(offer.sdp.includes("a=msid:" + videoStream.id + " " + video.id),
-      "offer contains the expected video msid");
-
-    pc.close();
-    stopTracks(audioStream, videoStream);
-  };
-
-  let checkAddTransceiverWithOfferToReceive = async kinds => {
-    let pc = new RTCPeerConnection();
-
-    let options = {};
-
-    for (let kind of kinds) {
-      if (kind == "audio") {
-        options.offerToReceiveAudio = true;
-      } else if (kind == "video") {
-        options.offerToReceiveVideo = true;
-      }
-    }
-
-    let offer = await pc.createOffer(options);
-
-    let expected = [];
-
-    // NOTE: The ordering here is not laid out in the spec at all, this is
-    // firefox specific.
-    if (options.offerToReceiveVideo) {
-      expected.push(
-        {
-          receiver: {track: {kind: "video"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        });
-    }
-
-    if (options.offerToReceiveAudio) {
-      expected.push(
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        });
-    }
-
-    hasProps(pc.getTransceivers(), expected);
-
-    pc.close();
-  };
-
-  let checkAddTransceiverWithSetRemoteOfferSending = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTransceiver(track, {streams: [stream]});
-
-    let offer = await pc1.createOffer();
-
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTransceiverWithSetRemoteOfferNoSend = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTransceiver(track);
-    pc1.getTransceivers()[0].direction = "recvonly";
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents, []);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: null},
-          // rtcweb-jsep says this is recvonly, w3c-webrtc does not...
-          direction: "recvonly",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTransceiverBadKind = async () => {
-    let pc = new RTCPeerConnection();
-    try {
-      pc.addTransceiver("foo");
-      ok(false, 'addTransceiver("foo") throws');
-    }
-    catch (e if e instanceof TypeError) {
-      ok(true, 'addTransceiver("foo") throws a TypeError');
-    }
-    catch (e) {
-      ok(false, 'addTransceiver("foo") throws a TypeError');
-    }
-
-    hasProps(pc.getTransceivers(), []);
-
-    pc.close();
-  };
-
-  let checkAddTransceiverNoTrackDoesntPair = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-    pc1.addTransceiver("audio");
-    pc2.addTransceiver("audio");
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[1].receiver.track,
-          streams: []
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {mid: null}, // no addTrack magic, doesn't auto-pair
-        {mid: "sdparta_0"} // Created by SRD
-      ]);
-
-    pc1.close();
-    pc2.close();
-  };
-
-  let checkAddTransceiverWithTrackDoesntPair = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-    pc1.addTransceiver("audio");
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc2.addTransceiver(track);
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[1].receiver.track,
-          streams: []
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {mid: null, sender: {track}},
-        {mid: "sdparta_0", sender: {track: null}} // Created by SRD
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTransceiverThenReplaceTrackDoesntPair = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-    pc1.addTransceiver("audio");
-    pc2.addTransceiver("audio");
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc2.getTransceivers()[0].sender.replaceTrack(track);
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[1].receiver.track,
-          streams: []
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {mid: null, sender: {track}},
-        {mid: "sdparta_0", sender: {track: null}} // Created by SRD
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTransceiverThenAddTrackPairs = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-    pc1.addTransceiver("audio");
-    pc2.addTransceiver("audio");
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: []
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {mid: "sdparta_0", sender: {track}}
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkAddTrackPairs = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-    pc1.addTransceiver("audio");
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: []
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {mid: "sdparta_0", sender: {track}}
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkReplaceTrackNullDoesntPreventPairing = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-    pc1.addTransceiver("audio");
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc2.addTrack(track, stream);
-    pc2.getTransceivers()[0].sender.replaceTrack(null);
-
-    let offer = await pc1.createOffer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: []
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {mid: "sdparta_0", sender: {track}}
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkSetDirection = async () => {
-    let pc = new RTCPeerConnection();
-    pc.addTransceiver("audio");
-
-    pc.getTransceivers()[0].direction = "sendonly";
-    hasProps(pc.getTransceivers(),[{direction: "sendonly"}]);
-    pc.getTransceivers()[0].direction = "recvonly";
-    hasProps(pc.getTransceivers(),[{direction: "recvonly"}]);
-    pc.getTransceivers()[0].direction = "inactive";
-    hasProps(pc.getTransceivers(),[{direction: "inactive"}]);
-    pc.getTransceivers()[0].direction = "sendrecv";
-    hasProps(pc.getTransceivers(),[{direction: "sendrecv"}]);
-
-    pc.close();
-  };
-
-  let checkCurrentDirection = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
-
-    let offer = await pc1.createOffer();
-    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
-
-    await pc1.setLocalDescription(offer);
-    hasProps(pc1.getTransceivers(), [{currentDirection: null}]);
-
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
-
-    let answer = await pc2.createAnswer();
-    hasProps(pc2.getTransceivers(), [{currentDirection: null}]);
-
-    await pc2.setLocalDescription(answer);
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    pc2.getTransceivers()[0].direction = "sendonly";
-
-    offer = await pc2.createOffer();
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    await pc2.setLocalDescription(offer);
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
-    hasProps(trackEvents, []);
-
-    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    answer = await pc1.createAnswer();
-    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    // TODO(bug 1400363): Check onmute/muted
-    await pc1.setLocalDescription(answer);
-    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
-    hasProps(trackEvents, []);
-
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
-
-    pc2.getTransceivers()[0].direction = "sendrecv";
-
-    offer = await pc2.createOffer();
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
-
-    await pc2.setLocalDescription(offer);
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, offer);
-    hasProps(trackEvents, []);
-
-    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
-
-    answer = await pc1.createAnswer();
-    hasProps(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
-
-    // TODO(bug 1400363): Check onunmute/muted
-    await pc1.setLocalDescription(answer);
-    hasProps(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, answer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkSendrecvWithNoSendTrack = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTransceiver("audio");
-    pc1.getTransceivers()[0].direction = "sendrecv";
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: []
-        }
-      ]);
-
-    trickle(pc1, pc2);
-    await pc1.setLocalDescription(offer);
-
-    let answer = await pc2.createAnswer();
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    // Spec language doesn't say anything about checking whether the transceiver
-    // is stopped here.
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    trickle(pc2, pc1);
-    await pc2.setLocalDescription(answer);
-
-    await iceConnected(pc1);
-    await iceConnected(pc2);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkStop = async () => {
-    let pc1 = new RTCPeerConnection();
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-
-    let pc2 = new RTCPeerConnection();
-    await pc2.setRemoteDescription(offer);
-
-    pc2.addTrack(track, stream);
-
-    let answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    let stoppedTransceiver = pc1.getTransceivers()[0];
-    let onended = new Promise(resolve => {
-      stoppedTransceiver.receiver.track.onended = resolve;
-    });
-    stoppedTransceiver.stop();
-
-    await onended;
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          sender: {track: {kind: "audio"}},
-          receiver: {track: {kind: "audio", readyState: "ended"}},
-          stopped: true,
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          direction: "sendrecv"
-        }
-      ]);
-
-    let transceiver = pc1.getTransceivers()[0];
-
-    checkThrows(() => transceiver.sender.setParameters(
-                        transceiver.sender.getParameters()),
-                "InvalidStateError", "setParameters on stopped transceiver");
-
-    let stream2 = await getUserMedia({audio: true});
-    let track2 = stream.getAudioTracks()[0];
-    checkThrows(() => transceiver.sender.replaceTrack(track2),
-                "InvalidStateError", "replaceTrack on stopped transceiver");
-
-    checkThrows(() => transceiver.direction = "sendrecv",
-                "InvalidStateError", "setDirection on stopped transceiver");
-
-    checkThrows(() => transceiver.sender.dtmf.insertDTMF("111"),
-                "InvalidStateError", "insertDTMF on stopped transceiver");
-
-    // Shouldn't throw
-    stoppedTransceiver.stop();
-
-    offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-
-    stoppedTransceiver = pc2.getTransceivers()[0];
-    onended = new Promise(resolve => {
-      stoppedTransceiver.receiver.track.onended = resolve;
-    });
-
-    await pc2.setRemoteDescription(offer);
-
-    await onended;
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          sender: {track: {kind: "audio"}},
-          receiver: {track: {kind: "audio", readyState: "ended"}},
-          stopped: true,
-          mid: null,
-          currentDirection: null,
-          direction: "sendrecv"
-        }
-      ]);
-
-    // Shouldn't throw either
-    stoppedTransceiver.stop();
-
-    pc1.close();
-    pc2.close();
-
-    // Still shouldn't throw
-    stoppedTransceiver.stop();
-
-    stopTracks(stream);
-  };
-
-  let checkStopAfterCreateOffer = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    pc1.getTransceivers()[0].stop();
-
-    await pc2.setRemoteDescription(offer)
-    trickle(pc1, pc2);
-    await pc1.setLocalDescription(offer);
-
-    let answer = await pc2.createAnswer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    // Spec language doesn't say anything about checking whether the transceiver
-    // is stopped here.
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: "sdparta_0"
-        }
-      ]);
-
-    trickle(pc2, pc1);
-    await pc2.setLocalDescription(answer);
-
-    await negotiationNeeded(pc1);
-    await iceConnected(pc1);
-    await iceConnected(pc2);
-
-    offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-    await pc2.setRemoteDescription(offer);
-    answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkStopAfterSetLocalOffer = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    await pc2.setRemoteDescription(offer)
-    trickle(pc1, pc2);
-    await pc1.setLocalDescription(offer);
-
-    pc1.getTransceivers()[0].stop();
-
-    let answer = await pc2.createAnswer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    // Spec language doesn't say anything about checking whether the transceiver
-    // is stopped here.
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: "sdparta_0"
-        }
-      ]);
-
-    trickle(pc2, pc1);
-    await pc2.setLocalDescription(answer);
-
-    await negotiationNeeded(pc1);
-    await iceConnected(pc1);
-    await iceConnected(pc2);
-
-    offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-    await pc2.setRemoteDescription(offer);
-    answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkStopAfterSetRemoteOffer = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    await pc2.setRemoteDescription(offer)
-    await pc1.setLocalDescription(offer);
-
-    // Stop on _answerer_side now. Should take effect in answer.
-    pc2.getTransceivers()[0].stop();
-
-    let answer = await pc2.createAnswer();
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    hasProps(trackEvents, []);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    await pc2.setLocalDescription(answer);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkStopAfterCreateAnswer = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    await pc2.setRemoteDescription(offer)
-    trickle(pc1, pc2);
-    await pc1.setLocalDescription(offer);
-
-    let answer = await pc2.createAnswer();
-
-    // Too late for this to go in the answer. ICE should succeed.
-    pc2.getTransceivers()[0].stop();
-
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: "sdparta_0"
-        }
-      ]);
-
-    trickle(pc2, pc1);
-    await pc2.setLocalDescription(answer);
-
-    await negotiationNeeded(pc2);
-    await iceConnected(pc1);
-    await iceConnected(pc2);
-
-    offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-    await pc2.setRemoteDescription(offer);
-    answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkStopAfterSetLocalAnswer = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    await pc2.setRemoteDescription(offer)
-    trickle(pc1, pc2);
-    await pc1.setLocalDescription(offer);
-
-    let answer = await pc2.createAnswer();
-
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    trickle(pc2, pc1);
-    await pc2.setLocalDescription(answer);
-
-    // ICE should succeed.
-    pc2.getTransceivers()[0].stop();
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: "sdparta_0"
-        }
-      ]);
-
-    await negotiationNeeded(pc2);
-    await iceConnected(pc1);
-    await iceConnected(pc2);
-
-    offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-    await pc2.setRemoteDescription(offer);
-    answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true,
-          mid: null
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream);
-  };
-
-  let checkStopAfterClose = async () => {
-    let pc1 = new RTCPeerConnection();
-    let pc2 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-    pc2.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-    await pc2.setRemoteDescription(offer)
-    await pc1.setLocalDescription(offer);
-    let answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    pc1.close();
-    pc2.close();
-    await checkThrows(() => pc1.getTransceivers()[0].stop(),
-                      "InvalidStateError",
-                      "Stopping a transceiver on a closed PC should throw.");
-    stopTracks(stream);
-  };
-
-  let checkLocalRollback = async () => {
-    let pc = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc.addTrack(track, stream);
-
-    let offer = await pc.createOffer();
-    await pc.setLocalDescription(offer);
-
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track},
-          direction: "sendrecv",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    // Verify that rollback doesn't stomp things it should not
-    pc.getTransceivers()[0].direction = "sendonly";
-    let stream2 = await getUserMedia({audio: true});
-    let track2 = stream2.getAudioTracks()[0];
-    await pc.getTransceivers()[0].sender.replaceTrack(track2);
-
-    await pc.setLocalDescription({type: "rollback"});
-
-    hasProps(pc.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: track2},
-          direction: "sendonly",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    // Make sure stop() isn't rolled back either.
-    offer = await pc.createOffer();
-    await pc.setLocalDescription(offer);
-    pc.getTransceivers()[0].stop();
-    await pc.setLocalDescription({type: "rollback"});
-
-    hasProps(pc.getTransceivers(), [{ stopped: true }]);
-
-    stopTracks(stream);
-    pc.close();
-  };
-
-  let checkRemoteRollback = async () => {
-    let pc1 = new RTCPeerConnection();
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-
-    let pc2 = new RTCPeerConnection();
-    await pc2.setRemoteDescription(offer);
-
-    let removedTransceiver = pc2.getTransceivers()[0];
-
-    let onended = new Promise(resolve => {
-      removedTransceiver.receiver.track.onended = resolve;
-    });
-
-    await pc2.setRemoteDescription({type: "rollback"});
-
-    // Transceiver should be _gone_
-    hasProps(pc2.getTransceivers(), []);
-
-    hasProps(removedTransceiver,
-      {
-        stopped: true,
-        mid: null,
-        currentDirection: null
-      }
-    );
-
-    await onended;
-
-    hasProps(removedTransceiver,
-      {
-        receiver: {track: {readyState: "ended"}},
-        stopped: true,
-        mid: null,
-        currentDirection: null
-      }
-    );
-
-    // Setting the same offer again should do the same thing as before
-    await pc2.setRemoteDescription(offer);
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    // Give pc2 a track with replaceTrack
-    let stream2 = await getUserMedia({audio: true});
-    let track2 = stream2.getAudioTracks()[0];
-    await pc2.getTransceivers()[0].sender.replaceTrack(track2);
-    pc2.getTransceivers()[0].direction = "sendrecv";
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: track2},
-          direction: "sendrecv",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    await pc2.setRemoteDescription({type: "rollback"});
-
-    // Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
-    // nor does setDirection.
-    hasProps(pc2.getTransceivers(), []);
-
-    // Setting the same offer for a _third_ time should do the same thing
-    await pc2.setRemoteDescription(offer);
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: null},
-          direction: "recvonly",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    // We should be able to add the same track again
-    pc2.addTrack(track2, stream2);
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: track2},
-          direction: "sendrecv",
-          mid: "sdparta_0", // Firefox-specific
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    await pc2.setRemoteDescription({type: "rollback"});
-    // Transceiver should _not_ be gone this time, because addTrack touched it.
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: track2},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: false
-        }
-      ]);
-
-    // Complete negotiation so we can test interactions with transceiver.stop()
-    await pc1.setLocalDescription(offer);
-
-    // After all this SRD/rollback, we should still get the track event
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    let answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-
-    // Make sure all this rollback hasn't messed up the signaling
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc1.getTransceivers()[0].receiver.track,
-          streams: [{id: stream2.id}]
-        }
-      ]);
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track},
-          direction: "sendrecv",
-          mid: "sdparta_0",
-          currentDirection: "sendrecv",
-          stopped: false
-        }
-      ]);
-
-    // Don't bother waiting for ICE and such
-
-    // Check to see whether rolling back a remote track removal works
-    pc1.getTransceivers()[0].direction = "recvonly";
-    offer = await pc1.createOffer();
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents, []);
-
-    trackEvents =
-      await setRemoteDescriptionReturnTrackEvents(pc2, {type: "rollback"});
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[0].receiver.track,
-          streams: [{id: stream.id}]
-        }
-      ]);
-
-    // Check to see that stop() cannot be rolled back
-    pc1.getTransceivers()[0].stop();
-    offer = await pc1.createOffer();
-
-    await pc2.setRemoteDescription(offer);
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: track2},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: true
-        }
-      ]);
-
-    // stop() cannot be rolled back!
-    await pc2.setRemoteDescription({type: "rollback"});
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          receiver: {track: {kind: "audio"}},
-          sender: {track: {kind: "audio"}},
-          direction: "sendrecv",
-          mid: null,
-          currentDirection: null,
-          stopped: true
-        }
-      ]);
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream, stream2);
-  };
-
-  let checkMsectionReuse = async () => {
-    // Use max-compat to make it easier to check for disabled m-sections
-    let pc1 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
-    let pc2 = new RTCPeerConnection({ bundlePolicy: "max-compat" });
-
-    let stream = await getUserMedia({audio: true});
-    let track = stream.getAudioTracks()[0];
-    pc1.addTrack(track, stream);
-
-    let offer = await pc1.createOffer();
-    await pc1.setLocalDescription(offer);
-    await pc2.setRemoteDescription(offer);
-
-    // answerer stops transceiver to reject m-section
-    pc2.getTransceivers()[0].stop();
-
-    let answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-    await pc1.setRemoteDescription(answer);
-
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          mid: null,
-          currentDirection: null,
-          stopped: true
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          mid: null,
-          currentDirection: null,
-          stopped: true
-        }
-      ]);
-
-    // Check that m-section is reused on both ends
-    let stream2 = await getUserMedia({audio: true});
-    let track2 = stream2.getAudioTracks()[0];
-
-    pc1.addTrack(track2, stream2);
-    offer = await pc1.createOffer();
-    is(offer.sdp.match(/m=/g).length, 1, "Exactly one m-line in offer, because it was reused");
-    hasProps(pc1.getTransceivers(),
-      [
-        {
-          stopped: true
-        },
-        {
-          sender: {track: track2}
-        }
-      ]);
-
-
-    pc2.addTrack(track, stream);
-    offer = await pc2.createOffer();
-    is(offer.sdp.match(/m=/g).length, 1, "Exactly one m-line in offer, because it was reused");
-    hasProps(pc2.getTransceivers(),
-      [
-        {
-          stopped: true
-        },
-        {
-          sender: {track}
-        }
-      ]);
-
-    await pc2.setLocalDescription(offer);
-    await pc1.setRemoteDescription(offer);
-    answer = await pc1.createAnswer();
-    await pc1.setLocalDescription(answer);
-    await pc2.setRemoteDescription(answer);
-    hasProps(pc1.getTransceivers(),
-      [
-        {},
-        {
-          sender: {track: track2},
-          currentDirection: "sendrecv"
-        }
-      ]);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {},
-        {
-          sender: {track},
-          currentDirection: "sendrecv"
-        }
-      ]);
-
-    // stop the transceiver, and add a track. Verify that we don't reuse
-    // prematurely in our offer. (There should be one rejected m-section, and a
-    // new one for the new track)
-    pc1.getTransceivers()[1].stop();
-    let stream3 = await getUserMedia({audio: true});
-    let track3 = stream3.getAudioTracks()[0];
-    pc1.addTrack(track3, stream3);
-    offer = await pc1.createOffer();
-    is(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer, because it is too early to reuse");
-    is(offer.sdp.match(/m=audio 0 /g).length, 1, "One m-line is rejected");
-
-    await pc1.setLocalDescription(offer);
-
-    let trackEvents = await setRemoteDescriptionReturnTrackEvents(pc2, offer);
-    hasProps(trackEvents,
-      [
-        {
-          track: pc2.getTransceivers()[2].receiver.track,
-          streams: [{id: stream3.id}]
-        }
-      ]);
-
-    answer = await pc2.createAnswer();
-    await pc2.setLocalDescription(answer);
-
-    trackEvents = await setRemoteDescriptionReturnTrackEvents(pc1, answer);
-    hasProps(trackEvents, []);
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {},
-        {
-          stopped: true
-        },
-        {
-          mid: "sdparta_1", // Firefox-specific
-          sender: {track: null},
-          currentDirection: "recvonly"
-        }
-      ]);
-
-    pc2.addTrack(track3, stream3);
-    // There are two ways to handle this new track; reuse the recvonly
-    // transceiver created above, or create a new transceiver and reuse the
-    // disabled m-section. We're supposed to do the former.
-    offer = await pc2.createOffer();
-    is(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer");
-    is(offer.sdp.match(/m=audio 0 /g).length, 1, "One m-line is rejected, because the other was used");
-
-    hasProps(pc2.getTransceivers(),
-      [
-        {},
-        {
-          stopped: true
-        },
-        {
-          mid: "sdparta_1", // Firefox-specific
-          sender: {track: track3},
-          currentDirection: "recvonly",
-          direction: "sendrecv"
-        }
-      ]);
-
-    // Add _another_ track; this should reuse the disabled m-section
-    let stream4 = await getUserMedia({audio: true});
-    let track4 = stream4.getAudioTracks()[0];
-    pc2.addTrack(track4, stream4);
-    offer = await pc2.createOffer();
-    await pc2.setLocalDescription(offer);
-    hasProps(pc2.getTransceivers(),
-      [
-        {}, {},
-        {
-          mid: "sdparta_1", // Firefox-specific
-        },
-        {
-          sender: {track: track4},
-          mid: "sdparta_0" // Firefox-specific
-        }
-      ]);
-    is(offer.sdp.match(/m=/g).length, 2, "Exactly 2 m-lines in offer, because m-section was reused");
-    is(offer.sdp.match(/m=audio 0 /g), null, "No rejected m-line, because it was reused");
-
-    pc1.close();
-    pc2.close();
-    stopTracks(stream, stream2, stream3, stream4);
-  };
-
-  runNetworkTest(async () => {
-    await checkAddTransceiverNoTrack();
-    await checkAddTransceiverWithTrack();
-    await checkAddTransceiverWithAddTrack();
-    await checkAddTransceiverWithDirection();
-    await checkAddTransceiverWithStream();
-    await checkAddTransceiverWithOfferToReceive(["audio"]);
-    await checkAddTransceiverWithOfferToReceive(["video"]);
-    await checkAddTransceiverWithOfferToReceive(["audio", "video"]);
-    await checkAddTransceiverWithSetRemoteOfferSending();
-    await checkAddTransceiverWithSetRemoteOfferNoSend();
-    await checkAddTransceiverBadKind();
-    await checkSetDirection();
-    await checkCurrentDirection();
-    await checkSendrecvWithNoSendTrack();
-    await checkAddTransceiverNoTrackDoesntPair();
-    await checkAddTransceiverWithTrackDoesntPair();
-    await checkAddTransceiverThenReplaceTrackDoesntPair();
-    await checkAddTransceiverThenAddTrackPairs();
-    await checkAddTrackPairs();
-    await checkReplaceTrackNullDoesntPreventPairing();
-    await checkStop();
-    await checkStopAfterCreateOffer();
-    await checkStopAfterSetLocalOffer();
-    await checkStopAfterSetRemoteOffer();
-    await checkStopAfterCreateAnswer();
-    await checkStopAfterSetLocalAnswer();
-    await checkStopAfterClose();
-    await checkLocalRollback();
-    await checkRemoteRollback();
-    await checkMsectionReuse();
-    return SimpleTest.finish();
-  });
-</script>
-</pre>
-</body>
-</html>
--- a/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
@@ -14,23 +14,35 @@
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
     test.chain.insertAfter("PC_REMOTE_GET_OFFER", [
         function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) {
           test._local_offer.sdp = test._local_offer.sdp.replace(
               /a=msid:[^\s]*/g,
               "a=msid:foo");
+        },
+        function PC_REMOTE_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+          Object.keys(
+              test.pcRemote.expectedRemoteTrackInfoById).forEach(trackId => {
+                test.pcRemote.expectedRemoteTrackInfoById[trackId].streamId = "foo";
+              });
         }
     ]);
     test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
         function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) {
           test._remote_answer.sdp = test._remote_answer.sdp.replace(
               /a=msid:[^\s]*/g,
               "a=msid:foo");
+        },
+        function PC_LOCAL_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+          Object.keys(
+              test.pcLocal.expectedRemoteTrackInfoById).forEach(trackId => {
+                test.pcLocal.expectedRemoteTrackInfoById[trackId].streamId = "foo";
+              });
         }
     ]);
     test.setMediaConstraints([{audio: true}, {audio: true}],
                              [{audio: true}, {audio: true}]);
     test.run();
   });
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html
@@ -14,23 +14,35 @@
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
     test.chain.insertAfter("PC_REMOTE_GET_OFFER", [
         function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) {
           test._local_offer.sdp = test._local_offer.sdp.replace(
               /a=msid:[^\s]*/g,
               "a=msid:foo");
+        },
+        function PC_REMOTE_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+          Object.keys(
+              test.pcRemote.expectedRemoteTrackInfoById).forEach(trackId => {
+                test.pcRemote.expectedRemoteTrackInfoById[trackId].streamId = "foo";
+              });
         }
     ]);
     test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
         function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) {
           test._remote_answer.sdp = test._remote_answer.sdp.replace(
               /a=msid:[^\s]*/g,
               "a=msid:foo");
+        },
+        function PC_LOCAL_OVERRIDE_EXPECTED_STREAM_IDS(test) {
+          Object.keys(
+              test.pcLocal.expectedRemoteTrackInfoById).forEach(trackId => {
+                test.pcLocal.expectedRemoteTrackInfoById[trackId].streamId = "foo";
+              });
         }
     ]);
     test.setMediaConstraints([{video: true}, {video: true}],
                              [{video: true}, {video: true}]);
     test.run();
   });
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
@@ -43,17 +43,17 @@
       }
     ]);
 
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}],
                                    []);
-          return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+          return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ]
     );
 
     test.chain.append([
       function CHECK_ASSUMPTIONS2() {
         is(test.pcLocal.localMediaElements.length, 2,
            "pcLocal should have two media elements");
--- a/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
@@ -73,17 +73,17 @@ runNetworkTest(() => {
 
   addRenegotiation(test.chain,
     [
       function PC_LOCAL_ADD_SECOND_STREAM(test) {
         canvas2 = h2.createAndAppendElement('canvas', 'source_canvas2');
         h2.drawColor(canvas2, h2.blue);
         stream2 = canvas2.captureStream(0);
 
-        // can't use test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+        // can't use test.pcLocal.getAllUserMedia([{video: true}]);
         // because it doesn't let us substitute the capture stream
         test.pcLocal.attachLocalStream(stream2);
       }
     ]
   );
 
   test.chain.append([
     function FIND_REMOTE2_VIDEO() {
--- a/dom/webidl/MediaStream.webidl
+++ b/dom/webidl/MediaStream.webidl
@@ -38,14 +38,9 @@ interface MediaStream : EventTarget {
     MediaStream                clone ();
     readonly    attribute boolean      active;
                 attribute EventHandler onaddtrack;
     //             attribute EventHandler onremovetrack;
     readonly attribute double currentTime;
 
     [ChromeOnly, Throws]
     static Promise<long> countUnderlyingStreams();
-
-    // Webrtc allows the remote side to name a stream whatever it wants, and we
-    // need to surface this to content.
-    [ChromeOnly]
-    void assignId(DOMString id);
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/MediaStreamList.webidl
@@ -0,0 +1,11 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+[ChromeOnly]
+interface MediaStreamList {
+  getter MediaStream? (unsigned long index);
+  readonly attribute unsigned long length;
+};
--- a/dom/webidl/PeerConnectionImpl.webidl
+++ b/dom/webidl/PeerConnectionImpl.webidl
@@ -36,34 +36,38 @@ interface PeerConnectionImpl  {
 
   /* Stats call, calls either |onGetStatsSuccess| or |onGetStatsError| on our
      observer. (see the |PeerConnectionObserver| interface) */
   [Throws]
   void getStats(MediaStreamTrack? selector);
 
   /* Adds the tracks created by GetUserMedia */
   [Throws]
+  void addTrack(MediaStreamTrack track, MediaStream... streams);
+  [Throws]
   void removeTrack(MediaStreamTrack track);
   [Throws]
-  TransceiverImpl createTransceiverImpl(DOMString kind,
-                                        MediaStreamTrack? track);
-  [Throws]
-  boolean checkNegotiationNeeded();
-  [Throws]
-  void insertDTMF(TransceiverImpl transceiver, DOMString tones,
+  void insertDTMF(RTCRtpSender sender, DOMString tones,
                   optional unsigned long duration = 100,
                   optional unsigned long interToneGap = 70);
   [Throws]
   DOMString getDTMFToneBuffer(RTCRtpSender sender);
   [Throws]
-  void replaceTrackNoRenegotiation(TransceiverImpl transceiverImpl,
-                                   MediaStreamTrack? withTrack);
+  void replaceTrack(MediaStreamTrack thisTrack, MediaStreamTrack withTrack);
+  [Throws]
+  void setParameters(MediaStreamTrack track,
+                     optional RTCRtpParameters parameters);
+  [Throws]
+  RTCRtpParameters getParameters(MediaStreamTrack track);
   [Throws]
   void closeStreams();
 
+  sequence<MediaStream> getLocalStreams();
+  sequence<MediaStream> getRemoteStreams();
+
   void addRIDExtension(MediaStreamTrack recvTrack, unsigned short extensionId);
   void addRIDFilter(MediaStreamTrack recvTrack, DOMString rid);
 
   void enablePacketDump(unsigned long level,
                         mozPacketDumpType type,
                         boolean sending);
 
   void disablePacketDump(unsigned long level,
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -18,37 +18,37 @@ interface PeerConnectionObserver
   void onCreateAnswerError(unsigned long name, DOMString message);
   void onSetLocalDescriptionSuccess();
   void onSetRemoteDescriptionSuccess();
   void onSetLocalDescriptionError(unsigned long name, DOMString message);
   void onSetRemoteDescriptionError(unsigned long name, DOMString message);
   void onAddIceCandidateSuccess();
   void onAddIceCandidateError(unsigned long name, DOMString message);
   void onIceCandidate(unsigned short level, DOMString mid, DOMString candidate);
+  void onNegotiationNeeded();
 
   /* Stats callbacks */
   void onGetStatsSuccess(optional RTCStatsReportInternal report);
   void onGetStatsError(unsigned long name, DOMString message);
 
+  /* replaceTrack callbacks */
+  void onReplaceTrackSuccess();
+  void onReplaceTrackError(unsigned long name, DOMString message);
+
   /* Data channel callbacks */
   void notifyDataChannel(DataChannel channel);
 
   /* Notification of one of several types of state changed */
   void onStateChange(PCObserverStateType state);
 
   /* Changes to MediaStreamTracks */
+  void onAddStream(MediaStream stream);
   void onRemoveStream(MediaStream stream);
-  void onTrack(DOMString webrtcTrackId, sequence<DOMString> streamIds);
-
-  /* Transceiver management; called when setRemoteDescription causes a
-     transceiver to be created on the C++ side */
-  void onTransceiverNeeded(DOMString kind, TransceiverImpl transceiverImpl);
+  void onAddTrack(MediaStreamTrack track, sequence<MediaStream> streams);
+  void onRemoveTrack(MediaStreamTrack track);
 
   /* DTMF callback */
-  void onDTMFToneChange(MediaStreamTrack track, DOMString tone);
+  void onDTMFToneChange(DOMString trackId, DOMString tone);
 
   /* Packet dump callback */
   void onPacket(unsigned long level, mozPacketDumpType type, boolean sending,
                 ArrayBuffer packet);
-
-  /* Transceiver sync */
-  void syncTransceivers();
 };
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -123,22 +123,18 @@ interface RTCPeerConnection : EventTarge
   // because a track can be part of multiple streams, stream parameters
   // indicate which particular streams should be referenced in signaling
 
   RTCRtpSender addTrack(MediaStreamTrack track,
                         MediaStream stream,
                         MediaStream... moreStreams);
   void removeTrack(RTCRtpSender sender);
 
-  RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind,
-                                   optional RTCRtpTransceiverInit init);
-
   sequence<RTCRtpSender> getSenders();
   sequence<RTCRtpReceiver> getReceivers();
-  sequence<RTCRtpTransceiver> getTransceivers();
 
   [ChromeOnly]
   void mozAddRIDExtension(RTCRtpReceiver receiver, unsigned short extensionId);
   [ChromeOnly]
   void mozAddRIDFilter(RTCRtpReceiver receiver, DOMString rid);
   [ChromeOnly]
   void mozSetPacketCallback(mozPacketCallback callback);
   [ChromeOnly]
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -64,25 +64,16 @@ dictionary RTCRtpParameters {
   sequence<RTCRtpHeaderExtensionParameters> headerExtensions;
   RTCRtcpParameters                         rtcp;
   sequence<RTCRtpCodecParameters>           codecs;
 };
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/rtpsender;1"]
 interface RTCRtpSender {
-  readonly attribute MediaStreamTrack? track;
+  readonly attribute MediaStreamTrack track;
   Promise<void> setParameters (optional RTCRtpParameters parameters);
   RTCRtpParameters getParameters();
-  Promise<void> replaceTrack(MediaStreamTrack? withTrack);
+  Promise<void> replaceTrack(MediaStreamTrack track);
   Promise<RTCStatsReport> getStats();
   [Pref="media.peerconnection.dtmf.enabled"]
   readonly attribute RTCDTMFSender? dtmf;
-  // Ugh, can't use a ChromeOnly attibute sequence<MediaStream>...
-  [ChromeOnly]
-  sequence<MediaStream> getStreams();
-  [ChromeOnly]
-  void setStreams(sequence<MediaStream> streams);
-  [ChromeOnly]
-  void setTrack(MediaStreamTrack? track);
-  [ChromeOnly]
-  void checkWasCreatedByPc(RTCPeerConnection pc);
 };
deleted file mode 100644
--- a/dom/webidl/RTCRtpTransceiver.webidl
+++ /dev/null
@@ -1,75 +0,0 @@
-/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * The origin of this IDL file is
- * http://w3c.github.io/webrtc-pc/#rtcrtptransceiver-interface
- */
-
-enum RTCRtpTransceiverDirection {
-    "sendrecv",
-    "sendonly",
-    "recvonly",
-    "inactive"
-};
-
-dictionary RTCRtpTransceiverInit {
-    RTCRtpTransceiverDirection         direction = "sendrecv";
-    sequence<MediaStream>              streams = [];
-    // TODO: bug 1396918
-    // sequence<RTCRtpEncodingParameters> sendEncodings;
-};
-
-[Pref="media.peerconnection.enabled",
- JSImplementation="@mozilla.org/dom/rtptransceiver;1"]
-interface RTCRtpTransceiver {
-    readonly attribute DOMString?                  mid;
-    [SameObject]
-    readonly attribute RTCRtpSender                sender;
-    [SameObject]
-    readonly attribute RTCRtpReceiver              receiver;
-    readonly attribute boolean                     stopped;
-             attribute RTCRtpTransceiverDirection  direction;
-    readonly attribute RTCRtpTransceiverDirection? currentDirection;
-
-    // Mostly for testing
-    [Pref="media.peerconnection.remoteTrackId.enabled"]
-    readonly attribute DOMString?                  remoteTrackId;
-
-    void stop();
-    // TODO: bug 1396922
-    // void setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
-
-    [ChromeOnly]
-    void setRemoteTrackId(DOMString trackId);
-    [ChromeOnly]
-    void setAddTrackMagic();
-    [ChromeOnly]
-    readonly attribute boolean addTrackMagic;
-    [ChromeOnly]
-    void setCurrentDirection(RTCRtpTransceiverDirection direction);
-    [ChromeOnly]
-    void setDirectionInternal(RTCRtpTransceiverDirection direction);
-    [ChromeOnly]
-    void setMid(DOMString mid);
-    [ChromeOnly]
-    void unsetMid();
-    [ChromeOnly]
-    void setStopped();
-    [ChromeOnly]
-    void remove();
-
-    [ChromeOnly]
-    DOMString getKind();
-    [ChromeOnly]
-    boolean hasBeenUsedToSend();
-    [ChromeOnly]
-    void sync();
-
-    [ChromeOnly]
-    void insertDTMF(DOMString tones,
-                    optional unsigned long duration = 100,
-                    optional unsigned long interToneGap = 70);
-};
-
--- a/dom/webidl/RTCTrackEvent.webidl
+++ b/dom/webidl/RTCTrackEvent.webidl
@@ -6,24 +6,22 @@
  * The origin of this IDL file is
  * http://w3c.github.io/webrtc-pc/#idl-def-RTCTrackEvent
  */
 
 dictionary RTCTrackEventInit : EventInit {
     required RTCRtpReceiver        receiver;
     required MediaStreamTrack      track;
     sequence<MediaStream> streams = [];
-    required RTCRtpTransceiver     transceiver;
 };
 
 [Pref="media.peerconnection.enabled",
  Constructor(DOMString type, RTCTrackEventInit eventInitDict)]
 interface RTCTrackEvent : Event {
     readonly        attribute RTCRtpReceiver           receiver;
     readonly        attribute MediaStreamTrack         track;
 
 // TODO: Use FrozenArray once available. (Bug 1236777)
 //  readonly        attribute FrozenArray<MediaStream> streams;
 
     [Frozen, Cached, Pure]
     readonly        attribute sequence<MediaStream> streams; // workaround
-    readonly        attribute RTCRtpTransceiver transceiver;
 };
deleted file mode 100644
--- a/dom/webidl/TransceiverImpl.webidl
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/.
- *
- * PeerConnection.js' interface to the C++ TransceiverImpl.
- *
- * Do not confuse with RTCRtpTransceiver. This interface is purely for
- * communication between the PeerConnection JS DOM binding and the C++
- * implementation.
- *
- * See media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
- *
- */
-
-// Constructed by PeerConnectionImpl::CreateTransceiverImpl.
-[ChromeOnly]
-interface TransceiverImpl {
-  MediaStreamTrack getReceiveTrack();
-  [Throws]
-  void syncWithJS(RTCRtpTransceiver transceiver);
-};
-
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -200,16 +200,19 @@ with Files("MediaEncryptedEvent.webidl")
     BUG_COMPONENT = ("Core", "Audio/Video")
 
 with Files("MediaKey*"):
     BUG_COMPONENT = ("Core", "Audio/Video: Playback")
 
 with Files("Media*List*"):
     BUG_COMPONENT = ("Core", "CSS Parsing and Computation")
 
+with Files("MediaStreamList.webidl"):
+    BUG_COMPONENT = ("Core", "Web Audio")
+
 with Files("*Record*"):
     BUG_COMPONENT = ("Core", "Audio/Video: Recording")
 
 with Files("Media*Track*"):
     BUG_COMPONENT = ("Core", "WebRTC: Audio/Video")
 
 with Files("Mouse*"):
     BUG_COMPONENT = ("Core", "DOM: Events")
@@ -985,33 +988,32 @@ WEBIDL_FILES = [
     'XULDocument.webidl',
     'XULElement.webidl',
     'XULTemplateBuilder.webidl',
 ]
 
 if CONFIG['MOZ_WEBRTC']:
     WEBIDL_FILES += [
         'DataChannel.webidl',
+        'MediaStreamList.webidl',
         'PeerConnectionImpl.webidl',
         'PeerConnectionImplEnums.webidl',
         'PeerConnectionObserver.webidl',
         'PeerConnectionObserverEnums.webidl',
         'RTCCertificate.webidl',
         'RTCConfiguration.webidl',
         'RTCDTMFSender.webidl',
         'RTCIceCandidate.webidl',
         'RTCIdentityAssertion.webidl',
         'RTCIdentityProvider.webidl',
         'RTCPeerConnection.webidl',
         'RTCPeerConnectionStatic.webidl',
         'RTCRtpReceiver.webidl',
         'RTCRtpSender.webidl',
-        'RTCRtpTransceiver.webidl',
         'RTCSessionDescription.webidl',
-        'TransceiverImpl.webidl',
         'WebrtcDeprecated.webidl',
         'WebrtcGlobalInformation.webidl',
     ]
 
 if CONFIG['MOZ_WEBSPEECH']:
     WEBIDL_FILES += [
         'SpeechGrammar.webidl',
         'SpeechGrammarList.webidl',
--- a/media/mtransport/nricectx.cpp
+++ b/media/mtransport/nricectx.cpp
@@ -1038,31 +1038,18 @@ nsresult NrIceCtx::ParseGlobalAttributes
     MOZ_MTLOG(ML_ERROR, "Couldn't parse global attributes for "
               << name_ << "'");
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
-bool NrIceCtx::HasStreamsToConnect() const {
-  for (auto& stream : streams_) {
-    if (stream && stream->state() != NrIceMediaStream::ICE_CLOSED) {
-      return true;
-    }
-  }
-  return false;
-}
-
 nsresult NrIceCtx::StartChecks(bool offerer) {
   int r;
-  if (!HasStreamsToConnect()) {
-    // Nothing to do
-    return NS_OK;
-  }
 
   offerer_ = offerer;
   ice_start_time_ = TimeStamp::Now();
 
   r=nr_ice_peer_ctx_pair_candidates(peer_);
   if (r) {
     MOZ_MTLOG(ML_ERROR, "Couldn't pair candidates on "
               << name_ << "'");
--- a/media/mtransport/nricectx.h
+++ b/media/mtransport/nricectx.h
@@ -267,18 +267,16 @@ class NrIceCtx {
   }
 
   // Some might be null
   size_t GetStreamCount() const
   {
     return streams_.size();
   }
 
-  bool HasStreamsToConnect() const;
-
   // The name of the ctx
   const std::string& name() const { return name_; }
 
   // Get ufrag and password.
   std::string ufrag() const;
   std::string pwd() const;
 
   // Current state
--- a/media/mtransport/test/transport_unittests.cpp
+++ b/media/mtransport/test/transport_unittests.cpp
@@ -609,18 +609,18 @@ class TransportTestPeer : public sigslot
     ice_ctx_->ctx()->SetStream(streams_.size(), stream);
     streams_.push_back(stream);
 
     // Listen for candidates
     stream->SignalCandidate.
         connect(this, &TransportTestPeer::GotCandidate);
 
     // Create the transport layer
-    ice_ = new TransportLayerIce();
-    ice_->SetParameters(stream, 1);
+    ice_ = new TransportLayerIce(name);
+    ice_->SetParameters(ice_ctx_->ctx(), stream, 1);
 
     // Assemble the stack
     nsAutoPtr<std::queue<mozilla::TransportLayer *> > layers(
       new std::queue<mozilla::TransportLayer *>);
     layers->push(ice_);
     layers->push(dtls_);
 
     test_utils_->sts_target()->Dispatch(
--- a/media/mtransport/transportlayerice.cpp
+++ b/media/mtransport/transportlayerice.cpp
@@ -79,28 +79,30 @@ extern "C" {
 namespace mozilla {
 
 #ifdef ERROR
 #undef ERROR
 #endif
 
 MOZ_MTLOG_MODULE("mtransport")
 
-TransportLayerIce::TransportLayerIce()
-    : stream_(nullptr), component_(0),
+TransportLayerIce::TransportLayerIce(const std::string& name)
+    : name_(name),
+      ctx_(nullptr), stream_(nullptr), component_(0),
       old_stream_(nullptr)
 {
   // setup happens later
 }
 
 TransportLayerIce::~TransportLayerIce() {
   // No need to do anything here, since we use smart pointers
 }
 
-void TransportLayerIce::SetParameters(RefPtr<NrIceMediaStream> stream,
+void TransportLayerIce::SetParameters(RefPtr<NrIceCtx> ctx,
+                                      RefPtr<NrIceMediaStream> stream,
                                       int component) {
   // Stream could be null in the case of some badly written js that causes
   // us to be in an ICE restart case, but not have valid streams due to
   // not calling PeerConnectionMedia::EnsureTransports if
   // PeerConnectionImpl::SetSignalingState_m thinks the conditions were
   // not correct.  We also solved a case where an incoming answer was
   // incorrectly beginning an ICE restart when the offer did not indicate one.
   if (!stream) {
@@ -114,23 +116,26 @@ void TransportLayerIce::SetParameters(Re
   if (stream_ && !old_stream_ && (stream_ != stream)) {
     // Here we leave the old stream's signals connected until we don't need
     // it anymore.  They will be disconnected if ice restart is successful.
     old_stream_ = stream_;
     MOZ_MTLOG(ML_INFO, LAYER_INFO << "SetParameters save old stream("
                                   << old_stream_->name() << ")");
   }
 
+  ctx_ = ctx;
   stream_ = stream;
   component_ = component;
 
   PostSetup();
 }
 
 void TransportLayerIce::PostSetup() {
+  target_ = ctx_->thread();
+
   stream_->SignalReady.connect(this, &TransportLayerIce::IceReady);
   stream_->SignalFailed.connect(this, &TransportLayerIce::IceFailed);
   stream_->SignalPacketReceived.connect(this,
                                         &TransportLayerIce::IcePacketReceived);
   if (stream_->state() == NrIceMediaStream::ICE_OPEN) {
     TL_SET_STATE(TS_OPEN);
   }
 }
--- a/media/mtransport/transportlayerice.h
+++ b/media/mtransport/transportlayerice.h
@@ -25,21 +25,22 @@
 #include "transportflow.h"
 #include "transportlayer.h"
 
 // An ICE transport layer -- corresponds to a single ICE
 namespace mozilla {
 
 class TransportLayerIce : public TransportLayer {
  public:
-  TransportLayerIce();
+  explicit TransportLayerIce(const std::string& name);
 
   virtual ~TransportLayerIce();
 
-  void SetParameters(RefPtr<NrIceMediaStream> stream,
+  void SetParameters(RefPtr<NrIceCtx> ctx,
+                     RefPtr<NrIceMediaStream> stream,
                      int component);
 
   void ResetOldStream(); // called after successful ice restart
   void RestoreOldStream(); // called after unsuccessful ice restart
 
   // Transport layer overrides.
   TransportResult SendPacket(const unsigned char *data, size_t len) override;
 
@@ -51,16 +52,18 @@ class TransportLayerIce : public Transpo
                          const unsigned char *data, int len);
 
   TRANSPORT_LAYER_ID("ice")
 
  private:
   DISALLOW_COPY_ASSIGN(TransportLayerIce);
   void PostSetup();
 
+  const std::string name_;
+  RefPtr<NrIceCtx> ctx_;
   RefPtr<NrIceMediaStream> stream_;
   int component_;
 
   // used to hold the old stream
   RefPtr<NrIceMediaStream> old_stream_;
 };
 
 }  // close namespace
--- a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
@@ -123,158 +123,52 @@ protected:
   {
     tdata.iceCredentialSerial = 0;
     GenerateNewIceCredentials(session, tdata);
     session.SetIceCredentials(tdata.mIceUfrag, tdata.mIcePwd);
     AddDtlsFingerprint("sha-1", session, tdata);
     AddDtlsFingerprint("sha-256", session, tdata);
   }
 
-  void
-  CheckTransceiverInvariants(
-      const std::vector<RefPtr<JsepTransceiver>>& oldTransceivers,
-      const std::vector<RefPtr<JsepTransceiver>>& newTransceivers)
-  {
-    ASSERT_LE(oldTransceivers.size(), newTransceivers.size());
-    std::set<size_t> levels;
-
-    for (const RefPtr<JsepTransceiver>& newTransceiver : newTransceivers) {
-      if (newTransceiver->HasLevel()) {
-        ASSERT_FALSE(levels.count(newTransceiver->GetLevel()))
-                     << "Two new transceivers are mapped to level "
-                     << newTransceiver->GetLevel();
-        levels.insert(newTransceiver->GetLevel());
-      }
-    }
-
-    auto last = levels.rbegin();
-    if (last != levels.rend()) {
-      ASSERT_LE(*last, levels.size())
-          << "Max level observed in transceivers was " << *last
-          << ", but there are only " << levels.size() << " levels in the "
-          "transceivers.";
-    }
-
-    for (const RefPtr<JsepTransceiver>& oldTransceiver : oldTransceivers) {
-      if (oldTransceiver->HasLevel()) {
-        ASSERT_TRUE(levels.count(oldTransceiver->GetLevel()))
-                    << "Level " << oldTransceiver->GetLevel()
-                    << " had a transceiver in the old, but not the new (or, "
-                    "perhaps this level had more than one transceiver in the "
-                    "old)";
-        levels.erase(oldTransceiver->GetLevel());
-      }
-    }
-  }
-
-  std::vector<RefPtr<JsepTransceiver>>
-  DeepCopy(const std::vector<RefPtr<JsepTransceiver>>& transceivers)
-  {
-    std::vector<RefPtr<JsepTransceiver>> copy;
-    for (const RefPtr<JsepTransceiver>& transceiver : transceivers) {
-      copy.push_back(new JsepTransceiver(*transceiver));
-    }
-    return copy;
-  }
-
   std::string
   CreateOffer(const Maybe<JsepOfferOptions>& options = Nothing())
   {
-    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
-      DeepCopy(mSessionOff->GetTransceivers());
     JsepOfferOptions defaultOptions;
     const JsepOfferOptions& optionsRef = options ? *options : defaultOptions;
     std::string offer;
     nsresult rv;
     rv = mSessionOff->CreateOffer(optionsRef, &offer);
     EXPECT_EQ(NS_OK, rv) << mSessionOff->GetLastError();
 
     std::cerr << "OFFER: " << offer << std::endl;
 
     ValidateTransport(*mOffererTransport, offer);
 
-    if (transceiversBefore.size() != mSessionOff->GetTransceivers().size()) {
-      EXPECT_TRUE(false) << "CreateOffer changed number of transceivers!";
-      return offer;
-    }
-
-    CheckTransceiverInvariants(transceiversBefore,
-                               mSessionOff->GetTransceivers());
-
-    for (size_t i = 0; i < transceiversBefore.size(); ++i) {
-      RefPtr<JsepTransceiver>& oldTransceiver = transceiversBefore[i];
-      RefPtr<JsepTransceiver>& newTransceiver = mSessionOff->GetTransceivers()[i];
-      EXPECT_EQ(oldTransceiver->IsStopped(), newTransceiver->IsStopped());
-
-      if (oldTransceiver->IsStopped()) {
-        if (!newTransceiver->HasLevel()) {
-          // Tolerate unmapping of stopped transceivers by removing this
-          // difference.
-          oldTransceiver->ClearLevel();
-        }
-      } else if (!oldTransceiver->HasLevel()) {
-        EXPECT_TRUE(newTransceiver->HasLevel());
-        // Tolerate new mappings.
-        oldTransceiver->SetLevel(newTransceiver->GetLevel());
-      }
-
-      EXPECT_TRUE(Equals(*oldTransceiver, *newTransceiver));
-    }
-
     return offer;
   }
 
-  typedef enum {
-    NO_ADDTRACK_MAGIC,
-    ADDTRACK_MAGIC
-  } AddTrackMagic;
-
   void
-  AddTracks(JsepSessionImpl& side, AddTrackMagic magic = ADDTRACK_MAGIC)
+  AddTracks(JsepSessionImpl& side)
   {
     // Add tracks.
     if (types.empty()) {
       types = BuildTypes(GetParam());
     }
-    AddTracks(side, types, magic);
+    AddTracks(side, types);
+
+    // Now that we have added streams, we expect audio, then video, then
+    // application in the SDP, regardless of the order in which the streams were
+    // added.
+    std::sort(types.begin(), types.end());
   }
 
   void
-  AddTracks(JsepSessionImpl& side,
-            const std::string& mediatypes,
-            AddTrackMagic magic = ADDTRACK_MAGIC)
+  AddTracks(JsepSessionImpl& side, const std::string& mediatypes)
   {
-    AddTracks(side, BuildTypes(mediatypes), magic);
-  }
-
-  JsepTrack
-  RemoveTrack(JsepSession& side, size_t index) {
-    if (side.GetTransceivers().size() <= index) {
-      EXPECT_TRUE(false) << "Index " << index << " out of bounds!";
-      return JsepTrack(SdpMediaSection::kAudio, sdp::kSend);
-    }
-
-    RefPtr<JsepTransceiver>& transceiver(side.GetTransceivers()[index]);
-    JsepTrack& track = transceiver->mSendTrack;
-    EXPECT_FALSE(track.GetTrackId().empty()) << "No track at index " << index;
-
-    JsepTrack original(track);
-    track.ClearTrackIds();
-    transceiver->mJsDirection &= SdpDirectionAttribute::Direction::kRecvonly;
-    return original;
-  }
-
-  void
-  SetDirection(JsepSession& side,
-               size_t index,
-               SdpDirectionAttribute::Direction direction) {
-    ASSERT_LT(index, side.GetTransceivers().size())
-      << "Index " << index << " out of bounds!";
-
-    side.GetTransceivers()[index]->mJsDirection = direction;
+    AddTracks(side, BuildTypes(mediatypes));
   }
 
   std::vector<SdpMediaSection::MediaType>
   BuildTypes(const std::string& mediatypes)
   {
     std::vector<SdpMediaSection::MediaType> result;
     size_t ptr = 0;
 
@@ -299,177 +193,87 @@ protected:
       ptr = comma + 1;
     }
 
     return result;
   }
 
   void
   AddTracks(JsepSessionImpl& side,
-            const std::vector<SdpMediaSection::MediaType>& mediatypes,
-            AddTrackMagic magic = ADDTRACK_MAGIC)
+            const std::vector<SdpMediaSection::MediaType>& mediatypes)
   {
     FakeUuidGenerator uuid_gen;
     std::string stream_id;
     std::string track_id;
 
     ASSERT_TRUE(uuid_gen.Generate(&stream_id));
 
-    AddTracksToStream(side, stream_id, mediatypes, magic);
+    AddTracksToStream(side, stream_id, mediatypes);
   }
 
   void
   AddTracksToStream(JsepSessionImpl& side,
                     const std::string stream_id,
-                    const std::string& mediatypes,
-                    AddTrackMagic magic = ADDTRACK_MAGIC)
+                    const std::string& mediatypes)
   {
-    AddTracksToStream(side, stream_id, BuildTypes(mediatypes), magic);
-  }
-
-  // A bit of a hack. JsepSessionImpl populates the track-id automatically, just
-  // in case, because the w3c spec requires msid to be set even when there's no
-  // send track.
-  bool IsNull(const JsepTrack& track) const {
-    return track.GetStreamIds().empty() &&
-           (track.GetMediaType() != SdpMediaSection::MediaType::kApplication);
+    AddTracksToStream(side, stream_id, BuildTypes(mediatypes));
   }
 
   void
   AddTracksToStream(JsepSessionImpl& side,
                     const std::string stream_id,
-                    const std::vector<SdpMediaSection::MediaType>& mediatypes,
-                    AddTrackMagic magic = ADDTRACK_MAGIC)
+                    const std::vector<SdpMediaSection::MediaType>& mediatypes)
 
   {
     FakeUuidGenerator uuid_gen;
     std::string track_id;
 
-    for (auto type : mediatypes) {
+    for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) {
       ASSERT_TRUE(uuid_gen.Generate(&track_id));
 
-      std::vector<RefPtr<JsepTransceiver>>& transceivers(side.GetTransceivers());
-      size_t i = transceivers.size();
-      if (magic == ADDTRACK_MAGIC) {
-        for (i = 0; i < transceivers.size(); ++i) {
-          if (transceivers[i]->mSendTrack.GetMediaType() != type) {
-            continue;
-          }
-
-          if (IsNull(transceivers[i]->mSendTrack) ||
-              type == SdpMediaSection::MediaType::kApplication) {
-            break;
-          }
-        }
-      }
-
-      if (i == transceivers.size()) {
-        side.AddTransceiver(new JsepTransceiver(type));
-        MOZ_ASSERT(i < transceivers.size());
-      }
-
-      std::cerr << "Updating send track for transceiver " << i << std::endl;
-      if (magic == ADDTRACK_MAGIC) {
-        transceivers[i]->SetAddTrackMagic();
-      }
-      transceivers[i]->mJsDirection |=
-        SdpDirectionAttribute::Direction::kSendonly;
-      transceivers[i]->mSendTrack.UpdateTrackIds(
-          std::vector<std::string>(1, stream_id), track_id);
+      RefPtr<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
+      side.AddTrack(mst);
     }
   }
 
-  bool HasMediaStream(const std::vector<JsepTrack>& tracks) const {
-    for (const auto& track : tracks) {
-      if (track.GetMediaType() != SdpMediaSection::kApplication) {
-        return true;
+  bool HasMediaStream(std::vector<RefPtr<JsepTrack>> tracks) const {
+    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
+      if ((*i)->GetMediaType() != SdpMediaSection::kApplication) {
+        return 1;
       }
     }
-    return false;
+    return 0;
   }
 
   const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const {
-    auto tracks = GetLocalTracks(side);
-    return tracks.begin()->GetStreamIds()[0];
-  }
-
-  std::vector<JsepTrack>
-  GetLocalTracks(const JsepSession& session) const {
-    std::vector<JsepTrack> result;
-    for (const auto& transceiver : session.GetTransceivers()) {
-      if (!IsNull(transceiver->mSendTrack)) {
-        result.push_back(transceiver->mSendTrack);
-      }
-    }
-    return result;
-  }
-
-  std::vector<JsepTrack>
-  GetRemoteTracks(const JsepSession& session) const {
-    std::vector<JsepTrack> result;
-    for (const auto& transceiver : session.GetTransceivers()) {
-      if (!IsNull(transceiver->mRecvTrack)) {
-        result.push_back(transceiver->mRecvTrack);
-      }
-    }
-    return result;
-  }
-
-  JsepTransceiver*
-  GetDatachannelTransceiver(JsepSession& side) {
-    for (const auto& transceiver : side.GetTransceivers()) {
-      if (transceiver->mSendTrack.GetMediaType() ==
-            SdpMediaSection::MediaType::kApplication) {
-        return transceiver.get();
-      }
-    }
-
-    return nullptr;
-  }
-
-  JsepTransceiver*
-  GetNegotiatedTransceiver(JsepSession& side, size_t index) {
-    for (RefPtr<JsepTransceiver>& transceiver : side.GetTransceivers()) {
-      if (transceiver->mSendTrack.GetNegotiatedDetails() ||
-          transceiver->mRecvTrack.GetNegotiatedDetails()) {
-        if (index) {
-          --index;
-          continue;
-        }
-
-        return transceiver.get();
-      }
-    }
-
-    return nullptr;
+    auto tracks = side.GetLocalTracks();
+    return (*tracks.begin())->GetStreamId();
   }
 
   std::vector<std::string>
-  GetMediaStreamIds(const std::vector<JsepTrack>& tracks) const {
+  GetMediaStreamIds(std::vector<RefPtr<JsepTrack>> tracks) const {
     std::vector<std::string> ids;
-    for (const auto& track : tracks) {
+    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
       // data channels don't have msid's
-      if (track.GetMediaType() == SdpMediaSection::kApplication) {
+      if ((*i)->GetMediaType() == SdpMediaSection::kApplication) {
         continue;
       }
-      ids.insert(ids.end(),
-                 track.GetStreamIds().begin(),
-                 track.GetStreamIds().end());
+      ids.push_back((*i)->GetStreamId());
     }
     return ids;
   }
 
   std::vector<std::string>
   GetLocalMediaStreamIds(JsepSessionImpl& side) const {
-    return GetMediaStreamIds(GetLocalTracks(side));
+    return GetMediaStreamIds(side.GetLocalTracks());
   }
 
   std::vector<std::string>
   GetRemoteMediaStreamIds(JsepSessionImpl& side) const {
-    return GetMediaStreamIds(GetRemoteTracks(side));
+    return GetMediaStreamIds(side.GetRemoteTracks());
   }
 
   std::vector<std::string>
   sortUniqueStrVector(std::vector<std::string> in) const {
     std::sort(in.begin(), in.end());
     auto it = std::unique(in.begin(), in.end());
     in.resize( std::distance(in.begin(), it));
     return in;
@@ -480,49 +284,59 @@ protected:
     return sortUniqueStrVector(GetLocalMediaStreamIds(side));
   }
 
   std::vector<std::string>
   GetRemoteUniqueStreamIds(JsepSessionImpl& side) const {
     return sortUniqueStrVector(GetRemoteMediaStreamIds(side));
   }
 
-  JsepTrack GetTrack(JsepSessionImpl& side,
-                     SdpMediaSection::MediaType type,
-                     size_t index) const {
-    for (const auto& transceiver : side.GetTransceivers()) {
-      if (IsNull(transceiver->mSendTrack) ||
-          transceiver->mSendTrack.GetMediaType() != type) {
+  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 transceiver->mSendTrack;
+      return *i;
     }
 
-    return JsepTrack(type, sdp::kSend);
+    return RefPtr<JsepTrack>(nullptr);
   }
 
-  JsepTrack GetTrackOff(size_t index, SdpMediaSection::MediaType type) {
+  RefPtr<JsepTrack> GetTrackOff(size_t index,
+                                SdpMediaSection::MediaType type) {
     return GetTrack(*mSessionOff, type, index);
   }
 
-  JsepTrack GetTrackAns(size_t index, SdpMediaSection::MediaType type) {
+  RefPtr<JsepTrack> GetTrackAns(size_t index,
+                                SdpMediaSection::MediaType type) {
     return GetTrack(*mSessionAns, type, index);
   }
 
-  size_t CountRtpTypes() const {
-    return std::count_if(
-        types.begin(), types.end(),
-        [](SdpMediaSection::MediaType type)
-          {return type != SdpMediaSection::MediaType::kApplication;});
+  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 SdpFingerprintAttributeList::Fingerprint& f1,
               const SdpFingerprintAttributeList::Fingerprint& f2) const {
     if (f1.hashFunc != f2.hashFunc) {
       return false;
     }
 
@@ -586,143 +400,75 @@ protected:
 
     if (t1->GetPassword() != t2->GetPassword()) {
       return false;
     }
 
     return true;
   }
 
-  bool Equals(const JsepTransport& t1,
-              const JsepTransport& t2) const {
-    if (t1.mTransportId != t2.mTransportId) {
-      std::cerr << "Transport id differs: " << t1.mTransportId << " vs "
-                << t2.mTransportId << std::endl;
-      return false;
+  bool Equals(const RefPtr<JsepTransport>& t1,
+              const RefPtr<JsepTransport>& t2) const {
+    if (!t1 && !t2) {
+      return true;
     }
 
-    if (t1.mComponents != t2.mComponents) {
-      std::cerr << "Component count differs" << std::endl;
-      return false;
-    }
-
-    if (!Equals(t1.mIce, t2.mIce)) {
-      std::cerr << "ICE differs" << std::endl;
+    if (!t1 || !t2) {
       return false;
     }
 
-    return true;
-  }
-
-  bool Equals(const JsepTrack& t1, const JsepTrack& t2) const {
-    if (t1.GetMediaType() != t2.GetMediaType()) {
-      return false;
-    }
-
-    if (t1.GetDirection() != t2.GetDirection()) {
-      return false;
-    }
-
-    if (t1.GetStreamIds() != t2.GetStreamIds()) {
+    if (t1->mTransportId != t2->mTransportId) {
       return false;
     }
 
-    if (t1.GetTrackId() != t2.GetTrackId()) {
-      return false;
-    }
-
-    if (t1.GetActive() != t2.GetActive()) {
+    if (t1->mComponents != t2->mComponents) {
       return false;
     }
 
-    if (t1.GetCNAME() != t2.GetCNAME()) {
-      return false;
-    }
-
-    if (t1.GetSsrcs() != t2.GetSsrcs()) {
+    if (!Equals(t1->mIce, t2->mIce)) {
       return false;
     }
 
     return true;
   }
 
-  bool Equals(const JsepTransceiver& p1,
-              const JsepTransceiver& p2) const {
-    if (p1.HasLevel() != p2.HasLevel()) {
-      std::cerr << "One transceiver has a level, the other doesn't"
-                << std::endl;
-      return false;
-    }
-
-    if (p1.HasLevel() && (p1.GetLevel() != p2.GetLevel())) {
-      std::cerr << "Level differs: " << p1.GetLevel() << " vs " << p2.GetLevel()
-                << std::endl;
+  bool Equals(const JsepTrackPair& p1,
+              const JsepTrackPair& p2) const {
+    if (p1.mLevel != p2.mLevel) {
       return false;
     }
 
     // We don't check things like BundleLevel(), since that can change without
     // any changes to the transport, which is what we're really interested in.
 
-    if (p1.IsStopped() != p2.IsStopped()) {
-      std::cerr << "One transceiver is stopped, the other is not" << std::endl;
-      return false;
-    }
-
-    if (p1.IsAssociated() != p2.IsAssociated()) {
-      std::cerr << "One transceiver has a mid, the other doesn't"
-                << std::endl;
+    if (p1.mSending.get() != p2.mSending.get()) {
       return false;
     }
 
-    if (p1.IsAssociated() && (p1.GetMid() != p2.GetMid())) {
-      std::cerr << "mid differs: " << p1.GetMid() << " vs " << p2.GetMid()
-                << std::endl;
-      return false;
-    }
-
-    if (!Equals(p1.mSendTrack, p2.mSendTrack)) {
-      std::cerr << "Send track differs" << std::endl;
+    if (p1.mReceiving.get() != p2.mReceiving.get()) {
       return false;
     }
 
-    if (!Equals(p1.mRecvTrack, p2.mRecvTrack)) {
-      std::cerr << "Receive track differs" << std::endl;
-      return false;
-    }
-
-    if (!Equals(p1.mTransport, p2.mTransport)) {
-      std::cerr << "Transport differs" << std::endl;
+    if (!Equals(p1.mRtpTransport, p2.mRtpTransport)) {
       return false;
     }
 
-    return true;
-  }
-
-  bool Equals(const std::vector<RefPtr<JsepTransceiver>>& t1,
-              const std::vector<RefPtr<JsepTransceiver>>& t2) const {
-    if (t1.size() != t2.size()) {
-      std::cerr << "Size differs: t1.size = " << t1.size() << ", t2.size = "
-                << t2.size() << std::endl;
+    if (!Equals(p1.mRtcpTransport, p2.mRtcpTransport)) {
       return false;
     }
 
-    for (size_t i = 0; i < t1.size(); ++i) {
-      if (!Equals(*t1[i], *t2[i])) {
-        return false;
-      }
-    }
-
     return true;
   }
 
   size_t GetTrackCount(JsepSessionImpl& side,
                        SdpMediaSection::MediaType type) const {
+    auto tracks = side.GetLocalTracks();
     size_t result = 0;
-    for (const auto& track : GetLocalTracks(side)) {
-      if (track.GetMediaType() == type) {
+    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
+      if ((*i)->GetMediaType() == type) {
         ++result;
       }
     }
     return result;
   }
 
   UniquePtr<Sdp> GetParsedLocalDescription(const JsepSessionImpl& side) const {
     return Parse(side.GetLocalDescription(kJsepDescriptionCurrent));
@@ -771,53 +517,52 @@ protected:
       }
     }
   }
 
   void
   EnsureNegotiationFailure(SdpMediaSection::MediaType type,
                            const std::string& codecName)
   {
-    for (auto* codec : mSessionOff->Codecs()) {
+    for (auto i = mSessionOff->Codecs().begin(); i != mSessionOff->Codecs().end();
+         ++i) {
+      auto* codec = *i;
       if (codec->mType == type && codec->mName != codecName) {
         codec->mEnabled = false;
       }
     }
 
-    for (auto* codec : mSessionAns->Codecs()) {
+    for (auto i = mSessionAns->Codecs().begin(); i != mSessionAns->Codecs().end();
+         ++i) {
+      auto* codec = *i;
       if (codec->mType == type && codec->mName == codecName) {
         codec->mEnabled = false;
       }
     }
   }
 
   std::string
   CreateAnswer()
   {
-    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
-      DeepCopy(mSessionAns->GetTransceivers());
-
     JsepAnswerOptions options;
     std::string answer;
 
     // detect ice restart and generate new ice credentials (like
     // PeerConnectionImpl does).
     if (mSessionAns->RemoteIceIsRestarting()) {
       GenerateNewIceCredentials(*mSessionAns, *mAnswererTransport);
       mSessionAns->SetIceCredentials(mAnswererTransport->mIceUfrag,
                                      mAnswererTransport->mIcePwd);
     }
     nsresult rv = mSessionAns->CreateAnswer(options, &answer);
     EXPECT_EQ(NS_OK, rv);
 
     std::cerr << "ANSWER: " << answer << std::endl;
 
     ValidateTransport(*mAnswererTransport, answer);
-    CheckTransceiverInvariants(transceiversBefore,
-                               mSessionAns->GetTransceivers());
 
     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;
@@ -831,187 +576,154 @@ protected:
     std::string answer = CreateAnswer();
     SetLocalAnswer(answer, checkFlags);
     SetRemoteAnswer(answer, checkFlags);
   }
 
   void
   SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
   {
-    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
-      DeepCopy(mSessionOff->GetTransceivers());
-
     nsresult rv = mSessionOff->SetLocalDescription(kJsepSdpOffer, offer);
 
-    CheckTransceiverInvariants(transceiversBefore,
-                               mSessionOff->GetTransceivers());
-
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
     if (checkFlags & CHECK_TRACKS) {
-      // This assumes no recvonly or inactive transceivers.
-      ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
-      for (const auto& transceiver : mSessionOff->GetTransceivers()) {
-        if (!transceiver->HasLevel()) {
-          continue;
-        }
-        const auto& track(transceiver->mSendTrack);
-        size_t level = transceiver->GetLevel();
-        ASSERT_FALSE(IsNull(track));
-        ASSERT_EQ(types[level], track.GetMediaType());
-        if (track.GetMediaType() != SdpMediaSection::kApplication) {
+      // Check that the transports exist.
+      ASSERT_EQ(types.size(), mSessionOff->GetTransports().size());
+      auto tracks = mSessionOff->GetLocalTracks();
+      for (size_t i = 0; i < types.size(); ++i) {
+        ASSERT_NE("", tracks[i]->GetStreamId());
+        ASSERT_NE("", tracks[i]->GetTrackId());
+        if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += track.GetStreamIds()[0];
+          msidAttr += tracks[i]->GetStreamId();
           msidAttr += " ";
-          msidAttr += track.GetTrackId();
+          msidAttr += tracks[i]->GetTrackId();
           ASSERT_NE(std::string::npos, offer.find(msidAttr))
             << "Did not find " << msidAttr << " in offer";
         }
       }
       if (types.size() == 1 &&
-          types[0] == SdpMediaSection::kApplication) {
+          tracks[0]->GetMediaType() == SdpMediaSection::kApplication) {
         ASSERT_EQ(std::string::npos, offer.find("a=ssrc"))
           << "Data channel should not contain SSRC";
       }
     }
   }
 
   void
   SetRemoteOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
   {
-    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
-      DeepCopy(mSessionAns->GetTransceivers());
-
     nsresult rv = mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer);
 
-    CheckTransceiverInvariants(transceiversBefore,
-                               mSessionAns->GetTransceivers());
-
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
     if (checkFlags & CHECK_TRACKS) {
-      // This assumes no recvonly or inactive transceivers.
-      ASSERT_EQ(types.size(), mSessionAns->GetTransceivers().size());
-      for (const auto& transceiver : mSessionAns->GetTransceivers()) {
-        if (!transceiver->HasLevel()) {
-          continue;
-        }
-        const auto& track(transceiver->mRecvTrack);
-        size_t level = transceiver->GetLevel();
-        ASSERT_FALSE(IsNull(track));
-        ASSERT_EQ(types[level], track.GetMediaType());
-        if (track.GetMediaType() != SdpMediaSection::kApplication) {
+      auto tracks = mSessionAns->GetRemoteTracks();
+      // Now verify that the right stuff is in the tracks.
+      ASSERT_EQ(types.size(), tracks.size());
+      for (size_t i = 0; i < tracks.size(); ++i) {
+        ASSERT_EQ(types[i], tracks[i]->GetMediaType());
+        ASSERT_NE("", tracks[i]->GetStreamId());
+        ASSERT_NE("", tracks[i]->GetTrackId());
+        if (tracks[i]->GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += track.GetStreamIds()[0];
+          msidAttr += tracks[i]->GetStreamId();
           msidAttr += " ";
-          msidAttr += track.GetTrackId();
+          msidAttr += tracks[i]->GetTrackId();
           ASSERT_NE(std::string::npos, offer.find(msidAttr))
             << "Did not find " << msidAttr << " in offer";
         }
       }
     }
   }
 
   void
   SetLocalAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
   {
-    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
-      DeepCopy(mSessionAns->GetTransceivers());
-
     nsresult rv = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer);
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
-    CheckTransceiverInvariants(transceiversBefore,
-                               mSessionAns->GetTransceivers());
-
     if (checkFlags & CHECK_TRACKS) {
       // Verify that the right stuff is in the tracks.
-      ASSERT_EQ(types.size(), mSessionAns->GetTransceivers().size());
-      for (const auto& transceiver : mSessionAns->GetTransceivers()) {
-        if (!transceiver->HasLevel()) {
-          continue;
-        }
-        const auto& sendTrack(transceiver->mSendTrack);
-        const auto& recvTrack(transceiver->mRecvTrack);
-        size_t level = transceiver->GetLevel();
-        ASSERT_FALSE(IsNull(sendTrack));
-        ASSERT_EQ(types[level], sendTrack.GetMediaType());
+      auto pairs = mSessionAns->GetNegotiatedTrackPairs();
+      ASSERT_EQ(types.size(), pairs.size());
+      for (size_t i = 0; i < types.size(); ++i) {
+        ASSERT_TRUE(pairs[i].mSending);
+        ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType());
+        ASSERT_TRUE(pairs[i].mReceiving);
+        ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType());
+        ASSERT_NE("", pairs[i].mSending->GetStreamId());
+        ASSERT_NE("", pairs[i].mSending->GetTrackId());
         // These might have been in the SDP, or might have been randomly
         // chosen by JsepSessionImpl
-        ASSERT_FALSE(IsNull(recvTrack));
-        ASSERT_EQ(types[level], recvTrack.GetMediaType());
-
-        if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
+        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
+        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
+
+        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += sendTrack.GetStreamIds()[0];
+          msidAttr += pairs[i].mSending->GetStreamId();
           msidAttr += " ";
-          msidAttr += sendTrack.GetTrackId();
+          msidAttr += pairs[i].mSending->GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
-            << "Did not find " << msidAttr << " in answer";
+            << "Did not find " << msidAttr << " in offer";
         }
       }
       if (types.size() == 1 &&
-          types[0] == SdpMediaSection::kApplication) {
+          pairs[0].mReceiving->GetMediaType() == SdpMediaSection::kApplication) {
         ASSERT_EQ(std::string::npos, answer.find("a=ssrc"))
           << "Data channel should not contain SSRC";
       }
     }
-    std::cerr << "Answerer transceivers:" << std::endl;
-    DumpTransceivers(*mSessionAns);
+    std::cerr << "OFFER pairs:" << std::endl;
+    DumpTrackPairs(*mSessionOff);
   }
 
   void
   SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
   {
-    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
-      DeepCopy(mSessionOff->GetTransceivers());
-
     nsresult rv = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer);
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
-    CheckTransceiverInvariants(transceiversBefore,
-                               mSessionOff->GetTransceivers());
-
     if (checkFlags & CHECK_TRACKS) {
       // Verify that the right stuff is in the tracks.
-      ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
-      for (const auto& transceiver : mSessionOff->GetTransceivers()) {
-        if (!transceiver->HasLevel()) {
-          continue;
-        }
-        const auto& sendTrack(transceiver->mSendTrack);
-        const auto& recvTrack(transceiver->mRecvTrack);
-        size_t level = transceiver->GetLevel();
-        ASSERT_FALSE(IsNull(sendTrack));
-        ASSERT_EQ(types[level], sendTrack.GetMediaType());
+      auto pairs = mSessionOff->GetNegotiatedTrackPairs();
+      ASSERT_EQ(types.size(), pairs.size());
+      for (size_t i = 0; i < types.size(); ++i) {
+        ASSERT_TRUE(pairs[i].mSending);
+        ASSERT_EQ(types[i], pairs[i].mSending->GetMediaType());
+        ASSERT_TRUE(pairs[i].mReceiving);
+        ASSERT_EQ(types[i], pairs[i].mReceiving->GetMediaType());
+        ASSERT_NE("", pairs[i].mSending->GetStreamId());
+        ASSERT_NE("", pairs[i].mSending->GetTrackId());
         // These might have been in the SDP, or might have been randomly
         // chosen by JsepSessionImpl
-        ASSERT_FALSE(IsNull(recvTrack));
-        ASSERT_EQ(types[level], recvTrack.GetMediaType());
-
-        if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
+        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
+        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
+
+        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += recvTrack.GetStreamIds()[0];
+          msidAttr += pairs[i].mReceiving->GetStreamId();
           msidAttr += " ";
-          msidAttr += recvTrack.GetTrackId();
+          msidAttr += pairs[i].mReceiving->GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
             << "Did not find " << msidAttr << " in answer";
         }
       }
     }
-    std::cerr << "Offerer transceivers:" << std::endl;
-    DumpTransceivers(*mSessionOff);
+    std::cerr << "ANSWER pairs:" << std::endl;
+    DumpTrackPairs(*mSessionAns);
   }
 
   typedef enum {
     RTP = 1,
     RTCP = 2
   } ComponentType;
 
   class CandidateSet {
@@ -1251,22 +963,23 @@ protected:
         << context << " (level " << msection.GetLevel() << ")";
     } else {
       ASSERT_FALSE(msection.GetAttributeList().HasAttribute(
             SdpAttribute::kEndOfCandidatesAttribute))
         << context << " (level " << msection.GetLevel() << ")";
     }
   }
 
-  void CheckTransceiversAreBundled(const JsepSession& session,
-                                   const std::string& context)
+  void CheckPairs(const JsepSession& session, const std::string& context)
   {
-    for (const auto& transceiver : session.GetTransceivers()) {
-      ASSERT_TRUE(transceiver->HasBundleLevel()) << context;
-      ASSERT_EQ(0U, transceiver->BundleLevel()) << context;
+    auto pairs = session.GetNegotiatedTrackPairs();
+
+    for (JsepTrackPair& pair : pairs) {
+      ASSERT_TRUE(pair.HasBundleLevel()) << context;
+      ASSERT_EQ(0U, pair.BundleLevel()) << context;
     }
   }
 
   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
@@ -1348,19 +1061,16 @@ protected:
     } else {
       // Not that we would have any test which tests this...
       ASSERT_EQ("19", msection->GetFormats()[0]);
       const SdpRtpmapAttributeList::Rtpmap* rtpmap(msection->FindRtpmap("19"));
       ASSERT_TRUE(rtpmap);
       ASSERT_EQ("19", rtpmap->pt);
       ASSERT_EQ("reserved", rtpmap->name);
     }
-
-    ASSERT_FALSE(msection->GetAttributeList().HasAttribute(
-          SdpAttribute::kMsidAttribute));
   }
 
   void
   ValidateSetupAttribute(const JsepSessionImpl& side,
                          const SdpSetupAttribute::Role expectedRole)
   {
     auto sdp = GetParsedLocalDescription(side);
     for (size_t i = 0; sdp && i < sdp->GetMediaSectionCount(); ++i) {
@@ -1371,22 +1081,17 @@ protected:
       }
     }
   }
 
   void
   DumpTrack(const JsepTrack& track)
   {
     const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
-    std::cerr << "  type=" << track.GetMediaType() << " track-id="
-              << track.GetTrackId() << std::endl;
-    if (!details) {
-      std::cerr << "  not negotiated" << std::endl;
-      return;
-    }
+    std::cerr << "  type=" << track.GetMediaType() << std::endl;
     std::cerr << "  encodings=" << std::endl;
     for (size_t i = 0; i < details->GetEncodingCount(); ++i) {
       const JsepTrackEncoding& encoding = details->GetEncoding(i);
       std::cerr << "    id=" << encoding.mRid << std::endl;
       for (const JsepCodecDescription* codec : encoding.GetCodecs()) {
         std::cerr << "      " << codec->mName
                   << " enabled(" << (codec->mEnabled?"yes":"no") << ")";
         if (track.GetMediaType() == SdpMediaSection::kAudio) {
@@ -1395,36 +1100,28 @@ protected:
           std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled?"yes":"no") << ")";
         }
         std::cerr << std::endl;
       }
     }
   }
 
   void
-  DumpTransceivers(const JsepSessionImpl& session)
+  DumpTrackPairs(const JsepSessionImpl& session)
   {
-    for (const auto& transceiver : mSessionAns->GetTransceivers()) {
-      std::cerr << "Transceiver ";
-      if (transceiver->HasLevel()) {
-        std::cerr << transceiver->GetLevel() << std::endl;
-      } else {
-        std::cerr << "<NO LEVEL>" << std::endl;
+    auto pairs = mSessionAns->GetNegotiatedTrackPairs();
+    for (auto i = pairs.begin(); i != pairs.end(); ++i) {
+      std::cerr << "Track pair " << i->mLevel << std::endl;
+      if (i->mSending) {
+        std::cerr << "Sending-->" << std::endl;
+        DumpTrack(*i->mSending);
       }
-      if (transceiver->HasBundleLevel()) {
-        std::cerr << "(bundle level is " << transceiver->BundleLevel() << ")"
-                  << std::endl;
-      }
-      if (!IsNull(transceiver->mSendTrack)) {
-        std::cerr << "Sending-->" << std::endl;
-        DumpTrack(transceiver->mSendTrack);
-      }
-      if (!IsNull(transceiver->mRecvTrack)) {
+      if (i->mReceiving) {
         std::cerr << "Receiving-->" << std::endl;
-        DumpTrack(transceiver->mRecvTrack);
+        DumpTrack(*i->mReceiving);
       }
     }
   }
 
   UniquePtr<Sdp>
   Parse(const std::string& sdp) const
   {
     SipccSdpParser parser;
@@ -1636,36 +1333,34 @@ TEST_P(JsepSessionTest, RenegotiationNoC
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
-  ASSERT_EQ(CountRtpTypes(), added.size());
+  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(CountRtpTypes(), added.size());
+  ASSERT_EQ(types.size(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  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());
@@ -1678,21 +1373,28 @@ TEST_P(JsepSessionTest, RenegotiationNoC
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  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]));
+  }
 }
 
 // Disabled: See Bug 1329028
 TEST_P(JsepSessionTest, DISABLED_RenegotiationSwappedRolesNoChange)
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
@@ -1711,18 +1413,18 @@ TEST_P(JsepSessionTest, DISABLED_Renegot
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(types.size(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererTransceivers = DeepCopy(mSessionOff->GetTransceivers());
-  auto answererTransceivers = DeepCopy(mSessionAns->GetTransceivers());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
 
   SwapOfferAnswerRoles();
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
@@ -1737,102 +1439,108 @@ TEST_P(JsepSessionTest, DISABLED_Renegot
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kPassive);
 
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_TRUE(Equals(offererTransceivers, newAnswererTransceivers));
-  ASSERT_TRUE(Equals(answererTransceivers, newOffererTransceivers));
+  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
+
+  ASSERT_EQ(offererPairs.size(), newAnswererPairs.size());
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(offererPairs[i], newAnswererPairs[i]));
+  }
+
+  ASSERT_EQ(answererPairs.size(), newOffererPairs.size());
+  for (size_t i = 0; i < answererPairs.size(); ++i) {
+    ASSERT_TRUE(Equals(answererPairs[i], newOffererPairs[i]));
+  }
 }
 
 
 TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  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);
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
   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());
+  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 newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_LE(2U, newOffererTransceivers.size());
-  newOffererTransceivers.resize(newOffererTransceivers.size() - 2);
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-
-  ASSERT_LE(2U, newAnswererTransceivers.size());
-  newAnswererTransceivers.resize(newAnswererTransceivers.size() - 2);
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  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();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  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
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kAudio, SdpDirectionAttribute::Direction::kRecvonly));
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kVideo, SdpDirectionAttribute::Direction::kRecvonly));
-
-  std::string offer = CreateOffer();
+  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);
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
@@ -1842,45 +1550,45 @@ TEST_P(JsepSessionTest, RenegotiationAns
   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 newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_LE(2U, newOffererTransceivers.size());
-  newOffererTransceivers.resize(newOffererTransceivers.size() - 2);
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-
-  ASSERT_LE(2U, newAnswererTransceivers.size());
-  newAnswererTransceivers.resize(newAnswererTransceivers.size() - 2);
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  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();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  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());
 
@@ -1888,533 +1596,591 @@ TEST_P(JsepSessionTest, RenegotiationBot
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
   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());
+  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 newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_LE(2U, newOffererTransceivers.size());
-  newOffererTransceivers.resize(newOffererTransceivers.size() - 2);
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-
-  ASSERT_LE(2U, newAnswererTransceivers.size());
-  newAnswererTransceivers.resize(newAnswererTransceivers.size() - 2);
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  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, RenegotiationBothAddTracksToExistingStream)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   if (GetParam() == "datachannel") {
     return;
   }
 
   OfferAnswer();
 
-  auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
-  auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
+  auto oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
+  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
   ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty());
 
   auto firstOffId = GetFirstLocalStreamId(*mSessionOff);
   auto firstAnsId = GetFirstLocalStreamId(*mSessionAns);
 
-  auto offererTransceivers = DeepCopy(mSessionOff->GetTransceivers());
-  auto answererTransceivers = DeepCopy(mSessionAns->GetTransceivers());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(SdpMediaSection::kAudio);
   extraTypes.push_back(SdpMediaSection::kVideo);
   AddTracksToStream(*mSessionOff, firstOffId, extraTypes);
   AddTracksToStream(*mSessionAns, firstAnsId, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
   OfferAnswer(CHECK_SUCCESS);
 
-  oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
-  aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
+  oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
+  aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
 
   ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty());
   if (oHasStream) {
     ASSERT_STREQ(firstOffId.c_str(),
                  GetFirstLocalStreamId(*mSessionOff).c_str());
   }
   if (aHasStream) {
     ASSERT_STREQ(firstAnsId.c_str(),
                  GetFirstLocalStreamId(*mSessionAns).c_str());
 
-    auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
-    auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
-    ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
-    ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
+  auto oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
+  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
+  ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
+  ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   }
 }
 
-// The JSEP draft explicitly forbids changing the msid on an m-section, but
-// that is a new restriction that older versions of Firefox do not follow.
-// JS will not see the msid change, since that is filtered out (except for
-// RTCRtpTransceiver.remoteTrackId)
-TEST_P(JsepSessionTest, RenegotiationOffererChangesMsid)
-{
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
-    return;
-  }
-
-  OfferAnswer();
-
-  std::string offer = CreateOffer();
-  SetLocalOffer(offer);
-
-  JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionOff, 0);
-  ASSERT_TRUE(transceiver);
-  std::string streamId = transceiver->mSendTrack.GetStreamIds()[0];
-  std::string trackId = transceiver->mSendTrack.GetTrackId();
-  std::string msidToReplace("a=msid:");
-  msidToReplace += streamId;
-  msidToReplace += " ";
-  msidToReplace += trackId;
-  size_t msidOffset = offer.find(msidToReplace);
-  ASSERT_NE(std::string::npos, msidOffset);
-  offer.replace(msidOffset, msidToReplace.size(), "a=msid:foo bar");
-
-  SetRemoteOffer(offer);
-
-  std::vector<JsepTrack> removedTracks = mSessionAns->GetRemoteTracksRemoved();
-  std::vector<JsepTrack> addedTracks = mSessionAns->GetRemoteTracksAdded();
-
-  ASSERT_EQ(1U, removedTracks.size());
-  ASSERT_FALSE(IsNull(removedTracks[0]));
-  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
-  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
-
-  ASSERT_EQ(1U, addedTracks.size());
-  ASSERT_FALSE(IsNull(addedTracks[0]));
-  ASSERT_EQ("foo", addedTracks[0].GetStreamIds()[0]);
-  ASSERT_EQ("bar", addedTracks[0].GetTrackId());
-
-  std::string answer = CreateAnswer();
-  SetLocalAnswer(answer);
-  SetRemoteAnswer(answer);
-}
-
-// The JSEP draft explicitly forbids changing the msid on an m-section, but
-// that is a new restriction that older versions of Firefox do not follow.
-TEST_P(JsepSessionTest, RenegotiationAnswererChangesMsid)
+TEST_P(JsepSessionTest, RenegotiationOffererRemovesTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  std::string offer = CreateOffer();
-  SetLocalOffer(offer);
-  SetRemoteOffer(offer);
-  std::string answer = CreateAnswer();
-  SetLocalAnswer(answer);
-
-  JsepTransceiver* transceiver = GetNegotiatedTransceiver(*mSessionAns, 0);
-  ASSERT_TRUE(transceiver);
-  std::string streamId = transceiver->mSendTrack.GetStreamIds()[0];
-  std::string trackId = transceiver->mSendTrack.GetTrackId();
-  std::string msidToReplace("a=msid:");
-  msidToReplace += streamId;
-  msidToReplace += " ";
-  msidToReplace += trackId;
-  size_t msidOffset = answer.find(msidToReplace);
-  ASSERT_NE(std::string::npos, msidOffset);
-  answer.replace(msidOffset, msidToReplace.size(), "a=msid:foo bar");
-
-  SetRemoteAnswer(answer);
-
-  std::vector<JsepTrack> removedTracks = mSessionOff->GetRemoteTracksRemoved();
-  std::vector<JsepTrack> addedTracks = mSessionOff->GetRemoteTracksAdded();
-
-  ASSERT_EQ(1U, removedTracks.size());
-  ASSERT_FALSE(IsNull(removedTracks[0]));
-  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
-  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
-
-  ASSERT_EQ(1U, addedTracks.size());
-  ASSERT_FALSE(IsNull(addedTracks[0]));
-  ASSERT_EQ("foo", addedTracks[0].GetStreamIds()[0]);
-  ASSERT_EQ("bar", addedTracks[0].GetTrackId());
-}
-
-TEST_P(JsepSessionTest, RenegotiationOffererStopsTransceiver)
-{
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns);
-  if (types.back() == SdpMediaSection::kApplication) {
-    return;
-  }
-
-  OfferAnswer();
-
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers =
-    DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers =
-    DeepCopy(mSessionAns->GetTransceivers());
-
-  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
-  mSessionOff->GetTransceivers().back()->Stop();
-  JsepTrack removedTrack(mSessionOff->GetTransceivers().back()->mSendTrack);
+  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.GetStreamIds(), removed[0].GetStreamIds());
-  ASSERT_EQ(removedTrack.GetTrackId(), removed[0].GetTrackId());
+  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(1U, removed.size());
-
-  // Last m-section should be disabled
+  ASSERT_EQ(0U, removed.size());
+
+  // First m-section should be recvonly
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  const SdpMediaSection* msection =
-    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
+  auto* msection = GetMsection(*offer, types.front(), 0);
   ASSERT_TRUE(msection);
-  ValidateDisabledMSection(msection);
-
-  // Last m-section should be disabled
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  // First audio m-section should be sendonly
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
+  msection = GetMsection(*answer, types.front(), 0);
   ASSERT_TRUE(msection);
-  ValidateDisabledMSection(msection);
-
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-
-  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
-
-  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
-  ASSERT_FALSE(origAnswererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newAnswererTransceivers.back()->IsStopped());
-  origOffererTransceivers.pop_back(); // Ignore this one
-  newOffererTransceivers.pop_back(); // Ignore this one
-  origAnswererTransceivers.pop_back(); // Ignore this one
-  newAnswererTransceivers.pop_back(); // Ignore this one
-
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  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, RenegotiationAnswererStopsTransceiver)
+TEST_P(JsepSessionTest, RenegotiationAnswererRemovesTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.back() == SdpMediaSection::kApplication) {
+  if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
-
-  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
-  mSessionAns->GetTransceivers().back()->Stop();
-  JsepTrack removedTrack(mSessionAns->GetTransceivers().back()->mSendTrack);
+  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.GetStreamIds(), removed[0].GetStreamIds());
-  ASSERT_EQ(removedTrack.GetTrackId(), removed[0].GetTrackId());
-
-  // Last m-section should be sendrecv
+  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);
-  const SdpMediaSection* msection =
-    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
+  auto* msection = GetMsection(*offer, types.front(), 0);
   ASSERT_TRUE(msection);
   ASSERT_TRUE(msection->IsReceiving());
   ASSERT_TRUE(msection->IsSending());
 
-  // Last m-section should be disabled
+  // First audio m-section should be recvonly
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
+  msection = GetMsection(*answer, types.front(), 0);
   ASSERT_TRUE(msection);
-  ValidateDisabledMSection(msection);
-
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-
-  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
-  ASSERT_FALSE(origAnswererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newAnswererTransceivers.back()->IsStopped());
-  origOffererTransceivers.pop_back(); // Ignore this one
-  newOffererTransceivers.pop_back(); // Ignore this one
-  origAnswererTransceivers.pop_back(); // Ignore this one
-  newAnswererTransceivers.pop_back(); // Ignore this one
-
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  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, RenegotiationBothStopSameTransceiver)
+TEST_P(JsepSessionTest, RenegotiationBothRemoveTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.back() == SdpMediaSection::kApplication) {
+  if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
-
-  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
-  mSessionOff->GetTransceivers().back()->Stop();
-  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back()->mSendTrack);
-  mSessionAns->GetTransceivers().back()->Stop();
-  JsepTrack removedTrackAnswer(mSessionAns->GetTransceivers().back()->mSendTrack);
+  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.GetStreamIds(), removed[0].GetStreamIds());
-  ASSERT_EQ(removedTrackOffer.GetTrackId(), removed[0].GetTrackId());
+  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.GetStreamIds(), removed[0].GetStreamIds());
-  ASSERT_EQ(removedTrackAnswer.GetTrackId(), removed[0].GetTrackId());
-
-  // Last m-section should be disabled
+  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);
-  const SdpMediaSection* msection =
-    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
+  auto* msection = GetMsection(*offer, types.front(), 0);
   ASSERT_TRUE(msection);
-  ValidateDisabledMSection(msection);
-
-  // Last m-section should be disabled
+  ASSERT_TRUE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+
+  // First m-section should be inactive, and rejected
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
+  msection = GetMsection(*answer, types.front(), 0);
   ASSERT_TRUE(msection);
-  ValidateDisabledMSection(msection);
-
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-
-  ASSERT_FALSE(origOffererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newOffererTransceivers.back()->IsStopped());
-  ASSERT_FALSE(origAnswererTransceivers.back()->IsStopped());
-  ASSERT_TRUE(newAnswererTransceivers.back()->IsStopped());
-  origOffererTransceivers.pop_back(); // Ignore this one
-  newOffererTransceivers.pop_back(); // Ignore this one
-  origAnswererTransceivers.pop_back(); // Ignore this one
-  newAnswererTransceivers.pop_back(); // Ignore this one
-
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
-  ASSERT_TRUE(Equals(origAnswererTransceivers, newAnswererTransceivers));
+  ASSERT_FALSE(msection->IsReceiving());
+  ASSERT_FALSE(msection->IsSending());
+  ASSERT_FALSE(msection->GetPort());
+
+  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
+
+  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
+
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    JsepTrackPair oldPair(offererPairs[i + 1]);
+    JsepTrackPair newPair(newOffererPairs[i]);
+    ASSERT_EQ(oldPair.mLevel, newPair.mLevel);
+    ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get());
+    ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get());
+    ASSERT_TRUE(oldPair.HasBundleLevel());
+    ASSERT_TRUE(newPair.HasBundleLevel());
+    ASSERT_EQ(0U, oldPair.BundleLevel());
+    ASSERT_EQ(1U, newPair.BundleLevel());
+  }
+
+  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
+
+  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
+    JsepTrackPair oldPair(answererPairs[i + 1]);
+    JsepTrackPair newPair(newAnswererPairs[i]);
+    ASSERT_EQ(oldPair.mLevel, newPair.mLevel);
+    ASSERT_EQ(oldPair.mSending.get(), newPair.mSending.get());
+    ASSERT_EQ(oldPair.mReceiving.get(), newPair.mReceiving.get());
+    ASSERT_TRUE(oldPair.HasBundleLevel());
+    ASSERT_TRUE(newPair.BundleLevel());
+    ASSERT_EQ(0U, oldPair.BundleLevel());
+    ASSERT_EQ(1U, newPair.BundleLevel());
+  }
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverThenAddTrack)
+TEST_P(JsepSessionTest, RenegotiationBothRemoveThenAddTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.back() == SdpMediaSection::kApplication) {
+  if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
-  SdpMediaSection::MediaType removedType = types.back();
+  SdpMediaSection::MediaType removedType = types.front();
 
   OfferAnswer();
 
-  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
-  mSessionOff->GetTransceivers().back()->Stop();
-  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back()->mSendTrack);
-  mSessionOff->GetTransceivers().back()->Stop();
-  JsepTrack removedTrackAnswer(mSessionOff->GetTransceivers().back()->mSendTrack);
+  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, removedType);
+  ASSERT_TRUE(removedTrackAnswer);
+  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrackAnswer->GetStreamId(),
+                                           removedTrackAnswer->GetTrackId()));
+
+  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, removedType);
+  ASSERT_TRUE(removedTrackOffer);
+  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrackOffer->GetStreamId(),
+                                           removedTrackOffer->GetTrackId()));
 
   OfferAnswer(CHECK_SUCCESS);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(removedType);
   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(1U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(removedType, added[0].GetMediaType());
+  ASSERT_EQ(removedType, added[0]->GetMediaType());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(1U, added.size());
   ASSERT_EQ(0U, removed.size());
-  ASSERT_EQ(removedType, added[0].GetMediaType());
-
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(origOffererTransceivers.size() + 1, newOffererTransceivers.size());
-  ASSERT_EQ(origAnswererTransceivers.size() + 1,
-            newAnswererTransceivers.size());
+  ASSERT_EQ(removedType, added[0]->GetMediaType());
+
+  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
+
+  ASSERT_EQ(offererPairs.size() + 1, newOffererPairs.size());
+  ASSERT_EQ(answererPairs.size() + 1, newAnswererPairs.size());
 
   // Ensure that the m-section was re-used; no gaps
-  ASSERT_EQ(origOffererTransceivers.back()->GetLevel(),
-            newOffererTransceivers.back()->GetLevel());
-
-  ASSERT_EQ(origAnswererTransceivers.back()->GetLevel(),
-            newAnswererTransceivers.back()->GetLevel());
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    ASSERT_EQ(i, newOffererPairs[i].mLevel);
+  }
+  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
+    ASSERT_EQ(i, newAnswererPairs[i].mLevel);
+  }
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverDifferentMsection)
+TEST_P(JsepSessionTest, RenegotiationBothRemoveTrackDifferentMsection)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-
-  if (types.size() < 2) {
+  if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
-  if (types[0] == SdpMediaSection::kApplication ||
-      types[1] == SdpMediaSection::kApplication) {
+  if (types.size() < 2 || types[0] != types[1]) {
+    // For simplicity, just run in cases where we have two of the same type
     return;
   }
 
   OfferAnswer();
 
-  mSessionOff->GetTransceivers()[0]->Stop();
-  mSessionOff->GetTransceivers()[1]->Stop();
+  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(2U, removed.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(2U, removed.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();
 
-  mSessionOff->GetTransceivers()[0]->mSendTrack.UpdateTrackIds(
-      std::vector<std::string>(1, "newstream"), "newtrack");
+  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);
 
-  // Latest JSEP spec says the msid never changes, so the other side will not
-  // notice track replacement.
   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());
-}
-
-TEST_P(JsepSessionTest, RenegotiationAnswererReplacesTrack)
-{
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns);
-
-  if (types.front() == SdpMediaSection::kApplication) {
-    return;
+
+  // 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]));
   }
 
-  OfferAnswer();
-
-  mSessionAns->GetTransceivers()[0]->mSendTrack.UpdateTrackIds(
-      std::vector<std::string>(1, "newstream"), "newtrack");
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  // Latest JSEP spec says the msid never changes, so the other side will not
-  // notice track replacement.
-  auto added = mSessionOff->GetRemoteTracksAdded();
-  auto removed = mSessionOff->GetRemoteTracksRemoved();
-  ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(0U, removed.size());
+  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();
@@ -2423,114 +2189,120 @@ TEST_P(JsepSessionTest, RenegotiationAut
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
-
-  ASSERT_EQ(origOffererTransceivers.size(), origAnswererTransceivers.size());
-  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
-    ASSERT_FALSE(IsNull(origOffererTransceivers[i]->mRecvTrack));
-    ASSERT_FALSE(IsNull(origAnswererTransceivers[i]->mSendTrack));
+  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(origOffererTransceivers[i]->mRecvTrack.GetStreamIds(),
-              origAnswererTransceivers[i]->mSendTrack.GetStreamIds());
-    ASSERT_NE(origOffererTransceivers[i]->mRecvTrack.GetTrackId(),
-              origAnswererTransceivers[i]->mSendTrack.GetTrackId());
+    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 newOffererTransceivers = mSessionOff->GetTransceivers();
-
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+  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]));
+  }
 }
 
 TEST_P(JsepSessionTest, RenegotiationOffererDisablesTelephoneEvent)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   OfferAnswer();
 
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+
   // check all the audio tracks to make sure they have 2 codecs (109 and 101),
   // and dtmf is enabled on all audio tracks
-  std::vector<JsepTrack> tracks;
-  for (const auto& transceiver : mSessionOff->GetTransceivers()) {
-    tracks.push_back(transceiver->mSendTrack);
-    tracks.push_back(transceiver->mRecvTrack);
-  }
-
-  for (const JsepTrack& track : tracks) {
-    if (track.GetMediaType() != SdpMediaSection::kAudio) {
-      continue;
-    }
-    const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
-    ASSERT_EQ(1U, details->GetEncodingCount());
-    const JsepTrackEncoding& encoding = details->GetEncoding(0);
-    ASSERT_EQ(2U, encoding.GetCodecs().size());
-    ASSERT_TRUE(encoding.HasFormat("109"));
-    ASSERT_TRUE(encoding.HasFormat("101"));
-    for (JsepCodecDescription* codec: encoding.GetCodecs()) {
-      ASSERT_TRUE(codec);
-      // we can cast here because we've already checked for audio track
-      JsepAudioCodecDescription *audioCodec =
-          static_cast<JsepAudioCodecDescription*>(codec);
-      ASSERT_TRUE(audioCodec->mDtmfEnabled);
+  for (size_t i = 0; i < offererPairs.size(); ++i) {
+    std::vector<JsepTrack*> tracks;
+    tracks.push_back(offererPairs[i].mSending.get());
+    tracks.push_back(offererPairs[i].mReceiving.get());
+    for (JsepTrack *track : tracks) {
+      if (track->GetMediaType() != SdpMediaSection::kAudio) {
+        continue;
+      }
+      const JsepTrackNegotiatedDetails* details = track->GetNegotiatedDetails();
+      ASSERT_EQ(1U, details->GetEncodingCount());
+      const JsepTrackEncoding& encoding = details->GetEncoding(0);
+      ASSERT_EQ(2U, encoding.GetCodecs().size());
+      ASSERT_TRUE(encoding.HasFormat("109"));
+      ASSERT_TRUE(encoding.HasFormat("101"));
+      for (JsepCodecDescription* codec: encoding.GetCodecs()) {
+        ASSERT_TRUE(codec);
+        // we can cast here because we've already checked for audio track
+        JsepAudioCodecDescription *audioCodec =
+            static_cast<JsepAudioCodecDescription*>(codec);
+        ASSERT_TRUE(audioCodec->mDtmfEnabled);
+      }
     }
   }
 
   std::string offer = CreateOffer();
   ReplaceInSdp(&offer, " 109 101 ", " 109 ");
   ReplaceInSdp(&offer, "a=fmtp:101 0-15\r\n", "");
   ReplaceInSdp(&offer, "a=rtpmap:101 telephone-event/8000/1\r\n", "");
   std::cerr << "modified OFFER: " << offer << std::endl;
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
+  AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
+  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
+
   // check all the audio tracks to make sure they have 1 codec (109),
   // and dtmf is disabled on all audio tracks
-  tracks.clear();
-  for (const auto& transceiver : mSessionOff->GetTransceivers()) {
-    tracks.push_back(transceiver->mSendTrack);
-    tracks.push_back(transceiver->mRecvTrack);
-  }
-
-  for (const JsepTrack& track : tracks) {
-    if (track.GetMediaType() != SdpMediaSection::kAudio) {
-      continue;
+  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+    std::vector<JsepTrack*> tracks;
+    tracks.push_back(newOffererPairs[i].mSending.get());
+    tracks.push_back(newOffererPairs[i].mReceiving.get());
+    for (JsepTrack* track : tracks) {
+      if (track->GetMediaType() != SdpMediaSection::kAudio) {
+        continue;
+      }
+      const JsepTrackNegotiatedDetails* details = track->GetNegotiatedDetails();
+      ASSERT_EQ(1U, details->GetEncodingCount());
+      const JsepTrackEncoding& encoding = details->GetEncoding(0);
+      ASSERT_EQ(1U, encoding.GetCodecs().size());
+      ASSERT_TRUE(encoding.HasFormat("109"));
+      // we can cast here because we've already checked for audio track
+      JsepAudioCodecDescription *audioCodec =
+          static_cast<JsepAudioCodecDescription*>(encoding.GetCodecs()[0]);
+      ASSERT_TRUE(audioCodec);
+      ASSERT_FALSE(audioCodec->mDtmfEnabled);
     }
-    const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
-    ASSERT_EQ(1U, details->GetEncodingCount());
-    const JsepTrackEncoding& encoding = details->GetEncoding(0);
-    ASSERT_EQ(1U, encoding.GetCodecs().size());
-    ASSERT_TRUE(encoding.HasFormat("109"));
-    // we can cast here because we've already checked for audio track
-    JsepAudioCodecDescription *audioCodec =
-        static_cast<JsepAudioCodecDescription*>(encoding.GetCodecs()[0]);
-    ASSERT_TRUE(audioCodec);
-    ASSERT_FALSE(audioCodec->mDtmfEnabled);
   }
 }
 
 // Tests behavior when the answerer does not use msid in the initial exchange,
 // but does on renegotiation.
 TEST_P(JsepSessionTest, RenegotiationAnswererEnablesMsid)
 {
   AddTracks(*mSessionOff);
@@ -2540,81 +2312,94 @@ TEST_P(JsepSessionTest, RenegotiationAns
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
+  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
-    ASSERT_EQ(origOffererTransceivers[i]->mRecvTrack.GetMediaType(),
-              newOffererTransceivers[i]->mRecvTrack.GetMediaType());
-
-    ASSERT_TRUE(Equals(origOffererTransceivers[i]->mSendTrack,
-                       newOffererTransceivers[i]->mSendTrack));
-    ASSERT_TRUE(Equals(origOffererTransceivers[i]->mTransport,
-                       newOffererTransceivers[i]->mTransport));
-
-    if (origOffererTransceivers[i]->mRecvTrack.GetMediaType() ==
+  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_TRUE(Equals(offererPairs[i].mRtpTransport,
+                       newOffererPairs[i].mRtpTransport));
+    ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport,
+                       newOffererPairs[i].mRtcpTransport));
+
+    if (offererPairs[i].mReceiving->GetMediaType() ==
         SdpMediaSection::kApplication) {
-      ASSERT_TRUE(Equals(origOffererTransceivers[i]->mRecvTrack,
-                         newOffererTransceivers[i]->mRecvTrack));
+      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
     } else {
       // This should be the only difference
-      ASSERT_FALSE(Equals(origOffererTransceivers[i]->mRecvTrack,
-                          newOffererTransceivers[i]->mRecvTrack));
+      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);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
+  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-
-  ASSERT_TRUE(Equals(origOffererTransceivers, newOffererTransceivers));
+  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_TRUE(Equals(offererPairs[i].mRtpTransport,
+                       newOffererPairs[i].mRtpTransport));
+    ASSERT_TRUE(Equals(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);
@@ -2629,157 +2414,178 @@ TEST_P(JsepSessionTest, RenegotiationOff
   DisableBundle(&offer);
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
 
   OfferAnswer();
 
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
-
-  for (size_t i = 0; i < newOffererTransceivers.size(); ++i) {
+  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(origOffererTransceivers[i]->HasBundleLevel());
-    ASSERT_FALSE(origAnswererTransceivers[i]->HasBundleLevel());
+    ASSERT_FALSE(offererPairs[i].HasBundleLevel());
+    ASSERT_FALSE(answererPairs[i].HasBundleLevel());
     if (i != 0) {
-      ASSERT_NE(origOffererTransceivers[0]->mTransport.get(),
-                origOffererTransceivers[i]->mTransport.get());
-      ASSERT_NE(origAnswererTransceivers[0]->mTransport.get(),
-                origAnswererTransceivers[i]->mTransport.get());
+      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(newOffererTransceivers[i]->HasBundleLevel());
-    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
-    ASSERT_EQ(newOffererTransceivers[0]->mTransport.get(),
-              newOffererTransceivers[i]->mTransport.get());
-    ASSERT_EQ(newAnswererTransceivers[0]->mTransport.get(),
-              newAnswererTransceivers[i]->mTransport.get());
+    ASSERT_TRUE(newOffererPairs[i].HasBundleLevel());
+    ASSERT_TRUE(newAnswererPairs[i].HasBundleLevel());
+    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();
 
-  mSessionOff->GetTransceivers()[0]->Stop();
-
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
-
-  ASSERT_FALSE(newOffererTransceivers[0]->HasBundleLevel());
-  ASSERT_FALSE(newAnswererTransceivers[0]->HasBundleLevel());
-
-  ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
-            origOffererTransceivers[0]->mTransport.get());
-  ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
-            origAnswererTransceivers[0]->mTransport.get());
-
-  ASSERT_EQ(0U, newOffererTransceivers[0]->mTransport->mComponents);
-  ASSERT_EQ(0U, newAnswererTransceivers[0]->mTransport->mComponents);
-
-  for (size_t i = 1; i < newOffererTransceivers.size(); ++i) {
-    ASSERT_TRUE(newOffererTransceivers[i]->HasBundleLevel());
-    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
-    ASSERT_EQ(1U, newOffererTransceivers[i]->BundleLevel());
-    ASSERT_EQ(1U, newAnswererTransceivers[i]->BundleLevel());
-    ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
-              newOffererTransceivers[i]->mTransport.get());
-    ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
-              newAnswererTransceivers[i]->mTransport.get());
+  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].HasBundleLevel());
+    ASSERT_TRUE(newAnswererPairs[i].HasBundleLevel());
+    ASSERT_EQ(1U, newOffererPairs[i].BundleLevel());
+    ASSERT_EQ(1U, newAnswererPairs[i].BundleLevel());
+    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());
+
+  ASSERT_LE(1U, mSessionOff->GetTransports().size());
+  ASSERT_LE(1U, mSessionAns->GetTransports().size());
+
+  ASSERT_EQ(0U, mSessionOff->GetTransports()[0]->mComponents);
+  ASSERT_EQ(0U, mSessionAns->GetTransports()[0]->mComponents);
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererDisablesBundleTransport)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   if (types.size() < 2) {
     return;
   }
 
   OfferAnswer();
 
-  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers
-    = DeepCopy(mSessionOff->GetTransceivers());
-  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
-    = DeepCopy(mSessionAns->GetTransceivers());
-
-  mSessionAns->GetTransceivers()[0]->Stop();
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  auto newOffererTransceivers = mSessionOff->GetTransceivers();
-  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
-
-  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
-  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
-  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
-
-  ASSERT_FALSE(newOffererTransceivers[0]->HasBundleLevel());
-  ASSERT_FALSE(newAnswererTransceivers[0]->HasBundleLevel());
-
-  ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
-            origOffererTransceivers[0]->mTransport.get());
-  ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
-            origAnswererTransceivers[0]->mTransport.get());
-
-  ASSERT_EQ(0U, newOffererTransceivers[0]->mTransport->mComponents);
-  ASSERT_EQ(0U, newAnswererTransceivers[0]->mTransport->mComponents);
-
-  for (size_t i = 1; i < newOffererTransceivers.size(); ++i) {
-    ASSERT_TRUE(newOffererTransceivers[i]->HasBundleLevel());
-    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
-    ASSERT_EQ(1U, newOffererTransceivers[i]->BundleLevel());
-    ASSERT_EQ(1U, newAnswererTransceivers[i]->BundleLevel());
-    ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
-              newOffererTransceivers[i]->mTransport.get());
-    ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
-              newAnswererTransceivers[i]->mTransport.get());
+  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+
+  std::string reoffer = CreateOffer();
+  SetLocalOffer(reoffer, CHECK_SUCCESS);
+  SetRemoteOffer(reoffer, CHECK_SUCCESS);
+  std::string reanswer = CreateAnswer();
+
+  CopyTransportAttributes(&reanswer, 0, 1);
+  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].HasBundleLevel());
+    ASSERT_TRUE(newAnswererPairs[i].HasBundleLevel());
+    ASSERT_EQ(1U, newOffererPairs[i].BundleLevel());
+    ASSERT_EQ(1U, newAnswererPairs[i].BundleLevel());
+    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, ParseRejectsBadMediaFormat)
 {
-  AddTracks(*mSessionOff);
-  if (types.front() == SdpMediaSection::MediaType::kApplication) {
+  if (GetParam() == "datachannel") {
     return;
   }
+  AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   UniquePtr<Sdp> munge(Parse(offer));
   SdpMediaSection& mediaSection = munge->GetMediaSection(0);
   mediaSection.AddCodec("75", "DummyFormatVal", 8000, 1);
   std::string sdpString = munge->ToString();
   nsresult rv = mSessionOff->SetLocalDescription(kJsepSdpOffer, sdpString);
   ASSERT_EQ(NS_ERROR_INVALID_ARG, rv);
 }
@@ -3098,23 +2904,23 @@ TEST_P(JsepSessionTest, RenegotiationAns
       msection.SetReceiving(false);
     }
   }
 
   answer = parsedAnswer->ToString();
 
   SetRemoteAnswer(answer);
 
-  for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) {
-    if (track.GetMediaType() != SdpMediaSection::kApplication) {
-      ASSERT_FALSE(track.GetActive());
+  for (const RefPtr<JsepTrack>& track : mSessionOff->GetLocalTracks()) {
+    if (track->GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track->GetActive());
     }
   }
 
-  ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(types.size(), mSessionOff->GetNegotiatedTrackPairs().size());
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererInactive)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   OfferAnswer();
 
@@ -3132,23 +2938,23 @@ TEST_P(JsepSessionTest, RenegotiationAns
       msection.SetSending(false);
     }
   }
 
   answer = parsedAnswer->ToString();
 
   SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks
 
-  for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) {
-    if (track.GetMediaType() != SdpMediaSection::kApplication) {
-      ASSERT_FALSE(track.GetActive());
+  for (const RefPtr<JsepTrack>& track : mSessionOff->GetLocalTracks()) {
+    if (track->GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track->GetActive());
     }
   }
 
-  ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(types.size(), mSessionOff->GetNegotiatedTrackPairs().size());
 }
 
 
 INSTANTIATE_TEST_CASE_P(
     Variants,
     JsepSessionTest,
     ::testing::Values("audio",
                       "video",
@@ -3171,23 +2977,20 @@ INSTANTIATE_TEST_CASE_P(
                       "audio,video,video",
                       "audio,audio,video,video",
                       "audio,audio,video,video,datachannel"));
 
 // offerToReceiveXxx variants
 
 TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines)
 {
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
-  std::string offer = CreateOffer();
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
+  options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
+  std::string offer = CreateOffer(Some(options));
 
   UniquePtr<Sdp> parsedOffer(Parse(offer));
   ASSERT_TRUE(!!parsedOffer);
 
   ASSERT_EQ(3U, parsedOffer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             parsedOffer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3236,32 +3039,34 @@ TEST_F(JsepSessionTest, OfferAnswerRecvO
   ASSERT_EQ(SdpMediaSection::kVideo,
             parsedAnswer->GetMediaSection(2).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kInactive,
             parsedAnswer->GetMediaSection(2).GetAttributeList().GetDirection());
 
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  std::vector<RefPtr<JsepTransceiver>> transceivers(mSessionOff->GetTransceivers());
-  ASSERT_EQ(3U, transceivers.size());
-  for (const auto& transceiver : transceivers) {
-    auto ssrcs = parsedOffer->GetMediaSection(transceiver->GetLevel())
-                 .GetAttributeList().GetSsrc().mSsrcs;
+  std::vector<JsepTrackPair> trackPairs(mSessionOff->GetNegotiatedTrackPairs());
+  ASSERT_EQ(2U, trackPairs.size());
+  for (auto pair : trackPairs) {
+    auto ssrcs = parsedOffer->GetMediaSection(pair.mLevel).GetAttributeList()
+                 .GetSsrc().mSsrcs;
     ASSERT_EQ(1U, ssrcs.size());
+    ASSERT_EQ(pair.mRecvonlySsrc, ssrcs.front().ssrc);
   }
 }
 
 TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines)
 {
   AddTracks(*mSessionOff, "audio,video,video");
 
-  SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly);
-  SetDirection(*mSessionOff, 2, SdpDirectionAttribute::kSendonly);
-  std::string offer = CreateOffer();
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U));
+  options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
+  std::string offer = CreateOffer(Some(options));
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(3U, outputSdp->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             outputSdp->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kSendonly,
@@ -3302,20 +3107,20 @@ TEST_F(JsepSessionTest, OfferAnswerSendO
   ASSERT_EQ(SdpMediaSection::kVideo,
             outputSdp->GetMediaSection(2).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
             outputSdp->GetMediaSection(2).GetAttributeList().GetDirection());
 }
 
 TEST_F(JsepSessionTest, OfferToReceiveAudioNotUsed)
 {
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
-
-  OfferAnswer(CHECK_SUCCESS);
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some<size_t>(1);
+
+  OfferAnswer(CHECK_SUCCESS, Some(options));
 
   UniquePtr<Sdp> offer(Parse(
         mSessionOff->GetLocalDescription(kJsepDescriptionCurrent)));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3328,20 +3133,20 @@ TEST_F(JsepSessionTest, OfferToReceiveAu
   ASSERT_EQ(SdpMediaSection::kAudio,
             answer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kInactive,
             answer->GetMediaSection(0).GetAttributeList().GetDirection());
 }
 
 TEST_F(JsepSessionTest, OfferToReceiveVideoNotUsed)
 {
-  mSessionOff->AddTransceiver(new JsepTransceiver(
-        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
-
-  OfferAnswer(CHECK_SUCCESS);
+  JsepOfferOptions options;
+  options.mOfferToReceiveVideo = Some<size_t>(1);
+
+  OfferAnswer(CHECK_SUCCESS, Some(options));
 
   UniquePtr<Sdp> offer(Parse(
         mSessionOff->GetLocalDescription(kJsepDescriptionCurrent)));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kVideo,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3354,25 +3159,23 @@ TEST_F(JsepSessionTest, OfferToReceiveVi
   ASSERT_EQ(SdpMediaSection::kVideo,
             answer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kInactive,
             answer->GetMediaSection(0).GetAttributeList().GetDirection());
 }
 
 TEST_F(JsepSessionTest, CreateOfferNoDatachannelDefault)
 {
-  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
-  audio->mSendTrack.UpdateTrackIds(
-      std::vector<std::string>(1, "offerer_stream"), "a1");
-  mSessionOff->AddTransceiver(audio);
-
-  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
-  video->mSendTrack.UpdateTrackIds(
-      std::vector<std::string>(1, "offerer_stream"), "v1");
-  mSessionOff->AddTransceiver(video);
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff->AddTrack(msta);
+
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+  mSessionOff->AddTrack(mstv1);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
@@ -3381,23 +3184,22 @@ TEST_F(JsepSessionTest, CreateOfferNoDat
             outputSdp->GetMediaSection(1).GetMediaType());
 }
 
 TEST_F(JsepSessionTest, ValidateOfferedVideoCodecParams)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
 
-  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
-  audio->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "a1");
-  mSessionOff->AddTransceiver(audio);
-
-  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
-  video->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "v1");
-  mSessionOff->AddTransceiver(video);
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff->AddTrack(msta);
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
+  mSessionOff->AddTrack(mstv1);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
@@ -3509,23 +3311,22 @@ TEST_F(JsepSessionTest, ValidateOfferedV
   ASSERT_EQ(123, parsed_red_params.encodings[4]);
 }
 
 TEST_F(JsepSessionTest, ValidateOfferedAudioCodecParams)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
 
-  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
-  audio->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "a1");
-  mSessionOff->AddTransceiver(audio);
-
-  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
-  video->mSendTrack.UpdateTrackIds(std::vector<std::string>(1, "offerer_stream"), "v1");
-  mSessionOff->AddTransceiver(video);
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff->AddTrack(msta);
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v2"));
+  mSessionOff->AddTrack(mstv1);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& audio_section = outputSdp->GetMediaSection(0);
@@ -3592,29 +3393,39 @@ TEST_F(JsepSessionTest, ValidateOfferedA
   ASSERT_EQ("0-15", parsed_dtmf_params.dtmfTones);
 }
 
 TEST_F(JsepSessionTest, ValidateNoFmtpLineForRedInOfferAndAnswer)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
 
-  AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video");
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff->AddTrack(msta);
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+  mSessionOff->AddTrack(mstv1);
 
   std::string offer = CreateOffer();
 
   // look for line with fmtp:122 and remove it
   size_t start = offer.find("a=fmtp:122");
   size_t end = offer.find("\r\n", start);
   offer.replace(start, end+2-start, "");
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
-  AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video");
+  RefPtr<JsepTrack> msta_ans(
+      new JsepTrack(SdpMediaSection::kAudio, "answerer_stream", "a1"));
+  mSessionAns->AddTrack(msta);
+  RefPtr<JsepTrack> mstv1_ans(
+      new JsepTrack(SdpMediaSection::kVideo, "answerer_stream", "v1"));
+  mSessionAns->AddTrack(mstv1);
 
   std::string answer = CreateAnswer();
   // because parsing will throw out the malformed fmtp, make sure it is not
   // in the answer sdp string
   ASSERT_EQ(std::string::npos, answer.find("a=fmtp:122"));
 
   UniquePtr<Sdp> outputSdp(Parse(answer));
   ASSERT_TRUE(!!outputSdp);
@@ -3651,40 +3462,40 @@ TEST_F(JsepSessionTest, ValidateNoFmtpLi
   ASSERT_EQ("126", fmtps[0].format);
   ASSERT_EQ("97", fmtps[1].format);
   ASSERT_EQ("120", fmtps[2].format);
   ASSERT_EQ("121", fmtps[3].format);
 
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto offerTransceivers = mSessionOff->GetTransceivers();
-  ASSERT_EQ(2U, offerTransceivers.size());
-  ASSERT_FALSE(IsNull(offerTransceivers[1]->mSendTrack));
-  ASSERT_FALSE(IsNull(offerTransceivers[1]->mRecvTrack));
-  ASSERT_TRUE(offerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
-  ASSERT_TRUE(offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
+  auto offerPairs = mSessionOff->GetNegotiatedTrackPairs();
+  ASSERT_EQ(2U, offerPairs.size());
+  ASSERT_TRUE(offerPairs[1].mSending);
+  ASSERT_TRUE(offerPairs[1].mReceiving);
+  ASSERT_TRUE(offerPairs[1].mSending->GetNegotiatedDetails());
+  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(6U,
-      offerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
+      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(6U,
-      offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
+      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
-  auto answerTransceivers = mSessionAns->GetTransceivers();
-  ASSERT_EQ(2U, answerTransceivers.size());
-  ASSERT_FALSE(IsNull(answerTransceivers[1]->mSendTrack));
-  ASSERT_FALSE(IsNull(answerTransceivers[1]->mRecvTrack));
-  ASSERT_TRUE(answerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
-  ASSERT_TRUE(answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
+  auto answerPairs = mSessionAns->GetNegotiatedTrackPairs();
+  ASSERT_EQ(2U, answerPairs.size());
+  ASSERT_TRUE(answerPairs[1].mSending);
+  ASSERT_TRUE(answerPairs[1].mReceiving);
+  ASSERT_TRUE(answerPairs[1].mSending->GetNegotiatedDetails());
+  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(6U,
-      answerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
+      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(6U,
-      answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
+      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 }
 
 TEST_F(JsepSessionTest, ValidateAnsweredCodecParams)
 {
   // TODO(bug 1099351): Once fixed, we can allow red in this offer,
   // which will also cause multiple codecs in answer.  For now,
   // red/ulpfec for video are behind a pref to mitigate potential for
@@ -3704,23 +3515,33 @@ TEST_F(JsepSessionTest, ValidateAnswered
         h264->mDefaultPt = "126";
       }
     }
   }
 
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
 
-  AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video");
+  RefPtr<JsepTrack> msta(
+      new JsepTrack(SdpMediaSection::kAudio, "offerer_stream", "a1"));
+  mSessionOff->AddTrack(msta);
+  RefPtr<JsepTrack> mstv1(
+      new JsepTrack(SdpMediaSection::kVideo, "offerer_stream", "v1"));
+  mSessionOff->AddTrack(mstv1);
 
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
-  AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video");
+  RefPtr<JsepTrack> msta_ans(
+      new JsepTrack(SdpMediaSection::kAudio, "answerer_stream", "a1"));
+  mSessionAns->AddTrack(msta);
+  RefPtr<JsepTrack> mstv1_ans(
+      new JsepTrack(SdpMediaSection::kVideo, "answerer_stream", "v1"));
+  mSessionAns->AddTrack(mstv1);
 
   std::string answer = CreateAnswer();
 
   UniquePtr<Sdp> outputSdp(Parse(answer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
@@ -3772,40 +3593,40 @@ TEST_F(JsepSessionTest, ValidateAnswered
 
   ASSERT_EQ((uint32_t)12288, parsed_vp8_params.max_fs);
   ASSERT_EQ((uint32_t)60, parsed_vp8_params.max_fr);
 
 
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto offerTransceivers = mSessionOff->GetTransceivers();
-  ASSERT_EQ(2U, offerTransceivers.size());
-  ASSERT_FALSE(IsNull(offerTransceivers[1]->mSendTrack));
-  ASSERT_FALSE(IsNull(offerTransceivers[1]->mRecvTrack));
-  ASSERT_TRUE(offerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
-  ASSERT_TRUE(offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
+  auto offerPairs = mSessionOff->GetNegotiatedTrackPairs();
+  ASSERT_EQ(2U, offerPairs.size());
+  ASSERT_TRUE(offerPairs[1].mSending);
+  ASSERT_TRUE(offerPairs[1].mReceiving);
+  ASSERT_TRUE(offerPairs[1].mSending->GetNegotiatedDetails());
+  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(1U,
-      offerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
+      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(1U,
-      offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
+      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
-  auto answerTransceivers = mSessionAns->GetTransceivers();
-  ASSERT_EQ(2U, answerTransceivers.size());
-  ASSERT_FALSE(IsNull(answerTransceivers[1]->mSendTrack));
-  ASSERT_FALSE(IsNull(answerTransceivers[1]->mRecvTrack));
-  ASSERT_TRUE(answerTransceivers[1]->mSendTrack.GetNegotiatedDetails());
-  ASSERT_TRUE(answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
+  auto answerPairs = mSessionAns->GetNegotiatedTrackPairs();
+  ASSERT_EQ(2U, answerPairs.size());
+  ASSERT_TRUE(answerPairs[1].mSending);
+  ASSERT_TRUE(answerPairs[1].mReceiving);
+  ASSERT_TRUE(answerPairs[1].mSending->GetNegotiatedDetails());
+  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(1U,
-      answerTransceivers[1]->mSendTrack.GetNegotiatedDetails()->GetEncoding(0)
+      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(1U,
-      answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->GetEncoding(0)
+      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
 #if 0
   // H264 packetization mode 1
   ASSERT_EQ("126", fmtps[1].format);
   ASSERT_TRUE(fmtps[1].parameters);
   ASSERT_EQ(SdpRtpmapAttributeList::kH264, fmtps[1].parameters->codec_type);
 
@@ -3862,34 +3683,35 @@ static void ReplaceAll(const std::string
 {
   while (in->find(toReplace) != std::string::npos) {
     Replace(toReplace, with, in);
   }
 }
 
 static void
 GetCodec(JsepSession& session,
-         size_t transceiverIndex,
+         size_t pairIndex,
          sdp::Direction direction,
          size_t encodingIndex,
          size_t codecIndex,
          const JsepCodecDescription** codecOut)
 {
   *codecOut = nullptr;
-  ASSERT_LT(transceiverIndex, session.GetTransceivers().size());
-  RefPtr<JsepTransceiver> transceiver(session.GetTransceivers()[transceiverIndex]);
-  JsepTrack& track =
-      (direction == sdp::kSend) ? transceiver->mSendTrack : transceiver->mRecvTrack;
-  ASSERT_TRUE(track.GetNegotiatedDetails());
-  ASSERT_LT(encodingIndex, track.GetNegotiatedDetails()->GetEncodingCount());
+  ASSERT_LT(pairIndex, session.GetNegotiatedTrackPairs().size());
+  JsepTrackPair pair(session.GetNegotiatedTrackPairs().front());
+  RefPtr<JsepTrack> track(
+      (direction == sdp::kSend) ? pair.mSending : pair.mReceiving);
+  ASSERT_TRUE(track);
+  ASSERT_TRUE(track->GetNegotiatedDetails());
+  ASSERT_LT(encodingIndex, track->GetNegotiatedDetails()->GetEncodingCount());
   ASSERT_LT(codecIndex,
-      track.GetNegotiatedDetails()->GetEncoding(encodingIndex)
+      track->GetNegotiatedDetails()->GetEncoding(encodingIndex)
       .GetCodecs().size());
   *codecOut =
-      track.GetNegotiatedDetails()->GetEncoding(encodingIndex)
+      track->GetNegotiatedDetails()->GetEncoding(encodingIndex)
       .GetCodecs()[codecIndex];
 }
 
 static void
 ForceH264(JsepSession& session, uint32_t profileLevelId)
 {
   for (JsepCodecDescription* codec : session.Codecs()) {
     if (codec->mName == "H264") {
@@ -3962,18 +3784,18 @@ TEST_F(JsepSessionTest, TestH264Negotiat
   SetLocalOffer(offer, CHECK_SUCCESS);
 
   SetRemoteOffer(offer, CHECK_SUCCESS);
   std::string answer(CreateAnswer());
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
   SetLocalAnswer(answer, CHECK_SUCCESS);
 
-  ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionOff, 0));
-  ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionAns, 0));
+  ASSERT_EQ(0U, mSessionOff->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(0U, mSessionAns->GetNegotiatedTrackPairs().size());
 }
 
 TEST_F(JsepSessionTest, TestH264NegotiationOffererDefault)
 {
   ForceH264(*mSessionOff, 0x42000d);
   ForceH264(*mSessionAns, 0x42000d);
 
   AddTracks(*mSessionOff, "video");
@@ -4195,16 +4017,17 @@ TEST_F(JsepSessionTest, TestH264LevelAsy
   // it did not set level-asymmetry-required, and we already check that
   // elsewhere
 }
 
 TEST_P(JsepSessionTest, TestRejectMline)
 {
   // We need to do this before adding tracks
   types = BuildTypes(GetParam());
+  std::sort(types.begin(), types.end());
 
   switch (types.front()) {
     case SdpMediaSection::kAudio:
       // Sabotage audio
       EnsureNegotiationFailure(types.front(), "opus");
       break;
     case SdpMediaSection::kVideo:
       // Sabotage video
@@ -4245,30 +4068,26 @@ TEST_P(JsepSessionTest, TestRejectMline)
   ASSERT_EQ(0U, failed_section->GetPort());
 
   mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer);
   mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer);
 
   size_t numRejected = std::count(types.begin(), types.end(), types.front());
   size_t numAccepted = types.size() - numRejected;
 
-  if (types.front() == SdpMediaSection::MediaType::kApplication) {
-    ASSERT_TRUE(GetDatachannelTransceiver(*mSessionOff));
-    ASSERT_FALSE(
-        GetDatachannelTransceiver(*mSessionOff)->mRecvTrack.GetActive());
-    ASSERT_TRUE(GetDatachannelTransceiver(*mSessionAns));
-    ASSERT_FALSE(
-        GetDatachannelTransceiver(*mSessionAns)->mRecvTrack.GetActive());
-  } else {
-    ASSERT_EQ(types.size(), GetLocalTracks(*mSessionOff).size());
-    ASSERT_EQ(numAccepted, GetRemoteTracks(*mSessionOff).size());
-
-    ASSERT_EQ(types.size(), GetLocalTracks(*mSessionAns).size());
-    ASSERT_EQ(types.size(), GetRemoteTracks(*mSessionAns).size());
-  }
+  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(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)
 {
   JsepOfferOptions options;
   std::string offer;
   nsresult rv = mSessionOff->CreateOffer(options, &offer);
   ASSERT_NE(NS_OK, rv);
@@ -4496,20 +4315,20 @@ TEST_F(JsepSessionTest, TestRtcpFbStar)
   offer = parsedOffer->ToString();
 
   SetLocalOffer(offer, CHECK_SUCCESS);
   SetRemoteOffer(offer, CHECK_SUCCESS);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  ASSERT_EQ(1U, GetRemoteTracks(*mSessionAns).size());
-  JsepTrack track = GetRemoteTracks(*mSessionAns)[0];
-  ASSERT_TRUE(track.GetNegotiatedDetails());
-  auto* details = track.GetNegotiatedDetails();
+  ASSERT_EQ(1U, mSessionAns->GetRemoteTracks().size());
+  RefPtr<JsepTrack> track = mSessionAns->GetRemoteTracks()[0];
+  ASSERT_TRUE(track->GetNegotiatedDetails());
+  auto* details = track->GetNegotiatedDetails();
   for (const JsepCodecDescription* codec :
        details->GetEncoding(0).GetCodecs()) {
     const JsepVideoCodecDescription* videoCodec =
       static_cast<const JsepVideoCodecDescription*>(codec);
     ASSERT_EQ(1U, videoCodec->mNackFbTypes.size());
     ASSERT_EQ("", videoCodec->mNackFbTypes[0]);
   }
 }
@@ -4523,55 +4342,55 @@ TEST_F(JsepSessionTest, TestUniquePayloa
 
   std::string offer = CreateOffer();
   SetLocalOffer(offer, CHECK_SUCCESS);
   SetRemoteOffer(offer, CHECK_SUCCESS);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offerTransceivers = mSessionOff->GetTransceivers();
-  auto answerTransceivers = mSessionAns->GetTransceivers();
-  ASSERT_EQ(3U, offerTransceivers.size());
-  ASSERT_EQ(3U, answerTransceivers.size());
-
-  ASSERT_FALSE(IsNull(offerTransceivers[0]->mRecvTrack));
-  ASSERT_TRUE(offerTransceivers[0]->mRecvTrack.GetNegotiatedDetails());
+  auto offerPairs = mSessionOff->GetNegotiatedTrackPairs();
+  auto answerPairs = mSessionAns->GetNegotiatedTrackPairs();
+  ASSERT_EQ(3U, offerPairs.size());
+  ASSERT_EQ(3U, answerPairs.size());
+
+  ASSERT_TRUE(offerPairs[0].mReceiving);
+  ASSERT_TRUE(offerPairs[0].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      offerTransceivers[0]->mRecvTrack.GetNegotiatedDetails()->
+      offerPairs[0].mReceiving->GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_FALSE(IsNull(offerTransceivers[1]->mRecvTrack));
-  ASSERT_TRUE(offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(offerPairs[1].mReceiving);
+  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      offerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->
+      offerPairs[1].mReceiving->GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_FALSE(IsNull(offerTransceivers[2]->mRecvTrack));
-  ASSERT_TRUE(offerTransceivers[2]->mRecvTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(offerPairs[2].mReceiving);
+  ASSERT_TRUE(offerPairs[2].mReceiving->GetNegotiatedDetails());
   ASSERT_NE(0U,
-      offerTransceivers[2]->mRecvTrack.GetNegotiatedDetails()->
+      offerPairs[2].mReceiving->GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_FALSE(IsNull(answerTransceivers[0]->mRecvTrack));
-  ASSERT_TRUE(answerTransceivers[0]->mRecvTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(answerPairs[0].mReceiving);
+  ASSERT_TRUE(answerPairs[0].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      answerTransceivers[0]->mRecvTrack.GetNegotiatedDetails()->
+      answerPairs[0].mReceiving->GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_FALSE(IsNull(answerTransceivers[1]->mRecvTrack));
-  ASSERT_TRUE(answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(answerPairs[1].mReceiving);
+  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      answerTransceivers[1]->mRecvTrack.GetNegotiatedDetails()->
+      answerPairs[1].mReceiving->GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_FALSE(IsNull(answerTransceivers[2]->mRecvTrack));
-  ASSERT_TRUE(answerTransceivers[2]->mRecvTrack.GetNegotiatedDetails());
+  ASSERT_TRUE(answerPairs[2].mReceiving);
+  ASSERT_TRUE(answerPairs[2].mReceiving->GetNegotiatedDetails());
   ASSERT_NE(0U,
-      answerTransceivers[2]->mRecvTrack.GetNegotiatedDetails()->
+      answerPairs[2].mReceiving->GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 }
 
 TEST_F(JsepSessionTest, UnknownFingerprintAlgorithm)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -4736,17 +4555,17 @@ TEST_P(JsepSessionTest, TestRejectOfferR
 
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
   ASSERT_EQ(NS_OK,
             mSessionAns->SetRemoteDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(kJsepStateStable, mSessionAns->GetState());
-  ASSERT_EQ(CountRtpTypes(), mSessionAns->GetRemoteTracksRemoved().size());
+  ASSERT_EQ(types.size(), mSessionAns->GetRemoteTracksRemoved().size());
 
   ASSERT_EQ(NS_OK,
             mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(kJsepStateStable, mSessionOff->GetState());
 
   OfferAnswer();
 }
 
@@ -4788,22 +4607,20 @@ TEST_P(JsepSessionTest, TestInvalidRollb
   ASSERT_EQ(NS_ERROR_UNEXPECTED,
             mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(NS_ERROR_UNEXPECTED,
             mSessionOff->SetRemoteDescription(kJsepSdpRollback, ""));
 }
 
 size_t GetActiveTransportCount(const JsepSession& session)
 {
+  auto transports = session.GetTransports();
   size_t activeTransportCount = 0;
-  for (const auto& transceiver : session.GetTransceivers()) {
-    if (!transceiver->HasBundleLevel() ||
-        (transceiver->BundleLevel() == transceiver->GetLevel())) {
-      activeTransportCount += transceiver->mTransport->mComponents;
-    }
+  for (RefPtr<JsepTransport>& transport : transports) {
+    activeTransportCount += transport->mComponents;
   }
   return activeTransportCount;
 }
 
 TEST_P(JsepSessionTest, TestBalancedBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4829,18 +4646,18 @@ TEST_P(JsepSessionTest, TestBalancedBund
   }
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers");
-  CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers");
+  CheckPairs(*mSessionOff, "Offerer pairs");
+  CheckPairs(*mSessionAns, "Answerer pairs");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_P(JsepSessionTest, TestMaxBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4860,18 +4677,18 @@ TEST_P(JsepSessionTest, TestMaxBundle)
   for (size_t i = 1; i < parsedOffer->GetMediaSectionCount(); ++i) {
     ASSERT_TRUE(
         parsedOffer->GetMediaSection(i).GetAttributeList().HasAttribute(
           SdpAttribute::kBundleOnlyAttribute));
     ASSERT_EQ(0U, parsedOffer->GetMediaSection(i).GetPort());
   }
 
 
-  CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers");
-  CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers");
+  CheckPairs(*mSessionOff, "Offerer pairs");
+  CheckPairs(*mSessionAns, "Answerer pairs");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_F(JsepSessionTest, TestNonDefaultProtocol)
 {
   AddTracks(*mSessionOff, "audio,video,datachannel");
   AddTracks(*mSessionAns, "audio,video,datachannel");
@@ -4993,48 +4810,60 @@ TEST_F(JsepSessionTest, CreateOfferDontR
 }
 
 TEST_F(JsepSessionTest, CreateOfferRemoveAudioTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
-  SetDirection(*mSessionOff, 1, SdpDirectionAttribute::kSendonly);
-  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
-  ASSERT_FALSE(IsNull(removedTrack));
-
-  CreateOffer();
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
+  options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U));
+
+  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
+  ASSERT_TRUE(removedTrack);
+  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
+                                           removedTrack->GetTrackId()));
+
+  CreateOffer(Some(options));
 }
 
 TEST_F(JsepSessionTest, CreateOfferDontReceiveAudioRemoveAudioTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
-  SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly);
-  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
-  ASSERT_FALSE(IsNull(removedTrack));
-
-  CreateOffer();
+  JsepOfferOptions options;
+  options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U));
+  options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
+
+  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
+  ASSERT_TRUE(removedTrack);
+  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
+                                           removedTrack->GetTrackId()));
+
+  CreateOffer(Some(options));
 }
 
 TEST_F(JsepSessionTest, CreateOfferDontReceiveVideoRemoveVideoTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
   JsepOfferOptions options;
   options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
   options.mOfferToReceiveVideo = Some(static_cast<size_t>(0U));
 
-  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
-  ASSERT_FALSE(IsNull(removedTrack));
+  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.back());
+  ASSERT_TRUE(removedTrack);
+  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
+                                           removedTrack->GetTrackId()));
 
   CreateOffer(Some(options));
 }
 
 static const std::string strSampleCandidate =
   "a=candidate:1 1 UDP 2130706431 192.168.2.1 50005 typ host\r\n";
 
 static const unsigned short nSamplelevel = 2;
@@ -5681,19 +5510,19 @@ TEST_F(JsepSessionTest, AudioCallMismatc
   std::string active = "\r\na=setup:active";
   match = answer.find(active);
   ASSERT_NE(match, std::string::npos);
   answer.replace(match, active.length(), "\r\na=setup:passive");
   SetRemoteAnswer(answer);
 
   // This is as good as it gets in a JSEP test (w/o starting DTLS)
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionOff->GetTransceivers()[0]->mTransport->mDtls->GetRole());
+      mSessionOff->GetTransports()[0]->mDtls->GetRole());
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionAns->GetTransceivers()[0]->mTransport->mDtls->GetRole());
+      mSessionAns->GetTransports()[0]->mDtls->GetRole());
 }
 
 // Verify that missing a=setup in offer gets rejected
 TEST_F(JsepSessionTest, AudioCallOffererNoSetup)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -5733,19 +5562,19 @@ TEST_F(JsepSessionTest, AudioCallAnswerN
   match = answer.find(active);
   ASSERT_NE(match, std::string::npos);
   answer.replace(match, active.length(), "");
   SetRemoteAnswer(answer);
   ASSERT_EQ(kJsepStateStable, mSessionAns->GetState());
 
   // This is as good as it gets in a JSEP test (w/o starting DTLS)
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsServer,
-      mSessionOff->GetTransceivers()[0]->mTransport->mDtls->GetRole());
+      mSessionOff->GetTransports()[0]->mDtls->GetRole());
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionAns->GetTransceivers()[0]->mTransport->mDtls->GetRole());
+      mSessionAns->GetTransports()[0]->mDtls->GetRole());
 }
 
 // Verify that 'holdconn' gets rejected
 TEST_F(JsepSessionTest, AudioCallDtlsRoleHoldconn)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -5899,659 +5728,9 @@ TEST_F(JsepSessionTest, AnswerWithoutVP8
   }
 
   std::string answer = CreateAnswer();
 
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 }
 
-// Ok. Hear me out.
-// The JSEP spec specifies very different behavior for the following two cases:
-// 1. AddTrack either caused a transceiver to be created, or set the send
-// track on a preexisting transceiver.
-// 2. The transceiver was not created as a side-effect of AddTrack, and the
-// send track was put in place by some other means than AddTrack.
-//
-// All together now...
-//
-// SADFACE :(
-//
-// Ok, enough of that. The upshot is we need to test two different codepaths for
-// the same thing here. Most of this unit-test suite tests the "magic" case
-// (case 1 above). Case 2 (the non-magic case) is simpler, so we have just a
-// handful of tests.
-TEST_F(JsepSessionTest, OffererNoAddTrackMagic)
-{
-  types = BuildTypes("audio,video");
-  AddTracks(*mSessionOff, NO_ADDTRACK_MAGIC);
-  AddTracks(*mSessionAns);
-
-  // Offerer's transceivers aren't "magic"; they will not associate with the
-  // remote side's m-sections automatically. But, since they went into the
-  // offer, everything works normally.
-  OfferAnswer();
-
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
-}
-
-TEST_F(JsepSessionTest, AnswererNoAddTrackMagic)
-{
-  types = BuildTypes("audio,video");
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns, NO_ADDTRACK_MAGIC);
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
-  // Since answerer's transceivers aren't "magic", they cannot automatically be
-  // attached to the offerer's m-sections.
-  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
-
-  SwapOfferAnswerRoles();
-
-  OfferAnswer(CHECK_SUCCESS);
-  ASSERT_EQ(4U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
-}
-
-// JSEP has rules about when a disabled m-section can be reused; the gist is
-// that the m-section has to be negotiated disabled, then it becomes a candidate
-// for reuse on the next renegotiation. Stopping a transceiver does not allow
-// you to reuse on the next negotiation.
-TEST_F(JsepSessionTest, OffererRecycle)
-{
-  types = BuildTypes("audio,video");
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns);
-
-  OfferAnswer();
-
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
-  mSessionOff->GetTransceivers()[0]->Stop();
-  AddTracks(*mSessionOff, "audio");
-  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  // It is too soon to recycle msection 0, so the new track should have been
-  // given a new msection.
-  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0]->GetLevel());
-  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers()[2]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
-
-  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
-  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
-  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
-
-  UniquePtr<Sdp> offer = GetParsedLocalDescription(*mSessionOff);
-  ASSERT_EQ(3U, offer->GetMediaSectionCount());
-  ValidateDisabledMSection(&offer->GetMediaSection(0));
-
-  UniquePtr<Sdp> answer = GetParsedLocalDescription(*mSessionAns);
-  ASSERT_EQ(3U, answer->GetMediaSectionCount());
-  ValidateDisabledMSection(&answer->GetMediaSection(0));
-
-  // Ok. Now renegotiating should recycle m-section 0.
-  AddTracks(*mSessionOff, "audio");
-  ASSERT_EQ(4U, mSessionOff->GetTransceivers().size());
-  OfferAnswer(CHECK_SUCCESS);
-
-  // Transceiver 3 should now be attached to m-section 0
-  ASSERT_EQ(4U, mSessionOff->GetTransceivers().size());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
-  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers()[2]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
-  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[3]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[3]->IsStopped());
-
-  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
-  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
-  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[3]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[3]->IsStopped());
-}
-
-TEST_F(JsepSessionTest, RecycleAnswererStopsTransceiver)
-{
-  types = BuildTypes("audio,video");
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns);
-
-  OfferAnswer();
-
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
-  mSessionAns->GetTransceivers()[0]->Stop();
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0]->GetLevel());
-  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
-
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
-  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0]->GetLevel());
-  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
-
-  UniquePtr<Sdp> offer = GetParsedLocalDescription(*mSessionOff);
-  ASSERT_EQ(2U, offer->GetMediaSectionCount());
-
-  UniquePtr<Sdp> answer = GetParsedLocalDescription(*mSessionAns);
-  ASSERT_EQ(2U, answer->GetMediaSectionCount());
-  ValidateDisabledMSection(&answer->GetMediaSection(0));
-
-  // Renegotiating should recycle m-section 0.
-  AddTracks(*mSessionOff, "audio");
-  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
-  OfferAnswer(CHECK_SUCCESS);
-
-  // Transceiver 3 should now be attached to m-section 0
-  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
-  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[1]->IsStopped());
-  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2]->GetLevel());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[2]->IsStopped());
-
-  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[0]->HasLevel());
-  ASSERT_TRUE(mSessionAns->GetTransceivers()[0]->IsStopped());
-  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[1]->IsStopped());
-  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[2]->GetLevel());
-  ASSERT_FALSE(mSessionAns->GetTransceivers()[2]->IsStopped());
-}
-
-// TODO: Have a test where offerer stops, and answerer adds a track and reoffers
-// once Nils' role swap code lands.
-
-// TODO: Have a test where answerer stops and adds a track.
-
-TEST_F(JsepSessionTest, OffererRecycleNoMagic)
-{
-  types = BuildTypes("audio,video");
-  AddTracks(*mSessionOff);
-  AddTracks(*mSessionAns);
-
-  OfferAnswer();
-
-  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
-  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
-  mSessionOff->GetTransceivers()[0]->Stop();
-
-  OfferAnswer(CHECK_SUCCESS);
-
-  // Ok. Now renegotiating should recycle m-section 0.
-  AddTracks(*mSessionOff, "audio", NO_ADDTRACK_MAGIC);
-  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
-  OfferAnswer(CHECK_SUCCESS);
-
-  // Transceiver 2 should now be attached to m-section 0
-  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
-  ASSERT_FALSE(mSessionOff->GetTransceivers()[0]->HasLevel());
-  ASSERT_TRUE(mSessionOff->GetTransceivers()[0]->IsStopped());