Bug 1290948: WIP Transceivers draft
authorByron Campen [:bwc] <docfaraday@gmail.com>
Mon, 27 Mar 2017 09:50:12 -0500
changeset 608484 66401d6e389b6cbc4d10388e0f04135df95e9e6a
parent 607503 09a4282d1172ac255038e7ccacfd772140b219e2
child 637320 06a3683162e2579b2a3058b9fac14577734010ed
push id68294
push userbcampen@mozilla.com
push dateThu, 13 Jul 2017 19:05:14 +0000
bugs1290948
milestone56.0a1
Bug 1290948: WIP Transceivers MozReview-Commit-ID: Ll6vVpSK659
dom/media/PeerConnection.js
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/templates.js
dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
dom/media/tests/mochitest/test_peerConnection_bug1064223.html
dom/media/tests/mochitest/test_peerConnection_constructedStream.html
dom/media/tests/mochitest/test_peerConnection_localRollback.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_setParameters.html
dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html
dom/webidl/MediaStreamList.webidl
dom/webidl/PeerConnectionImpl.webidl
dom/webidl/PeerConnectionObserver.webidl
dom/webidl/RTCPeerConnection.webidl
dom/webidl/RTCRtpReceiver.webidl
dom/webidl/RTCRtpSender.webidl
dom/webidl/RTCRtpTransceiver.webidl
dom/webidl/RTCTrackEvent.webidl
dom/webidl/moz.build
media/mtransport/test/transport_unittests.cpp
media/mtransport/transportlayerice.cpp
media/mtransport/transportlayerice.h
media/webrtc/moz.build
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/signaling.gyp
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/SsrcGenerator.cpp
media/webrtc/signaling/src/jsep/SsrcGenerator.h
media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
media/webrtc/signaling/src/media-conduit/VideoConduit.h
media/webrtc/signaling/src/mediapipeline/MediaPipeline.cpp
media/webrtc/signaling/src/mediapipeline/MediaPipeline.h
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/TransceiverImpl.cpp
media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
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/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -20,28 +20,30 @@ 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);
@@ -344,18 +346,22 @@ setupPrototype(RTCStatsReport, {
         "candidate-pair": "candidatepair",
         "local-candidate": "localcandidate",
         "remote-candidate": "remotecandidate"
   }
 });
 
 class RTCPeerConnection {
   constructor() {
+    // TODO: remove
     this._senders = [];
+    // TODO: remove
     this._receivers = [];
+    this._receiveStreams = new Map();
+    this._transceivers = [];
 
     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
@@ -679,16 +685,34 @@ class RTCPeerConnection {
   // spec. See Bug 831756.
   _checkClosed() {
     if (this._closed) {
       throw new this._win.DOMException("Peer connection is closed",
                                        "InvalidStateError");
     }
   }
 
+  _getTransceiverWithSender(sender) {
+    let transceiver = this.getTransceivers().find(t => t.sender.track == sender.track);
+    if (!transceiver) {
+      throw new this._win.DOMException("This isn't one of my senders!",
+                                       "InvalidAccessError");
+    }
+    return transceiver;
+  }
+
+  _getTransceiverWithReceiver(receiver) {
+    let transceiver = this.getTransceivers().find(t => t.receiver == receiver);
+    if (!transceiver) {
+      throw new this._win.DOMException("This isn't one of my receivers!",
+                                       "InvalidAccessError");
+    }
+    return transceiver;
+  }
+
   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._closed || this._inClose) {
       this.__DOM_IMPL__.dispatchEvent(event);
     }
   }
 
@@ -753,19 +777,80 @@ class RTCPeerConnection {
                           });
   }
 
   createOffer(optionsOrOnSucc, onErr, options) {
     // This entry-point handles both new and legacy call sig. Decipher which one
     if (typeof optionsOrOnSucc == "function") {
       return this._legacy(optionsOrOnSucc, onErr, () => this._createOffer(options));
     }
+    this._ensureTransceiversForOfferToReceive(optionsOrOnSucc);
     return this._async(() => this._createOffer(optionsOrOnSucc));
   }
 
+  _disableReceive(transceiver) {
+    if (transceiver.direction == "sendrecv") {
+      transceiver.setDirection("sendonly");
+      return true;
+    } else if (transceiver.direction == "recvonly") {
+      transceiver.setDirection("inactive");
+      return true;
+    }
+    return false;
+  }
+
+  _enableReceive(transceiver) {
+    if (transceiver.direction == "sendonly") {
+      transceiver.setDirection("sendrecv");
+      return true;
+    } else if (transceiver.direction == "inactive") {
+      transceiver.setDirection("recvonly");
+      return true;
+    }
+    return false;
+  }
+
+  _disableSend(transceiver) {
+    if (transceiver.direction == "sendrecv") {
+      transceiver.setDirection("recvonly");
+      return true;
+    } else if (transceiver.direction == "sendonly") {
+      transceiver.setDirection("inactive");
+      return true;
+    }
+    return false;
+  }
+
+  // This ensures there are at least |count| |kind| transceivers that are
+  // configured to receive. It will create transceivers if necessary.
+  _applyOfferToReceive(kind, count) {
+    this._transceivers.forEach(transceiver => {
+      if (count && transceiver.getKind() == kind && !transceiver.stopped) {
+        this._enableReceive(transceiver);
+        count--;
+      }
+    });
+
+    while (count) {
+      this._addTransceiverNoEvents(kind, {direction: "recvonly"});
+      --count;
+    }
+  }
+
+  // Handles legacy offerToReceiveAudio/Video
+  _ensureTransceiversForOfferToReceive(options) {
+    if (options.offerToReceiveVideo) {
+      this._applyOfferToReceive("video", options.offerToReceiveVideo);
+    }
+
+    if (options.offerToReceiveAudio) {
+      this._applyOfferToReceive("audio", options.offerToReceiveAudio);
+    }
+  }
+
   async _createOffer(options) {
     this._checkClosed();
     let origin = Cu.getWebIDLCallerPrincipal().origin;
     return await this._chain(async () => {
       let haveAssertion;
       if (this._localIdp.enabled) {
         haveAssertion = this._getIdentityAssertion(origin);
       }
@@ -1057,56 +1142,191 @@ 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();
-    this._senders.forEach(sender => {
-      if (sender.track == track) {
-        throw new this._win.DOMException("already added.",
-                                         "InvalidParameterError");
-      }
+
+    if (this._transceivers.some(
+          transceiver => transceiver.sender.track === track)) {
+      throw new this._win.DOMException("invalid stream.", "InvalidAccessError");
+    }
+
+    let transceiver = this._transceivers.find(transceiver => {
+      return transceiver.sender.track === null &&
+             transceiver.getKind() === track.kind &&
+             !transceiver.stopped &&
+             !transceiver.hasBeenUsedToSend();
     });
-    this._impl.addTrack(track, stream);
-    let sender = this._win.RTCRtpSender._create(this._win,
-                                                new RTCRtpSender(this, track,
-                                                                 stream));
-    this._senders.push(sender);
-    return sender;
+
+    if (transceiver) {
+      transceiver.sender.setTrack(track);
+      transceiver.sender.streams = [stream];
+      switch (transceiver.direction) {
+        case "inactive":
+          transceiver.setDirection("sendonly");
+          break;
+        case "recvonly":
+          transceiver.setDirection("sendrecv");
+          break;
+        default:
+      }
+    } else {
+      transceiver = this._addTransceiverNoEvents(
+          track,
+          {
+            streams: [stream],
+            direction: "sendrecv"
+          });
+    }
+
+    transceiver.setAddTrackMagic();
+    this._syncTransceivers();
+    this._updateNegotiationNeeded();
+    return transceiver.sender;
   }
 
   removeTrack(sender) {
     this._checkClosed();
-    var i = this._senders.indexOf(sender);
-    if (i >= 0) {
-      this._senders.splice(i, 1);
-      this._impl.removeTrack(sender.track); // fires negotiation needed
+    let transceiver = this._getTransceiverWithSender(sender);
+    this._impl.removeTrack(sender.track);
+    sender.setTrack(null);
+    this._disableSend(transceiver);
+    this._syncTransceivers();
+    this._updateNegotiationNeeded();
+  }
+
+  mozGetWebrtcTrackId(track) {
+    let matchingTransceiver = this._transceivers.find(
+        transceiver => transceiver.receiver.track == track);
+    if (!matchingTransceiver) {
+      return null;
+    }
+
+    return matchingTransceiver.receiver.webrtcTrackId;
+  }
+
+  mozGetWebrtcStreamId(stream) {
+    let webrtcId = null;
+    this._receiveStreams.forEach((value, key) => {
+      if (value == stream) {
+        webrtcId = key;
+      }
+    });
+
+    return webrtcId;
+  }
+
+  _addTransceiverNoEvents(sendTrackOrKind, init) {
+    let kind = "";
+    let sendTrack = null;
+    if (typeof(sendTrackOrKind) == "string") {
+      kind = sendTrackOrKind;
+      switch (kind) {
+        case "audio":
+        case "video":
+          break;
+        default:
+          throw new this._win.DOMException("Invalid media kind", "TypeError");
+      }
+    } else {
+      sendTrack = sendTrackOrKind;
+      kind = sendTrack.kind;
     }
+
+    let transceiver = this._win.RTCRtpTransceiver._create(
+        this._win,
+        new RTCRtpTransceiver(this, init, kind, sendTrack));
+    this._impl.addTransceiver(transceiver);
+    this._transceivers.push(transceiver);
+    return transceiver;
+  }
+
+  addTransceiver(sendTrackOrKind, init) {
+    let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init);
+    this._updateNegotiationNeeded();
+    return transceiver;
+  }
+
+  _syncTransceivers() {
+    this._impl.syncTransceivers(this._transceivers);
+  }
+
+  _updateNegotiationNeeded() {
+    this._checkClosed();
+    if (this.signalingState != "stable") {
+      return;
+    }
+
+    let negotiationNeeded = this._impl.checkNegotiationNeeded();
+    if (!negotiationNeeded) {
+      this._negotiationNeeded = false;
+      return;
+    }
+
+    if (this._negotiationNeeded) {
+      return;
+    }
+
+    this._negotiationNeeded = true;
+    this.dispatchEvent(new this._win.Event("negotiationneeded"));
+  }
+
+  _getOrCreateStream(id) {
+    if (!this._receiveStreams.has(id)) {
+      // This stream does _not_ have the id that was signalled in SDP. Does it
+      // make sense to fix this up? If we do set the id, will crazy things
+      // happen if a stream id shows up in more than one PeerConnection? I sure
+      // hope we are not expected to handle this case by adding the tracks from
+      // both PCs to the _same_ stream...
+      let stream = new this._win.MediaStream();
+      // 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(sender, tones, duration, interToneGap) {
     return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
   }
 
   _getDTMFToneBuffer(sender) {
     return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
   }
 
   async _replaceTrack(sender, withTrack) {
     this._checkClosed();
-    return await this._chain(() => new Promise((resolve, reject) => {
-      this._onReplaceTrackSender = sender;
-      this._onReplaceTrackWithTrack = withTrack;
-      this._onReplaceTrackSuccess = resolve;
-      this._onReplaceTrackFailure = reject;
-      this._impl.replaceTrack(sender.track, withTrack);
-    }));
+    let transceiver = this._getTransceiverWithSender(sender);
+
+    return await this._chain(() => {
+      if (transceiver.stopped) {
+        return Promise.reject(new this._win.DOMException(
+              "Cannot replace a track on a stopped transceiver",
+              "InvalidStateError"));
+      }
+
+      if (withTrack && withTrack.kind != sender.track.kind) {
+        return Promise.reject(new this._win.DOMException(
+              "Cannot replace a track of kind " + sender.track.kind +
+              " with a track of kind " + withTrack.kind,
+              "TypeError"));
+      }
+
+      return new Promise((resolve, reject) => {
+        this._onReplaceTrackSuccess = resolve;
+        this._onReplaceTrackFailure = reject;
+        this._impl.replaceTrackNoRenegotiation(sender.track, withTrack);
+      });
+    });
   }
 
   _setParameters({ track }, parameters) {
     if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
       return;
     }
     // validate parameters input
     var encodings = parameters.encodings || [];
@@ -1145,40 +1365,50 @@ class RTCPeerConnection {
     this._localIdp.close();
     this._remoteIdp.close();
     this._impl.close();
     this._inClose = false;
   }
 
   getLocalStreams() {
     this._checkClosed();
-    return this._impl.getLocalStreams();
+    let localStreams = new Set();
+    this._transceivers.forEach(transceiver => {
+      transceiver.sender.mozGetStreams().forEach(stream => {
+        localStreams.add(stream);
+      });
+    });
+    return [...localStreams.values()];
   }
 
   getRemoteStreams() {
     this._checkClosed();
-    return this._impl.getRemoteStreams();
+    return [...this._receiveStreams.values()];
   }
 
   getSenders() {
-    return this._senders;
+    return this._transceivers.map(transceiver => transceiver.sender);
   }
 
   getReceivers() {
-    return this._receivers;
+    return this._transceivers.map(transceiver => transceiver.receiver);
   }
 
   mozAddRIDExtension(receiver, extensionId) {
     this._impl.addRIDExtension(receiver.track, extensionId);
   }
 
   mozAddRIDFilter(receiver, rid) {
     this._impl.addRIDFilter(receiver.track, rid);
   }
 
+  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 });
   }
@@ -1270,19 +1500,22 @@ 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.
-    return this._impl.createDataChannel(label, protocol, type, ordered,
-                                        maxPacketLifeTime, maxRetransmits,
-                                        negotiated, id);
+    let dataChannel =
+      this._impl.createDataChannel(label, protocol, type, ordered,
+                                   maxPacketLifeTime, maxRetransmits,
+                                   negotiated, id);
+    this._updateNegotiationNeeded();
+    return dataChannel;
   }
 }
 setupPrototype(RTCPeerConnection, {
   classID: PC_CID,
   contractID: PC_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer]),
   _actions: {
@@ -1339,20 +1572,23 @@ class PeerConnectionObserver {
   }
 
   onCreateAnswerError(code, message) {
     this._dompc._onCreateAnswerFailure(this.newError(message, code));
   }
 
   onSetLocalDescriptionSuccess() {
     this._dompc._onSetLocalDescriptionSuccess();
+    this._dompc._updateNegotiationNeeded();
   }
 
   onSetRemoteDescriptionSuccess() {
+    this._dompc._syncTransceivers();
     this._dompc._onSetRemoteDescriptionSuccess();
+    this._dompc._updateNegotiationNeeded();
   }
 
   onSetLocalDescriptionError(code, message) {
     this._localType = null;
     this._dompc._onSetLocalDescriptionFailure(this.newError(message, code));
   }
 
   onSetRemoteDescriptionError(code, message) {
@@ -1378,16 +1614,17 @@ class PeerConnectionObserver {
     } else {
       candidate = null;
 
     }
     this.dispatchEvent(new win.RTCPeerConnectionIceEvent("icecandidate",
                                                          { candidate }));
   }
 
+  // TODO: Remove?
   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:
@@ -1513,71 +1750,90 @@ 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 }));
   }
 
-  onAddTrack(track, streams) {
+  _getTransceiverWithSendTrack(webrtcTrackId) {
+    return this._dompc.getTransceivers().find(
+        transceiver => transceiver.receiver.webrtcTrackId == webrtcTrackId);
+  }
+
+  onTrack(webrtcTrackId, streamIds) {
     let pc = this._dompc;
-    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 });
+    let matchingTransceiver = this._getTransceiverWithSendTrack(webrtcTrackId);
+    if (!matchingTransceiver) {
+      throw new pc._win.DOMException(
+          "No transceiver with receive track " + webrtcTrackId,
+          "InternalError");
+    }
+
+    // Get or create MediaStreams, and add the new track to them.
+    let streams = streamIds.map(id => this._dompc._getOrCreateStream(id));
+
+    if (!streams.length) {
+      throw new pc._win.DOMException(
+          "No streams for receive track " + webrtcTrackId,
+          "InternalError");
+    }
+
+    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.
+          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 });
     this.dispatchEvent(ev);
 
     // Fire legacy event as well for a little bit.
-    ev = new pc._win.MediaStreamTrackEvent("addtrack", { track });
+    ev = new pc._win.MediaStreamTrackEvent("addtrack",
+        { track: matchingTransceiver.receiver.track });
     this.dispatchEvent(ev);
   }
 
-  onRemoveTrack(track) {
-    let pc = this._dompc;
-    let i = pc._receivers.findIndex(receiver => receiver.track == track);
-    if (i >= 0) {
-      pc._receivers.splice(i, 1);
-    }
+  onTransceiverNeeded(kind) {
+    this._dompc._addTransceiverNoEvents(kind, {direction: "recvonly"});
   }
 
   onReplaceTrackSuccess() {
     var pc = this._dompc;
-    pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack;
-    pc._onReplaceTrackWithTrack = null;
-    pc._onReplaceTrackSender = null;
+    pc._syncTransceivers();
     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(trackId, tone) {
     var pc = this._dompc;
-    var sender = pc._senders.find(({track}) => track.id == trackId);
+    var sender = pc.getSenders().find(({track}) => track.id == trackId);
     sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
                                                                  { tone }));
   }
 }
 setupPrototype(PeerConnectionObserver, {
   classID: PC_OBS_CID,
   contractID: PC_OBS_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
@@ -1616,18 +1872,18 @@ class RTCDTMFSender {
 
   set ontonechange(handler) {
     this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
   }
 
   insertDTMF(tones, duration, interToneGap) {
     this._sender._pc._checkClosed();
 
-    if (this._sender._pc._senders.indexOf(this._sender.__DOM_IMPL__) == -1) {
-      throw new this._sender._pc._win.DOMException("RTCRtpSender is stopped",
+    if (!this._sender.track) {
+      throw new this._sender._pc._win.DOMException("RTCRtpSender has no track",
                                                    "InvalidStateError");
     }
 
     duration = Math.max(40, Math.min(duration, 6000));
     if (interToneGap < 30) interToneGap = 30;
 
     tones = tones.toUpperCase();
 
@@ -1641,61 +1897,258 @@ class RTCDTMFSender {
 }
 setupPrototype(RTCDTMFSender, {
   classID: PC_DTMF_SENDER_CID,
   contractID: PC_DTMF_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpSender {
-  constructor(pc, track, stream) {
+  constructor(pc, track, streams, sendEncodings) {
     let dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
-    Object.assign(this, { _pc: pc, track, _stream: stream, dtmf });
+    let parameters = {
+      encodings : sendEncodings || [
+        {
+          ssrc : null,
+          // rtx and fec are unset if not being used, need to work that out
+          active : true,
+          priority : "medium",
+          // TODO: How do we indicate that we don't want a maxBitrate? Leave
+          // undefined? Pick a huge number?
+          degradationPreference : "balanced",
+          // TODO: Leave rid unset?
+          scaleResolutionDownBy : 1.0
+        }
+      ],
+      headerExtensions : [
+        // This is 100% controlled by code in Gecko right now.
+      ],
+      rtcp : {
+        cname : null, // Chosen by Gecko code
+        reducedSize : false // TODO: Do we support this?
+      },
+      codecs : [
+        // Controlled by Gecko right now.
+      ]
+    };
+
+    Object.assign(this, {
+      _pc: pc,
+      track,
+      _streams: streams,
+      dtmf,
+      parameters });
   }
 
   replaceTrack(withTrack) {
-    return this._pc._async(() => this._pc._replaceTrack(this, withTrack));
+    return this._pc._async(() =>
+        this._pc._replaceTrack(this, withTrack)
+          .then(() => this.track = withTrack));
   }
 
   setParameters(parameters) {
-    return this._pc._win.Promise.resolve()
-      .then(() => this._pc._setParameters(this, parameters));
+    let copy = Object.create(parameters);
+
+    return this._pc._win.Promise.resolve().then(() => {
+      this._pc._checkClosed();
+
+      if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
+        return;
+      }
+
+      copy.encodings = copy.encodings || [];
+
+      copy.encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
+        if (scaleResolutionDownBy < 1.0) {
+          throw new this._pc._win.RangeError("scaleResolutionDownBy must be >= 1.0");
+        }
+        if (!rid && copy.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;
+      }, {});
+
+      let transceiver = this._pc._getTransceiverWithSender(this);
+      if (!transceiver) {
+        throw new this._pc._win.DOMException(
+            "No transceiver with this sender", "InternalError");
+      }
+
+      if (transceiver.stopped) {
+        throw new this._pc._win.DOMException(
+            "This sender's transceiver is stopped", "InvalidStateError");
+      }
+
+      //if (copy.encodings.length != this.parameters.encodings.length) {
+      //  throw new this._pc._win.DOMException(
+      //      "Encodings cannot be added/removed by setParameters. You will need " +
+      //      "to stop this transceiver and create a new one.",
+      //      "InvalidModificationError");
+      //}
+
+      //let readonly = ['ssrc', 'fec', 'rtx', 'rid'];
+      //readonly.forEach(key => {
+      //  if (this.parameters[key] !== copy[key]) {
+      //    throw new this._pc._win.DOMException(
+      //        "Parameter " + key + " is read-only", "InvalidModificationError");
+      //  }
+      //});
+
+      copy.encodings.forEach(encoding => {
+        if (typeof encoding.scaleResolutionDownBy === "number" &&
+            encoding.scaleResolutionDownBy < 1.0) {
+          throw new this._pc._win.DOMException(
+              "scaleResolutionDownBy must not be less than 1",
+              "RangeError");
+        }
+      })
+
+      // TODO: transaction ids
+
+      this.parameters = copy;
+      this._pc._syncTransceivers();
+    });
   }
 
   getParameters() {
-    return this._pc._getParameters(this);
+    // TODO: transaction ids
+
+    // All the other stuff that the spec says to update is handled when
+    // transceivers are synced.
+    return this.parameters;
+  }
+
+  mozGetStreams() {
+    return this._streams;
+  }
+
+  setTrack(track) {
+    this.track = track;
   }
 
   getStats() {
     return this._pc._async(
       async () => this._pc._getStats(this.track));
   }
 }
 setupPrototype(RTCRtpSender, {
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpReceiver {
-  constructor(pc, track) {
-    Object.assign(this, { _pc: pc, track });
+  constructor(pc, kind) {
+    // We do not set the track here; that is done by PeerConnectionImpl inside
+    // addTransceiver
+    Object.assign(this,
+        {
+          _pc: pc,
+          _kind: kind,
+          webrtcTrackId: ""
+        });
+  }
+
+  setTrack(track) {
+    this.track = track;
+  }
+
+  getKind() {
+    return this._kind;
   }
 
   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, init, kind, sendTrack) {
+    let receiver = pc._win.RTCRtpReceiver._create(
+        pc._win, new RTCRtpReceiver(pc, kind));
+    let streams = (init && init.streams) || [];
+    let sender = pc._win.RTCRtpSender._create(
+        pc._win, new RTCRtpSender(pc, sendTrack, streams));
+
+    let direction = (init && init.direction) || "sendrecv";
+    Object.assign(this,
+        {
+          mid: null,
+          sender,
+          receiver,
+          stopped: false,
+          direction,
+          currentDirection: null,
+          addTrackMagic: false,
+          _hasBeenUsedToSend: false,
+          // the receiver starts out without a track, so record this here
+          _kind: kind
+        });
+  }
+
+  setDirection(direction) {
+    this.direction = direction;
+    // TODO: This needs to cause negotiationneeded to fire.
+  }
+
+  setCurrentDirection(direction) {
+    switch (direction) {
+      case "sendrecv":
+      case "sendonly":
+        this._hasBeenUsedToSend = true;
+        break;
+      default:
+        ;
+    }
+
+    this.currentDirection = direction;
+  }
+
+  stop() {
+    this.stopped = true;
+    // TODO: negotiation needed
+  }
+
+  setMid(mid) {
+    this.mid = mid;
+  }
+
+  unsetMid() {
+    this.mid = null;
+  }
+
+  getKind() {
+    return this._kind;
+  }
+
+  hasBeenUsedToSend() {
+    return this._hasBeenUsedToSend;
+  }
+
+  setAddTrackMagic() {
+    this.addTrackMagic = true;
+  }
+}
+
+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,
@@ -1706,12 +2159,13 @@ this.NSGetFactory = XPCOMUtils.generateN
   [GlobalPCList,
    RTCDTMFSender,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCPeerConnectionStatic,
    RTCRtpReceiver,
    RTCRtpSender,
+   RTCRtpTransceiver,
    RTCStatsReport,
    PeerConnectionObserver,
    CreateOfferRequest]
 );
--- 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("ended fired for track " + receiver.track.id);
+              info(pc + " 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,18 +756,21 @@ 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.expectedRemoteTrackInfoById = {};
+  this.expectedSignalledTrackInfoById = {};
+  // this.expectedRemoteTrackInfoById = {};
   this.observedRemoteTrackInfoById = {};
 
   this.disableRtpCountChecking = false;
 
   this.iceConnectedResolve;
   this.iceConnectedReject;
   this.iceConnected = new Promise((resolve, reject) => {
     this.iceConnectedResolve = resolve;
@@ -890,36 +893,64 @@ PeerConnectionWrapper.prototype = {
   get iceConnectionState() {
     return this._pc.iceConnectionState;
   },
 
   setIdentityProvider: function(provider, protocol, identity) {
     this._pc.setIdentityProvider(provider, protocol, identity);
   },
 
-  ensureMediaElement : function(track, direction) {
+  getMediaElementForTrack : function(track, direction)
+  {
     const idPrefix = [this.label, direction].join('_');
-    var element = getMediaElementForTrack(track, idPrefix);
+    return getMediaElementForTrack(track, idPrefix);
+  },
 
+  createMediaElementForTrack : function(track, direction)
+  {
+    const idPrefix = [this.label, direction].join('_');
+    return createMediaElementForTrack(track, idPrefix);
+  },
+
+  ensureMediaElement : function(track, direction) {
+    var element = this.getMediaElementForTrack(track, direction);
     if (!element) {
-      element = createMediaElementForTrack(track, idPrefix);
+      element = this.createMediaElementForTrack(track, direction);
       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
@@ -936,16 +967,20 @@ 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;
   },
@@ -971,42 +1006,47 @@ PeerConnectionWrapper.prototype = {
     } 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");
     });
   },
 
   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(index, withTrack, withStreamId) {
-    var sender = this._pc.getSenders()[index];
+  senderReplaceTrack : function(sender, withTrack, stream) {
     delete this.expectedLocalTrackInfoById[sender.track.id];
     this.expectedLocalTrackInfoById[withTrack.id] = {
         type: withTrack.kind,
-        streamId: withStreamId
+        streamId: stream.id
       };
+    this.addSendStream(stream);
+    this.ensureMediaElement(withTrack, 'local');
     return sender.replaceTrack(withTrack);
   },
 
   /**
    * Requests all the media streams as specified in the constrains property.
    *
    * @param {array} constraintsList
    *        Array of constraints for GUM calls
@@ -1179,44 +1219,59 @@ 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(track,
+  checkTrackIsExpected : function(trackId,
+                                  kind,
                                   expectedTrackInfoById,
                                   observedTrackInfoById) {
-    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;
+    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;
     is(observedKind, expectedKind,
-        "track id " + track.id + " was of kind " +
+        "track id " + trackId + " was of kind " +
         observedKind + ", which matches " + expectedKind);
-    observedTrackInfoById[track.id] = expectedTrackInfoById[track.id];
+    observedTrackInfoById[trackId] = expectedTrackInfoById[trackId];
   },
 
   isTrackOnPC: function(track) {
-    return this._pc.getRemoteStreams().some(s => !!s.getTrackById(track.id));
+    return !!this.getStreamForRecvTrack(track);
   },
 
   allExpectedTracksAreObserved: function(expected, observed) {
     return Object.keys(expected).every(trackId => observed[trackId]);
   },
 
   setupTrackEventHandler: function() {
     this._pc.addEventListener('track', event => {
-      info(this + ": 'ontrack' event fired for " + JSON.stringify(event.track));
+      info(this + ": 'ontrack' event fired for " + event.track.id +
+                  "(SDP msid is " + this._pc.mozGetWebrtcTrackId(event.track) +
+                  ")");
 
-      this.checkTrackIsExpected(event.track,
-                                this.expectedRemoteTrackInfoById,
-                                this.observedRemoteTrackInfoById);
+// 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.
+//      this.checkTrackIsExpected(this._pc.mozGetWebrtcTrackId(event.track),
+//                                event.track.kind,
+//                                this.expectedRemoteTrackInfoById,
+//                                this.observedRemoteTrackInfoById);
+      let trackId = this._pc.mozGetWebrtcTrackId(event.track);
+      ok(!this.observedRemoteTrackInfoById[trackId],
+         "track id " + trackId + " was not yet observed");
+      this.observedRemoteTrackInfoById[trackId] = {
+        type: event.track.kind
+      };
       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
@@ -1339,53 +1394,58 @@ 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 => {
-      this.checkTrackIsExpected(sender.track, this.expectedLocalTrackInfoById, observed);
+      if (sender.track) {
+        this.checkTrackIsExpected(sender.track.id,
+                                  sender.track.kind,
+                                  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));
+    // 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));
+    //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.expectedLocalTrackInfoById,
+    checkSdpForMsids(this.localDescription, this.expectedSignalledTrackInfoById,
                      "local");
-    checkSdpForMsids(this.remoteDescription, this.expectedRemoteTrackInfoById,
-                     "remote");
+    //checkSdpForMsids(this.remoteDescription, this.expectedRemoteTrackInfoById,
+    //                 "remote");
   },
 
   markRemoteTracksAsNegotiated: function() {
     Object.values(this.observedRemoteTrackInfoById).forEach(
         trackInfo => trackInfo.negotiated = true);
   },
 
   rollbackRemoteTracksIfNotNegotiated: function() {
@@ -1476,32 +1536,65 @@ 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._pc.mozGetWebrtcTrackId(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)),
-      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))));
+      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))));
   },
 
   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) {
@@ -1537,64 +1630,104 @@ 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. Uses WebAudio AnalyserNodes to compare input and output
-   * audio data in the frequency domain.
+   * PeerConnection for each transceiver that should be receiving. 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 from |from|.
+   *        A promise that resolves when we're receiving the tone/s from |from|.
    */
   checkReceivingToneFrom : function(audiocontext, from) {
-    var inputElem = from.localMediaElements[0];
+    var localTransceivers = this._pc.getTransceivers()
+      .filter(t => t.mid)
+      .sort((t1, t2) => t1.mid < t2.mid);
+    var remoteTransceivers = from._pc.getTransceivers()
+      .filter(t => t.mid)
+      .sort((t1, t2) => t1.mid < t2.mid);
+    var promises = [];
 
-    // As input we use the stream of |from|'s first available audio sender.
-    var inputSenderTracks = from._pc.getSenders().map(sn => sn.track);
-    var inputAudioStream = from._pc.getLocalStreams()
-      .find(s => inputSenderTracks.some(t => t.kind == "audio" && s.getTrackById(t.id)));
-    var inputAnalyser = new AudioStreamAnalyser(audiocontext, inputAudioStream);
-
-    // It would have been nice to have a working getReceivers() here, but until
-    // we do, let's use what remote streams we have.
-    var outputAudioStream = this._pc.getRemoteStreams()
-      .find(s => s.getAudioTracks().length > 0);
-    var outputAnalyser = new AudioStreamAnalyser(audiocontext, outputAudioStream);
+    is(localTransceivers.length, remoteTransceivers.length,
+       "Same number of associated transceivers on remote and local.");
 
-    var maxWithIndex = (a, b, i) => (b >= a.value) ? { value: b, index: i } : a;
-    var initial = { value: -1, index: -1 };
-
-    return new Promise((resolve, reject) => inputElem.ontimeupdate = () => {
-      var inputData = inputAnalyser.getByteFrequencyData();
-      var outputData = outputAnalyser.getByteFrequencyData();
+    for (var i = 0; i < localTransceivers.length; i++) {
+      is(localTransceivers[i].receiver.track.kind,
+         remoteTransceivers[i].receiver.track.kind,
+         "Transceivers at index " + i + " are the same kind.");
 
-      var inputMax = inputData.reduce(maxWithIndex, initial);
-      var outputMax = outputData.reduce(maxWithIndex, initial);
-      info("Comparing maxima; input[" + inputMax.index + "] = " + inputMax.value +
-           ", output[" + outputMax.index + "] = " + outputMax.value);
-      if (!inputMax.value || !outputMax.value) {
-        return;
+      if (localTransceivers[i].receiver.track.kind != "audio") {
+        continue;
+      }
+
+      if (!remoteTransceivers[i].sender.track) {
+        continue;
+      }
+
+      if (remoteTransceivers[i].currentDirection == "recvonly" ||
+          remoteTransceivers[i].currentDirection == "inactive") {
+        continue;
       }
 
-      // When the input and output maxima are within reasonable distance
-      // from each other, we can be sure that the input tone has made it
-      // through the peer connection.
-      if (Math.abs(inputMax.index - outputMax.index) < 10) {
-        ok(true, "input and output audio data matches");
-        inputElem.ontimeupdate = null;
-        resolve();
-      }
-    });
+      var sendTrack = remoteTransceivers[i].sender.track;
+      var inputElem = from.getMediaElementForTrack(sendTrack, "local");
+      ok(inputElem,
+         "Remote wrapper should have a media element for track id " +
+         sendTrack.id);
+      var inputAudioStream = from.getStreamForSendTrack(sendTrack);
+      ok(inputAudioStream,
+         "Remote wrapper should have a stream for track id " + sendTrack.id);
+      var inputAnalyser =
+        new AudioStreamAnalyser(audiocontext, inputAudioStream);
+
+      var recvTrack = localTransceivers[i].receiver.track;
+      var outputAudioStream = this.getStreamForRecvTrack(recvTrack);
+      ok(outputAudioStream,
+         "Local wrapper should have a stream for track id " + recvTrack.id);
+      var outputAnalyser =
+        new AudioStreamAnalyser(audiocontext, outputAudioStream);
+
+      var maxWithIndex = (a, b, i) => (b >= a.value) ? { value: b, index: i } : a;
+      var initial = { value: -1, index: -1 };
+
+      promises.push(
+          new Promise((resolve, reject) => inputElem.ontimeupdate = () => {
+        var inputData = inputAnalyser.getByteFrequencyData();
+        var outputData = outputAnalyser.getByteFrequencyData();
+
+        var inputMax = inputData.reduce(maxWithIndex, initial);
+        var outputMax = outputData.reduce(maxWithIndex, initial);
+        info("Comparing maxima; input[" + inputMax.index + "] = " + inputMax.value +
+             ", output[" + outputMax.index + "] = " + outputMax.value);
+        if (!inputMax.value || !outputMax.value) {
+          return;
+        }
+
+        // When the input and output maxima are within reasonable distance
+        // from each other, we can be sure that the input tone has made it
+        // through the peer connection.
+        if (Math.abs(inputMax.index - outputMax.index) < 10) {
+          ok(true, "input and output audio data matches");
+          inputElem.ontimeupdate = null;
+          resolve();
+        }
+      }));
+    }
+
+    isnot(promises.length, 0, "Found at least one audio transceiver to check.");
+
+    return Promise.all(promises);
   },
 
   /**
    * Get stats from the "legacy" getStats callback interface
    */
   getStatsLegacy : function(selector, onSuccess, onFail) {
     let wrapper = stats => {
       info(this + ": Got legacy stats: " + JSON.stringify(stats));
@@ -1632,16 +1765,17 @@ 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
@@ -1665,21 +1799,27 @@ PeerConnectionWrapper.prototype = {
       if (res.isRemote) {
         continue;
       }
       counters[res.type] = (counters[res.type] || 0) + 1;
 
       switch (res.type) {
         case "inbound-rtp":
         case "outbound-rtp": {
-          // 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");
+          // 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");
+          }
 
           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");
@@ -1744,17 +1884,22 @@ 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 = Object.keys(this.expectedRemoteTrackInfoById).length;
+    var nin = this._pc.getTransceivers()
+      .filter(t => {
+        return !t.stopped &&
+               (t.currentDirection != "inactive") &&
+               (t.currentDirection != "sendonly");
+      }).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)");
@@ -1820,49 +1965,46 @@ 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,
-      offerConstraintsList, offerOptions, testOptions) {
+  checkStatsIceConnections : function(stats, 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 {
-      // 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 numAudioTransceivers =
+        this._pc.getTransceivers().filter((transceiver) => {
+          return (!transceiver.stopped) && transceiver.receiver.track.kind == "audio";
+        }).length;
 
-      var numVideoTracks =
-          sdputils.countTracksInConstraint('video', offerConstraintsList) ||
-          ((offerOptions && offerOptions.offerToReceiveVideo) ? 1 : 0);
+      var numVideoTransceivers =
+        this._pc.getTransceivers().filter((transceiver) => {
+          return (!transceiver.stopped) && transceiver.receiver.track.kind == "video";
+        }).length;
 
-      var numExpectedTransports = numAudioTracks + numVideoTracks;
+      var numExpectedTransports = numAudioTransceivers + numVideoTransceivers;
       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,18 +78,17 @@ 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, rtpSenderOrReceiver, outbound) {
-  var track = rtpSenderOrReceiver.track;
+function checkTrackStats(pc, track, outbound) {
   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"
@@ -101,18 +100,18 @@ function checkTrackStats(pc, rtpSenderOr
     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._pc.getSenders().map(sender => checkTrackStats(pc, sender, true)),
-    pc._pc.getReceivers().map(receiver => checkTrackStats(pc, receiver, false))));
+    pc.getExpectedActiveReceiveTracks().map(track => checkTrackStats(pc, track, false)),
+    pc.getExpectedSendTracks().map(track => checkTrackStats(pc, track, true))));
 }
 
 // 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();
@@ -209,41 +208,41 @@ 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_GET_EXPECTED_REMOTE_TRACKS(test) {
+//    if (test.testOptions.steeplechase) {
+//      return test.getSignalingMessage("remote_expected_tracks").then(
+//          message => {
+//            test.pcLocal.expectedRemoteTrackInfoByMid = message.expected_tracks;
+//          });
+//    }
+//
+//    // Deep copy, as similar to steeplechase as possible
+//    test.pcLocal.expectedRemoteTrackInfoByMid =
+//      JSON.parse(JSON.stringify(test.pcRemote.expectedLocalTrackInfoByMid));
+//  },
+//
+//  function PC_REMOTE_GET_EXPECTED_REMOTE_TRACKS(test) {
+//    if (test.testOptions.steeplechase) {
+//      return test.getSignalingMessage("local_expected_tracks").then(
+//          message => {
+//            test.pcRemote.expectedRemoteTrackInfoByMid = message.expected_tracks;
+//          });
+//    }
+//
+//    // Deep copy, as similar to steeplechase as possible
+//    test.pcRemote.expectedRemoteTrackInfoByMid =
+//      JSON.parse(JSON.stringify(test.pcLocal.expectedLocalTrackInfoByMid));
+//  },
 
   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");
     });
   },
 
@@ -430,29 +429,23 @@ 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._offer_constraints,
-                                            test._offer_options,
-                                            test.testOptions);
+      test.pcLocal.checkStatsIceConnections(stats, test.testOptions);
     });
   },
 
   function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
     return test.pcRemote.getStats().then(stats => {
-      test.pcRemote.checkStatsIceConnections(stats,
-                                             test._offer_constraints,
-                                             test._offer_options,
-                                             test.testOptions);
+      test.pcRemote.checkStatsIceConnections(stats, 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_addtrack_removetrack_events.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
@@ -15,53 +15,54 @@ 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");
 
-          const addTrackPromise = haveEvent(remoteStream, "addtrack",
-              wait(50000, new Error("No addtrack event")))
+          eventsPromise = haveEvent(remoteStream, "addtrack",
+              wait(50000, new Error("No addtrack event for " + newTrack.id)))
             .then(trackEvent => {
               ok(trackEvent instanceof MediaStreamTrackEvent,
                  "Expected event to be instance of MediaStreamTrackEvent");
               is(trackEvent.type, "addtrack",
                  "Expected addtrack event type");
-              is(trackEvent.track.id, newTrack.id, "Expected track in event");
+              is(test.pcRemote._pc.mozGetWebrtcTrackId(trackEvent.track), 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_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, "InternalError",
+                                 e => is(e.name, "InvalidStateError",
                                          "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
@@ -36,26 +36,26 @@ runNetworkTest(() => {
         .then(stream => dummyStreamTracks.push(...stream.getTracks()))
         .then(() => dummyStreamTracks.forEach(t =>
           test.pcLocal.attachLocalTrack(t, dummyStream)));
     },
   ]);
 
   let checkSentTracksReceived = (sentStreamId, sentTracks) => {
     let receivedStream =
-      test.pcRemote._pc.getRemoteStreams().find(s => s.id == sentStreamId);
+      test.pcRemote._pc.getRemoteStreams().find(s => test.pcRemote._pc.mozGetWebrtcStreamId(s) == sentStreamId);
     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 == t2.id),
+      ok(receivedStream.getTracks().find(t2 => t.id == test.pcRemote._pc.mozGetWebrtcTrackId(t2)),
          "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_localRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_localRollback.html
@@ -18,16 +18,19 @@
     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_removeAudioTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
@@ -32,20 +32,21 @@
         },
         function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
           test.setOfferOptions({ offerToReceiveAudio: true });
           return test.pcLocal.removeSender(0);
         },
       ],
       [
         function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
-          is(test.pcRemote._pc.getReceivers().length, 0,
-             "pcRemote should have no more receivers");
-          is(receivedTrack.readyState, "ended",
-             "The received track should have ended");
+          // 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");
 
           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
@@ -29,26 +29,35 @@
           // 0, but the remote side will keep its old pipeline and packet
           // count.
           test.pcLocal.disableRtpCountChecking = true;
           return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          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");
+          is(test.pcRemote._pc.getTransceivers().length, 2,
+              "pcRemote should have two transceivers");
+          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
 
           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,16 +7,18 @@
 <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,29 +28,44 @@
           // 0, but the remote side will keep its old pipeline and packet
           // count.
           test.pcLocal.disableRtpCountChecking = true;
           return test.pcLocal.removeSender(0);
         },
         function PC_LOCAL_ADD_AUDIO_TRACK(test) {
           return test.pcLocal.getAllUserMedia([{audio: true}]);
         },
+        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+          test.pcLocal.expectIceChecking();
+        },
+        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+          test.pcRemote.expectIceChecking();
+        },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          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");
+          is(test.pcRemote._pc.getTransceivers().length, 2,
+              "pcRemote should have two transceivers");
+          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
 
           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
@@ -33,28 +33,33 @@
         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.getAllUserMedia([{video: true, fake: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          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");
+          is(test.pcRemote._pc.getTransceivers().length, 2,
+              "pcRemote should have two transceivers");
+          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
 
-          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, 10, 10, 16);
         },
+        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,16 +8,18 @@
 <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,31 +32,42 @@
           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.getAllUserMedia([{video: true, fake: true}]);
         },
+        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+          test.pcLocal.expectIceChecking();
+        },
+        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+          test.pcRemote.expectIceChecking();
+        },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
-          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");
+          is(test.pcRemote._pc.getTransceivers().length, 2,
+              "pcRemote should have two transceivers");
+          const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
 
-          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, 10, 10, 16);
         },
+        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,12 +1,13 @@
 <!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"
   });
@@ -31,22 +32,24 @@
         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.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");
+          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);
         },
       ]
     );
 
     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,29 +42,31 @@
     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");
-        return sender.replaceTrack(newTrack);
+        // Use wrapper function, since it updates expected tracks
+        return wrapper.senderReplaceTrack(sender, newTrack, newStream);
       })
       .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");
-        ok(pc.getLocalStreams().some(s => s.getTracks()
+        // Spec does not say we add this new track to any stream
+        ok(!pc.getLocalStreams().some(s => s.getTracks()
                                            .some(t => t == sender.track)),
-           "track exists among pc's local streams");
+           "track does not exist among pc's local streams");
         return sender.replaceTrack(audiotrack)
           .then(() => ok(false, "replacing with different kind should fail"),
-                e => is(e.name, "IncompatibleMediaStreamTrackError",
+                e => is(e.name, "TypeError",
                         "replacing with different kind should fail"));
       });
   }
 
   runNetworkTest(function () {
     test = new PeerConnectionTest();
     test.audioCtx = new AudioContext();
     test.setMediaConstraints([{video: true, audio: true}], [{video: true}]);
@@ -125,53 +127,65 @@
         // (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 sender.replaceTrack(newTrack)
+        return test.pcLocal.senderReplaceTrack(
+            sender, newTrack, destNode.stream)
           .then(() => {
             is(pc.getSenders().length, oldSenderCount, "same sender count");
             ok(!pc.getSenders().some(sn => sn.track == oldTrack),
                "Replaced track should be removed from senders");
-            ok(allLocalStreamsHaveSender(pc),
-               "Shouldn't have any streams without a corresponding sender");
+            // Current spec says nothing about removing streams whose tracks
+            // aren't associated with the PC anymore. This may change, though.
+            //ok(allLocalStreamsHaveSender(pc),
+            //   "Shouldn't have any streams without a corresponding sender");
             is(sender.track, newTrack, "sender.track has been replaced");
-            ok(pc.getLocalStreams().some(s => s.getTracks()
+            // Spec does not say we add this new track to any stream
+            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) {
-        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.pcLocal._pc.getTransceivers()
+          .filter(transceiver => {
+            return !transceiver.stopped &&
+                   transceiver.receiver.track.kind == "video" &&
+                   transceiver.sender.track;
+          })
+          .forEach(transceiver => {
+            var stream = transceiver.sender.mozGetStreams()[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");
+            }
+            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");
+            }
+          });
       }
     ]);
     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,60 +31,38 @@
     ]);
     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];
-          return test.pcLocal.senderReplaceTrack(0, newtrack, newstream.id)
+          var sender = test.pcLocal._pc.getSenders()[0];
+          return test.pcLocal.senderReplaceTrack(sender, newtrack, newstream)
             .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_ENDED(test) {
+        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;
+
           const vremote = test.pcRemote.remoteMediaElements.find(
-              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));
+              elem => elem.id.includes(track.id));
           if (!vremote) {
             return Promise.reject(new Error("Couldn't find video element"));
           }
-          return addFinallyToPromise(helper.checkVideoPlaying(vremote, 10, 10, 16))
-            .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"));
-          }
+          ok(!vremote.ended, "Original track should not have ended after renegotiation (replaceTrack is not signalled!)");
           return helper.checkVideoPlaying(vremote, 10, 10, 16);
-        },
+        }
       ]
     );
 
     test.run();
    });
   });
 
 </script>
--- a/dom/media/tests/mochitest/test_peerConnection_setParameters.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setParameters.html
@@ -12,20 +12,21 @@ 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]));
     };
 
--- a/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
@@ -14,35 +14,23 @@
   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,35 +14,23 @@
   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>
deleted file mode 100644
--- a/dom/webidl/MediaStreamList.webidl
+++ /dev/null
@@ -1,11 +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/.
- */
-
-[ChromeOnly]
-interface MediaStreamList {
-  getter MediaStream? (unsigned long index);
-  readonly attribute unsigned long length;
-};
--- a/dom/webidl/PeerConnectionImpl.webidl
+++ b/dom/webidl/PeerConnectionImpl.webidl
@@ -40,34 +40,33 @@ interface PeerConnectionImpl  {
   void getStats(MediaStreamTrack? selector);
 
   /* Adds the tracks created by GetUserMedia */
   [Throws]
   void addTrack(MediaStreamTrack track, MediaStream... streams);
   [Throws]
   void removeTrack(MediaStreamTrack track);
   [Throws]
+  void addTransceiver(RTCRtpTransceiver transceiver);
+  [Throws]
+  void syncTransceivers(sequence<RTCRtpTransceiver> transceivers);
+  [Throws]
+  boolean checkNegotiationNeeded();
+  [Throws]
   void insertDTMF(RTCRtpSender sender, DOMString tones,
                   optional unsigned long duration = 100,
                   optional unsigned long interToneGap = 70);
   [Throws]
   DOMString getDTMFToneBuffer(RTCRtpSender sender);
   [Throws]
-  void replaceTrack(MediaStreamTrack thisTrack, MediaStreamTrack withTrack);
-  [Throws]
-  void setParameters(MediaStreamTrack track,
-                     optional RTCRtpParameters parameters);
-  [Throws]
-  RTCRtpParameters getParameters(MediaStreamTrack track);
+  void replaceTrackNoRenegotiation(MediaStreamTrack thisTrack,
+                                   MediaStreamTrack withTrack);
   [Throws]
   void closeStreams();
 
-  sequence<MediaStream> getLocalStreams();
-  sequence<MediaStream> getRemoteStreams();
-
   void addRIDExtension(MediaStreamTrack recvTrack, unsigned short extensionId);
   void addRIDFilter(MediaStreamTrack recvTrack, DOMString rid);
 
   /* As the ICE candidates roll in this one should be called each time
    * in order to keep the candidate list up-to-date for the next SDP-related
    * call PeerConnectionImpl does not parse ICE candidates, just sticks them
    * into the SDP.
    */
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -35,16 +35,18 @@ interface PeerConnectionObserver
 
   /* 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 onAddTrack(MediaStreamTrack track, sequence<MediaStream> streams);
-  void onRemoveTrack(MediaStreamTrack track);
+  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);
 
   /* DTMF callback */
   void onDTMFToneChange(DOMString trackId, DOMString tone);
 };
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -111,18 +111,28 @@ 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);
 
+  // Gets the track id that was in the SDP for this track
+  DOMString mozGetWebrtcTrackId(MediaStreamTrack track);
+
+  // Gets the stream id that was in the SDP for this stream
+  DOMString mozGetWebrtcStreamId(MediaStream stream);
+
+  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);
 
   void close ();
   attribute EventHandler onnegotiationneeded;
--- a/dom/webidl/RTCRtpReceiver.webidl
+++ b/dom/webidl/RTCRtpReceiver.webidl
@@ -7,9 +7,16 @@
  * http://lists.w3.org/Archives/Public/public-webrtc/2014May/0067.html
  */
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/rtpreceiver;1"]
 interface RTCRtpReceiver {
   readonly attribute MediaStreamTrack track;
   Promise<RTCStatsReport> getStats();
+
+  [ChromeOnly]
+  void setTrack(MediaStreamTrack track);
+  [ChromeOnly]
+  DOMString getKind();
+  [ChromeOnly]
+  attribute DOMString? webrtcTrackId;
 };
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -64,16 +64,19 @@ 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 track);
   Promise<RTCStatsReport> getStats();
   [Pref="media.peerconnection.dtmf.enabled"]
   readonly attribute RTCDTMFSender? dtmf;
+  sequence<MediaStream> mozGetStreams();
+  [ChromeOnly]
+  void setTrack(MediaStreamTrack? track);
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCRtpTransceiver.webidl
@@ -0,0 +1,52 @@
+/* -*- 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;
+    // 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;
+    readonly attribute RTCRtpTransceiverDirection  direction;
+    readonly attribute RTCRtpTransceiverDirection? currentDirection;
+    [ChromeOnly]
+    void setAddTrackMagic();
+    [ChromeOnly]
+    readonly attribute boolean addTrackMagic;
+    void setDirection(RTCRtpTransceiverDirection direction);
+    void stop();
+    // void setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
+    [ChromeOnly]
+    void setCurrentDirection(RTCRtpTransceiverDirection direction);
+    [ChromeOnly]
+    void setMid(DOMString mid);
+    [ChromeOnly]
+    void unsetMid();
+    [ChromeOnly]
+    DOMString getKind();
+    [ChromeOnly]
+    boolean hasBeenUsedToSend();
+};
+
--- a/dom/webidl/RTCTrackEvent.webidl
+++ b/dom/webidl/RTCTrackEvent.webidl
@@ -6,22 +6,24 @@
  * 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;
 };
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -200,19 +200,16 @@ 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")
@@ -982,31 +979,31 @@ 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',
         'WebrtcDeprecated.webidl',
         'WebrtcGlobalInformation.webidl',
     ]
 
 if CONFIG['MOZ_WEBSPEECH']:
     WEBIDL_FILES += [
         'SpeechGrammar.webidl',
--- 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(name);
-    ice_->SetParameters(ice_ctx_->ctx(), stream, 1);
+    ice_ = new TransportLayerIce();
+    ice_->SetParameters(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,52 +79,47 @@ extern "C" {
 namespace mozilla {
 
 #ifdef ERROR
 #undef ERROR
 #endif
 
 MOZ_MTLOG_MODULE("mtransport")
 
-TransportLayerIce::TransportLayerIce(const std::string& name)
-    : name_(name),
-      ctx_(nullptr), stream_(nullptr), component_(0),
+TransportLayerIce::TransportLayerIce()
+    : 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<NrIceCtx> ctx,
-                                      RefPtr<NrIceMediaStream> stream,
+void TransportLayerIce::SetParameters(RefPtr<NrIceMediaStream> stream,
                                       int component) {
   // If SetParameters is called and we already have a stream_, this means
   // we're handling an ICE restart.  We need to hold the old stream until
   // we know the new stream is working.
   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,22 +25,21 @@
 #include "transportflow.h"
 #include "transportlayer.h"
 
 // An ICE transport layer -- corresponds to a single ICE
 namespace mozilla {
 
 class TransportLayerIce : public TransportLayer {
  public:
-  explicit TransportLayerIce(const std::string& name);
+  TransportLayerIce();
 
   virtual ~TransportLayerIce();
 
-  void SetParameters(RefPtr<NrIceCtx> ctx,
-                     RefPtr<NrIceMediaStream> stream,
+  void SetParameters(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;
 
@@ -52,18 +51,16 @@ 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/moz.build
+++ b/media/webrtc/moz.build
@@ -95,21 +95,20 @@ if CONFIG['MOZ_WEBRTC_SIGNALING']:
         'signaling/src/jsep/JsepSessionImpl.cpp',
         'signaling/src/media-conduit/AudioConduit.cpp',
         'signaling/src/media-conduit/MediaCodecVideoCodec.cpp',
         'signaling/src/media-conduit/VideoConduit.cpp',
         'signaling/src/media-conduit/WebrtcMediaCodecVP8VideoCodec.cpp',
         'signaling/src/mediapipeline/MediaPipeline.cpp',
         'signaling/src/mediapipeline/MediaPipelineFilter.cpp',
         'signaling/src/mediapipeline/SrtpFlow.cpp',
-        'signaling/src/peerconnection/MediaPipelineFactory.cpp',
-        'signaling/src/peerconnection/MediaStreamList.cpp',
         'signaling/src/peerconnection/PeerConnectionCtx.cpp',
         'signaling/src/peerconnection/PeerConnectionImpl.cpp',
         'signaling/src/peerconnection/PeerConnectionMedia.cpp',
+        'signaling/src/peerconnection/TransceiverImpl.cpp',
         'signaling/src/peerconnection/WebrtcGlobalInformation.cpp',
         'signaling/src/sdp/sipcc/cpr_string.c',
         'signaling/src/sdp/sipcc/sdp_access.c',
         'signaling/src/sdp/sipcc/sdp_attr.c',
         'signaling/src/sdp/sipcc/sdp_attr_access.c',
         'signaling/src/sdp/sipcc/sdp_base64.c',
         'signaling/src/sdp/sipcc/sdp_config.c',
         'signaling/src/sdp/sipcc/sdp_main.c',
--- a/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
+++ b/media/webrtc/signaling/gtest/jsep_session_unittest.cpp
@@ -103,52 +103,148 @@ protected:
     // Values here semi-borrowed from JSEP draft.
     tdata.mIceUfrag = session.GetName() + "-ufrag";
     tdata.mIcePwd = session.GetName() + "-1234567890";
     session.SetIceCredentials(tdata.mIceUfrag, tdata.mIcePwd);
     AddDtlsFingerprint("sha-1", session, tdata);
     AddDtlsFingerprint("sha-256", session, tdata);
   }
 
+  void
+  CheckTransceiverInvariants(
+      const std::vector<JsepTransceiver>& oldTransceivers,
+      const std::vector<JsepTransceiver>& newTransceivers)
+  {
+    ASSERT_LE(oldTransceivers.size(), newTransceivers.size());
+    std::set<size_t> levels;
+
+    for (const 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 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::string
   CreateOffer(const Maybe<JsepOfferOptions>& options = Nothing())
   {
+    std::vector<JsepTransceiver> transceiversBefore =
+      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) {
+      JsepTransceiver& oldTransceiver = transceiversBefore[i];
+      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)
+  AddTracks(JsepSessionImpl& side, AddTrackMagic magic = ADDTRACK_MAGIC)
   {
     // Add tracks.
     if (types.empty()) {
       types = BuildTypes(GetParam());
     }
-    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());
+    AddTracks(side, types, magic);
   }
 
   void
-  AddTracks(JsepSessionImpl& side, const std::string& mediatypes)
+  AddTracks(JsepSessionImpl& side,
+            const std::string& mediatypes,
+            AddTrackMagic magic = ADDTRACK_MAGIC)
   {
-    AddTracks(side, BuildTypes(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::MakeNullSendTrack(SdpMediaSection::kAudio, "fake");
+    }
+
+    JsepTransceiver& transceiver(side.GetTransceivers()[index]);
+    JsepTrack& track = transceiver.mSending;
+    EXPECT_FALSE(track.IsNull()) << "No track at index " << index;
+
+    JsepTrack original(track);
+    track.ClearSendTrack("fake");
+    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;
   }
 
   std::vector<SdpMediaSection::MediaType>
   BuildTypes(const std::string& mediatypes)
   {
     std::vector<SdpMediaSection::MediaType> result;
     size_t ptr = 0;
 
@@ -173,87 +269,169 @@ protected:
       ptr = comma + 1;
     }
 
     return result;
   }
 
   void
   AddTracks(JsepSessionImpl& side,
-            const std::vector<SdpMediaSection::MediaType>& mediatypes)
+            const std::vector<SdpMediaSection::MediaType>& mediatypes,
+            AddTrackMagic magic = ADDTRACK_MAGIC)
   {
     FakeUuidGenerator uuid_gen;
     std::string stream_id;
     std::string track_id;
 
     ASSERT_TRUE(uuid_gen.Generate(&stream_id));
 
-    AddTracksToStream(side, stream_id, mediatypes);
+    AddTracksToStream(side, stream_id, mediatypes, magic);
   }
 
   void
   AddTracksToStream(JsepSessionImpl& side,
                     const std::string stream_id,
-                    const std::string& mediatypes)
+                    const std::string& mediatypes,
+                    AddTrackMagic magic = ADDTRACK_MAGIC)
   {
-    AddTracksToStream(side, stream_id, BuildTypes(mediatypes));
+    AddTracksToStream(side, stream_id, BuildTypes(mediatypes), magic);
   }
 
   void
   AddTracksToStream(JsepSessionImpl& side,
                     const std::string stream_id,
-                    const std::vector<SdpMediaSection::MediaType>& mediatypes)
+                    const std::vector<SdpMediaSection::MediaType>& mediatypes,
+                    AddTrackMagic magic = ADDTRACK_MAGIC)
 
   {
     FakeUuidGenerator uuid_gen;
     std::string track_id;
 
-    for (auto track = mediatypes.begin(); track != mediatypes.end(); ++track) {
+    for (auto type : mediatypes) {
       ASSERT_TRUE(uuid_gen.Generate(&track_id));
 
-      RefPtr<JsepTrack> mst(new JsepTrack(*track, stream_id, track_id));
-      side.AddTrack(mst);
+      std::vector<JsepTransceiver>& transceivers(side.GetTransceivers());
+      size_t i = transceivers.size();
+      if (magic == ADDTRACK_MAGIC) {
+        for (i = 0; i < transceivers.size(); ++i) {
+          if (transceivers[i].mSending.GetMediaType() != type) {
+            continue;
+          }
+
+          if (transceivers[i].mSending.IsNull() ||
+              type == SdpMediaSection::MediaType::kApplication) {
+            break;
+          }
+        }
+      }
+
+      if (i == transceivers.size()) {
+        side.AddTransceiver(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].mSending.UpdateSendTrack(
+          std::vector<std::string>(1, stream_id), track_id);
     }
   }
 
-  bool HasMediaStream(std::vector<RefPtr<JsepTrack>> tracks) const {
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
-      if ((*i)->GetMediaType() != SdpMediaSection::kApplication) {
+  bool HasMediaStream(const std::vector<JsepTrack>& tracks) const {
+    for (const auto& track : tracks) {
+      if (track.GetMediaType() != SdpMediaSection::kApplication) {
         return 1;
       }
     }
     return 0;
   }
 
   const std::string GetFirstLocalStreamId(JsepSessionImpl& side) const {
-    auto tracks = side.GetLocalTracks();
-    return (*tracks.begin())->GetStreamId();
+    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 (!transceiver.mSending.IsNull()) {
+        result.push_back(transceiver.mSending);
+      }
+    }
+    return result;
+  }
+
+  std::vector<JsepTrack>
+  GetRemoteTracks(const JsepSession& session) const {
+    std::vector<JsepTrack> result;
+    for (const auto& transceiver : session.GetTransceivers()) {
+      if (!transceiver.mReceiving.IsNull()) {
+        result.push_back(transceiver.mReceiving);
+      }
+    }
+    return result;
+  }
+
+  JsepTransceiver*
+  GetDatachannelTransceiver(JsepSession& side) {
+    for (auto& transceiver : side.GetTransceivers()) {
+      if (transceiver.mSending.GetMediaType() ==
+            SdpMediaSection::MediaType::kApplication) {
+        return &transceiver;
+      }
+    }
+
+    return nullptr;
+  }
+
+  JsepTransceiver*
+  GetNegotiatedTransceiver(JsepSession& side, size_t index) {
+    for (size_t i = 0; i < side.GetTransceivers().size(); ++i) {
+      if (side.GetTransceivers()[i].mSending.GetNegotiatedDetails() ||
+          side.GetTransceivers()[i].mReceiving.GetNegotiatedDetails()) {
+        if (index) {
+          --index;
+          continue;
+        }
+
+        return &side.GetTransceivers()[i];
+      }
+    }
+
+    return nullptr;
   }
 
   std::vector<std::string>
-  GetMediaStreamIds(std::vector<RefPtr<JsepTrack>> tracks) const {
+  GetMediaStreamIds(const std::vector<JsepTrack>& tracks) const {
     std::vector<std::string> ids;
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
+    for (const auto& track : tracks) {
       // data channels don't have msid's
-      if ((*i)->GetMediaType() == SdpMediaSection::kApplication) {
+      if (track.GetMediaType() == SdpMediaSection::kApplication) {
         continue;
       }
-      ids.push_back((*i)->GetStreamId());
+      ids.insert(ids.end(),
+                 track.GetStreamIds().begin(),
+                 track.GetStreamIds().end());
     }
     return ids;
   }
 
   std::vector<std::string>
   GetLocalMediaStreamIds(JsepSessionImpl& side) const {
-    return GetMediaStreamIds(side.GetLocalTracks());
+    return GetMediaStreamIds(GetLocalTracks(side));
   }
 
   std::vector<std::string>
   GetRemoteMediaStreamIds(JsepSessionImpl& side) const {
-    return GetMediaStreamIds(side.GetRemoteTracks());
+    return GetMediaStreamIds(GetRemoteTracks(side));
   }
 
   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;
@@ -264,59 +442,49 @@ protected:
     return sortUniqueStrVector(GetLocalMediaStreamIds(side));
   }
 
   std::vector<std::string>
   GetRemoteUniqueStreamIds(JsepSessionImpl& side) const {
     return sortUniqueStrVector(GetRemoteMediaStreamIds(side));
   }
 
-  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) {
+  JsepTrack GetTrack(JsepSessionImpl& side,
+                     SdpMediaSection::MediaType type,
+                     size_t index) const {
+    for (const auto& transceiver : side.GetTransceivers()) {
+      if (!transceiver.mSending.IsNull() &&
+          transceiver.mSending.GetMediaType() != type) {
         continue;
       }
 
       if (index != 0) {
         --index;
         continue;
       }
 
-      return *i;
+      return transceiver.mSending;
     }
 
-    return RefPtr<JsepTrack>(nullptr);
+    return JsepTrack::MakeNullSendTrack(type, "fake");
   }
 
-  RefPtr<JsepTrack> GetTrackOff(size_t index,
-                                SdpMediaSection::MediaType type) {
+  JsepTrack GetTrackOff(size_t index, SdpMediaSection::MediaType type) {
     return GetTrack(*mSessionOff, type, index);
   }
 
-  RefPtr<JsepTrack> GetTrackAns(size_t index,
-                                SdpMediaSection::MediaType type) {
+  JsepTrack GetTrackAns(size_t index, SdpMediaSection::MediaType type) {
     return GetTrack(*mSessionAns, type, index);
   }
 
-  class ComparePairsByLevel {
-    public:
-      bool operator()(const JsepTrackPair& lhs,
-                      const JsepTrackPair& rhs) const {
-        return lhs.mLevel < rhs.mLevel;
-      }
-  };
-
-  std::vector<JsepTrackPair> GetTrackPairsByLevel(JsepSessionImpl& side) const {
-    auto pairs = side.GetNegotiatedTrackPairs();
-    std::sort(pairs.begin(), pairs.end(), ComparePairsByLevel());
-    return pairs;
+  size_t CountRtpTypes() const {
+    return std::count_if(
+        types.begin(), types.end(),
+        [](SdpMediaSection::MediaType type)
+          {return type != SdpMediaSection::MediaType::kApplication;});
   }
 
   bool Equals(const SdpFingerprintAttributeList::Fingerprint& f1,
               const SdpFingerprintAttributeList::Fingerprint& f2) const {
     if (f1.hashFunc != f2.hashFunc) {
       return false;
     }
 
@@ -380,75 +548,130 @@ protected:
 
     if (t1->GetPassword() != t2->GetPassword()) {
       return false;
     }
 
     return true;
   }
 
-  bool Equals(const RefPtr<JsepTransport>& t1,
-              const RefPtr<JsepTransport>& t2) const {
-    if (!t1 && !t2) {
-      return true;
-    }
-
-    if (!t1 || !t2) {
+  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;
     }
 
-    if (t1->mTransportId != t2->mTransportId) {
+    if (t1.mComponents != t2.mComponents) {
+      std::cerr << "Component count differs" << std::endl;
       return false;
     }
 
-    if (t1->mComponents != t2->mComponents) {
-      return false;
-    }
-
-    if (!Equals(t1->mIce, t2->mIce)) {
+    if (!Equals(t1.mIce, t2.mIce)) {
+      std::cerr << "ICE differs" << std::endl;
       return false;
     }
 
     return true;
   }
 
-  bool Equals(const JsepTrackPair& p1,
-              const JsepTrackPair& p2) const {
-    if (p1.mLevel != p2.mLevel) {
+  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.IsNull() != t2.IsNull()) {
+      return false;
+    }
+
+    if (t1.GetStreamIds() != t2.GetStreamIds()) {
+      return false;
+    }
+
+    if (t1.GetTrackId() != t2.GetTrackId()) {
+      return false;
+    }
+
+    if (t1.GetActive() != t2.GetActive()) {
+      return false;
+    }
+
+    if (t1.GetCNAME() != t2.GetCNAME()) {
+      return false;
+    }
+
+    if (t1.GetSsrcs() != t2.GetSsrcs()) {
+      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;
       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.mSending.get() != p2.mSending.get()) {
+    if (p1.IsStopped() != p2.IsStopped()) {
+      std::cerr << "One transceiver is stopped, the other is not" << std::endl;
       return false;
     }
 
-    if (p1.mReceiving.get() != p2.mReceiving.get()) {
+    if (p1.IsAssociated() != p2.IsAssociated()) {
+      std::cerr << "One transceiver has a mid, the other doesn't"
+                << std::endl;
       return false;
     }
 
-    if (!Equals(p1.mRtpTransport, p2.mRtpTransport)) {
+    if (p1.IsAssociated() && (p1.GetMid() != p2.GetMid())) {
+      std::cerr << "mid differs: " << p1.GetMid() << " vs " << p2.GetMid()
+                << std::endl;
       return false;
     }
 
-    if (!Equals(p1.mRtcpTransport, p2.mRtcpTransport)) {
+    if (p1.mSending.IsNull() != p2.mSending.IsNull()) {
+      std::cerr << "Send track differs" << std::endl;
+      return false;
+    }
+
+    if (p1.mReceiving.IsNull() != p2.mReceiving.IsNull()) {
+      std::cerr << "Receive track differs" << std::endl;
+      return false;
+    }
+
+    if (!Equals(p1.mTransport, p2.mTransport)) {
+      std::cerr << "Transport differs" << std::endl;
       return false;
     }
 
     return true;
   }
 
   size_t GetTrackCount(JsepSessionImpl& side,
                        SdpMediaSection::MediaType type) const {
-    auto tracks = side.GetLocalTracks();
     size_t result = 0;
-    for (auto i = tracks.begin(); i != tracks.end(); ++i) {
-      if ((*i)->GetMediaType() == type) {
+    for (const auto& track : GetLocalTracks(side)) {
+      if (track.GetMediaType() == type) {
         ++result;
       }
     }
     return result;
   }
 
   UniquePtr<Sdp> GetParsedLocalDescription(const JsepSessionImpl& side) const {
     return Parse(side.GetLocalDescription());
@@ -497,44 +720,45 @@ protected:
       }
     }
   }
 
   void
   EnsureNegotiationFailure(SdpMediaSection::MediaType type,
                            const std::string& codecName)
   {
-    for (auto i = mSessionOff->Codecs().begin(); i != mSessionOff->Codecs().end();
-         ++i) {
-      auto* codec = *i;
+    for (auto* codec : mSessionOff->Codecs()) {
       if (codec->mType == type && codec->mName != codecName) {
         codec->mEnabled = false;
       }
     }
 
-    for (auto i = mSessionAns->Codecs().begin(); i != mSessionAns->Codecs().end();
-         ++i) {
-      auto* codec = *i;
+    for (auto* codec : mSessionAns->Codecs()) {
       if (codec->mType == type && codec->mName == codecName) {
         codec->mEnabled = false;
       }
     }
   }
 
   std::string
   CreateAnswer()
   {
+    std::vector<JsepTransceiver> transceiversBefore =
+      mSessionAns->GetTransceivers();
+
     JsepAnswerOptions options;
     std::string answer;
     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;
@@ -548,154 +772,186 @@ protected:
     std::string answer = CreateAnswer();
     SetLocalAnswer(answer, checkFlags);
     SetRemoteAnswer(answer, checkFlags);
   }
 
   void
   SetLocalOffer(const std::string& offer, uint32_t checkFlags = ALL_CHECKS)
   {
+    std::vector<JsepTransceiver> transceiversBefore =
+      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) {
-      // 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) {
+      // 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.mSending);
+        size_t level = transceiver.GetLevel();
+        ASSERT_FALSE(track.IsNull());
+        ASSERT_EQ(types[level], track.GetMediaType());
+        if (track.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += tracks[i]->GetStreamId();
+          msidAttr += track.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += tracks[i]->GetTrackId();
+          msidAttr += track.GetTrackId();
           ASSERT_NE(std::string::npos, offer.find(msidAttr))
             << "Did not find " << msidAttr << " in offer";
         }
       }
       if (types.size() == 1 &&
-          tracks[0]->GetMediaType() == SdpMediaSection::kApplication) {
+          types[0] == 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<JsepTransceiver> transceiversBefore =
+      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) {
-      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) {
+      ASSERT_EQ(types.size(), mSessionAns->GetTransceivers().size());
+      for (const auto& transceiver : mSessionAns->GetTransceivers()) {
+        if (!transceiver.HasLevel()) {
+          continue;
+        }
+        const auto& track(transceiver.mReceiving);
+        size_t level = transceiver.GetLevel();
+        ASSERT_FALSE(track.IsNull());
+        ASSERT_EQ(types[level], track.GetMediaType());
+        if (track.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += tracks[i]->GetStreamId();
+          msidAttr += track.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += tracks[i]->GetTrackId();
+          msidAttr += track.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<JsepTransceiver> transceiversBefore =
+      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.
-      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());
+      ASSERT_EQ(types.size(), mSessionAns->GetTransceivers().size());
+      for (const auto& transceiver : mSessionAns->GetTransceivers()) {
+        if (!transceiver.HasLevel()) {
+          continue;
+        }
+        const auto& sendTrack(transceiver.mSending);
+        const auto& recvTrack(transceiver.mReceiving);
+        size_t level = transceiver.GetLevel();
+        ASSERT_FALSE(sendTrack.IsNull());
+        ASSERT_EQ(types[level], sendTrack.GetMediaType());
         // These might have been in the SDP, or might have been randomly
         // chosen by JsepSessionImpl
-        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
-        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
-
-        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
+        ASSERT_FALSE(recvTrack.IsNull());
+        ASSERT_EQ(types[level], recvTrack.GetMediaType());
+
+        if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += pairs[i].mSending->GetStreamId();
+          msidAttr += sendTrack.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += pairs[i].mSending->GetTrackId();
+          msidAttr += sendTrack.GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
-            << "Did not find " << msidAttr << " in offer";
+            << "Did not find " << msidAttr << " in answer";
         }
       }
       if (types.size() == 1 &&
-          pairs[0].mReceiving->GetMediaType() == SdpMediaSection::kApplication) {
+          types[0] == SdpMediaSection::kApplication) {
         ASSERT_EQ(std::string::npos, answer.find("a=ssrc"))
           << "Data channel should not contain SSRC";
       }
     }
-    std::cerr << "OFFER pairs:" << std::endl;
-    DumpTrackPairs(*mSessionOff);
+    std::cerr << "Answerer transceivers:" << std::endl;
+    DumpTransceivers(*mSessionAns);
   }
 
   void
   SetRemoteAnswer(const std::string& answer, uint32_t checkFlags = ALL_CHECKS)
   {
+    std::vector<JsepTransceiver> transceiversBefore =
+      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.
-      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());
+      ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
+      for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+        if (!transceiver.HasLevel()) {
+          continue;
+        }
+        const auto& sendTrack(transceiver.mSending);
+        const auto& recvTrack(transceiver.mReceiving);
+        size_t level = transceiver.GetLevel();
+        ASSERT_FALSE(sendTrack.IsNull());
+        ASSERT_EQ(types[level], sendTrack.GetMediaType());
         // These might have been in the SDP, or might have been randomly
         // chosen by JsepSessionImpl
-        ASSERT_NE("", pairs[i].mReceiving->GetStreamId());
-        ASSERT_NE("", pairs[i].mReceiving->GetTrackId());
-
-        if (pairs[i].mReceiving->GetMediaType() != SdpMediaSection::kApplication) {
+        ASSERT_FALSE(recvTrack.IsNull());
+        ASSERT_EQ(types[level], recvTrack.GetMediaType());
+
+        if (recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
           std::string msidAttr("a=msid:");
-          msidAttr += pairs[i].mReceiving->GetStreamId();
+          msidAttr += recvTrack.GetStreamIds()[0];
           msidAttr += " ";
-          msidAttr += pairs[i].mReceiving->GetTrackId();
+          msidAttr += recvTrack.GetTrackId();
           ASSERT_NE(std::string::npos, answer.find(msidAttr))
             << "Did not find " << msidAttr << " in answer";
         }
       }
     }
-    std::cerr << "ANSWER pairs:" << std::endl;
-    DumpTrackPairs(*mSessionAns);
+    std::cerr << "Offerer transceivers:" << std::endl;
+    DumpTransceivers(*mSessionOff);
   }
 
   typedef enum {
     RTP = 1,
     RTCP = 2
   } ComponentType;
 
   class CandidateSet {
@@ -934,23 +1190,23 @@ protected:
         << context << " (level " << msection.GetLevel() << ")";
     } else {
       ASSERT_FALSE(msection.GetAttributeList().HasAttribute(
             SdpAttribute::kEndOfCandidatesAttribute))
         << context << " (level " << msection.GetLevel() << ")";
     }
   }
 
-  void CheckPairs(const JsepSession& session, const std::string& context)
+  void CheckTransceivers(const JsepSession& session, const std::string& context)
   {
-    auto pairs = session.GetNegotiatedTrackPairs();
-
-    for (JsepTrackPair& pair : pairs) {
-      ASSERT_TRUE(pair.HasBundleLevel()) << context;
-      ASSERT_EQ(0U, pair.BundleLevel()) << context;
+    auto transceivers = session.GetTransceivers();
+
+    for (JsepTransceiver& transceiver : transceivers) {
+      ASSERT_TRUE(transceiver.HasBundleLevel()) << context;
+      ASSERT_EQ(0U, transceiver.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
@@ -1028,16 +1284,21 @@ 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));
+    ASSERT_FALSE(msection->GetAttributeList().HasAttribute(
+          SdpAttribute::kMidAttribute));
   }
 
   void
   ValidateSetupAttribute(const JsepSessionImpl& side,
                          const SdpSetupAttribute::Role expectedRole)
   {
     auto sdp = GetParsedLocalDescription(side);
     for (size_t i = 0; sdp && i < sdp->GetMediaSectionCount(); ++i) {
@@ -1048,17 +1309,22 @@ protected:
       }
     }
   }
 
   void
   DumpTrack(const JsepTrack& track)
   {
     const JsepTrackNegotiatedDetails* details = track.GetNegotiatedDetails();
-    std::cerr << "  type=" << track.GetMediaType() << std::endl;
+    std::cerr << "  type=" << track.GetMediaType() << " track-id="
+              << track.GetTrackId() << std::endl;
+    if (!details) {
+      std::cerr << "  not negotiated" << std::endl;
+      return;
+    }
     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) {
@@ -1067,28 +1333,36 @@ protected:
           std::cerr << " dtmf(" << (audioCodec->mDtmfEnabled?"yes":"no") << ")";
         }
         std::cerr << std::endl;
       }
     }
   }
 
   void
-  DumpTrackPairs(const JsepSessionImpl& session)
+  DumpTransceivers(const JsepSessionImpl& session)
   {
-    auto pairs = mSessionAns->GetNegotiatedTrackPairs();
-    for (auto i = pairs.begin(); i != pairs.end(); ++i) {
-      std::cerr << "Track pair " << i->mLevel << std::endl;
-      if (i->mSending) {
+    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;
+      }
+      if (transceiver.HasBundleLevel()) {
+        std::cerr << "(bundle level is " << transceiver.BundleLevel() << ")"
+                  << std::endl;
+      }
+      if (!transceiver.mSending.IsNull()) {
         std::cerr << "Sending-->" << std::endl;
-        DumpTrack(*i->mSending);
+        DumpTrack(transceiver.mSending);
       }
-      if (i->mReceiving) {
+      if (!transceiver.mReceiving.IsNull()) {
         std::cerr << "Receiving-->" << std::endl;
-        DumpTrack(*i->mReceiving);
+        DumpTrack(transceiver.mReceiving);
       }
     }
   }
 
   UniquePtr<Sdp>
   Parse(const std::string& sdp) const
   {
     SipccSdpParser parser;
@@ -1228,34 +1502,36 @@ TEST_P(JsepSessionTest, RenegotiationNoC
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
-  ASSERT_EQ(types.size(), added.size());
+  ASSERT_EQ(CountRtpTypes(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
-  ASSERT_EQ(types.size(), added.size());
+  ASSERT_EQ(CountRtpTypes(), added.size());
   ASSERT_EQ(0U, removed.size());
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
   removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
@@ -1268,27 +1544,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 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]));
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[i]));
   }
 
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+  for (size_t i = 0; i < origAnswererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
 // Disabled: See Bug 1329028
 TEST_P(JsepSessionTest, DISABLED_RenegotiationSwappedRolesNoChange)
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
@@ -1308,18 +1585,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 offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  auto offererTransceivers = mSessionOff->GetTransceivers();
+  auto answererTransceivers = mSessionAns->GetTransceivers();
 
   SwapOfferAnswerRoles();
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
@@ -1334,108 +1611,113 @@ 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 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]));
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(offererTransceivers.size(), newAnswererTransceivers.size());
+  for (size_t i = 0; i < offererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(offererTransceivers[i], newAnswererTransceivers[i]));
   }
 
-  ASSERT_EQ(answererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newOffererPairs[i]));
+  ASSERT_EQ(answererTransceivers.size(), newOffererTransceivers.size());
+  for (size_t i = 0; i < answererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(answererTransceivers[i], newOffererTransceivers[i]));
   }
 }
 
 
 TEST_P(JsepSessionTest, RenegotiationOffererAddsTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   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 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]));
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size() + 2, newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[i]));
   }
 
-  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  ASSERT_EQ(origAnswererTransceivers.size() + 2,
+            newAnswererTransceivers.size());
+  for (size_t i = 0; i < origAnswererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererAddsTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   std::vector<SdpMediaSection::MediaType> extraTypes;
   extraTypes.push_back(SdpMediaSection::kAudio);
   extraTypes.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionAns, extraTypes);
   types.insert(types.end(), extraTypes.begin(), extraTypes.end());
 
   // We need to add a recvonly m-section to the offer for this to work
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio =
-    Some(GetTrackCount(*mSessionOff, SdpMediaSection::kAudio) + 1);
-  options.mOfferToReceiveVideo =
-    Some(GetTrackCount(*mSessionOff, SdpMediaSection::kVideo) + 1);
-
-  std::string offer = CreateOffer(Some(options));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::Direction::kRecvonly));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::Direction::kRecvonly));
+
+  std::string offer = CreateOffer();
   SetLocalOffer(offer, CHECK_SUCCESS);
   SetRemoteOffer(offer, CHECK_SUCCESS);
 
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer, CHECK_SUCCESS);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
@@ -1445,45 +1727,49 @@ 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 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(SdpMediaSection::kAudio, added[0].GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1].GetMediaType());
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size() + 2, newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[i]));
   }
 
-  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  ASSERT_EQ(origAnswererTransceivers.size() + 2,
+            newAnswererTransceivers.size());
+  for (size_t i = 0; i < origAnswererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationBothAddTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   OfferAnswer();
 
   ValidateSetupAttribute(*mSessionOff, SdpSetupAttribute::kActpass);
   ValidateSetupAttribute(*mSessionAns, SdpSetupAttribute::kActive);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   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());
 
@@ -1491,591 +1777,547 @@ 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 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(SdpMediaSection::kAudio, added[0].GetMediaType());
+  ASSERT_EQ(SdpMediaSection::kVideo, added[1].GetMediaType());
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size() + 2, newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[i]));
   }
 
-  ASSERT_EQ(answererPairs.size() + 2, newAnswererPairs.size());
-  for (size_t i = 0; i < answererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
+  ASSERT_EQ(origAnswererTransceivers.size() + 2,
+            newAnswererTransceivers.size());
+  for (size_t i = 0; i < origAnswererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationBothAddTracksToExistingStream)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   if (GetParam() == "datachannel") {
     return;
   }
 
   OfferAnswer();
 
-  auto oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
-  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
+  auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+  auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
   ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(*mSessionOff).size() > 0);
   ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(*mSessionAns).size() > 0);
   ASSERT_EQ(aHasStream, GetRemoteUniqueStreamIds(*mSessionOff).size()> 0);
   ASSERT_EQ(oHasStream, GetRemoteUniqueStreamIds(*mSessionAns).size() > 0);
 
   auto firstOffId = GetFirstLocalStreamId(*mSessionOff);
   auto firstAnsId = GetFirstLocalStreamId(*mSessionAns);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  auto offererTransceivers = mSessionOff->GetTransceivers();
+  auto answererTransceivers = mSessionAns->GetTransceivers();
 
   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(mSessionOff->GetLocalTracks());
-  aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
+  oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+  aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
 
   ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(*mSessionOff).size() > 0);
   ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(*mSessionAns).size() > 0);
   ASSERT_EQ(aHasStream, GetRemoteUniqueStreamIds(*mSessionOff).size() > 0);
   ASSERT_EQ(oHasStream, GetRemoteUniqueStreamIds(*mSessionAns).size() > 0);
   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(mSessionOff->GetLocalTracks());
-  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
-  ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(*mSessionOff).size() > 0);
-  ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(*mSessionAns).size() > 0);
+    auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+    auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
+    ASSERT_EQ(oHasStream, GetLocalUniqueStreamIds(*mSessionOff).size() > 0);
+    ASSERT_EQ(aHasStream, GetLocalUniqueStreamIds(*mSessionAns).size() > 0);
   }
 }
 
-TEST_P(JsepSessionTest, RenegotiationOffererRemovesTrack)
+// 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, 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->mSending.GetStreamIds()[0];
+  std::string trackId = transceiver->mSending.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(removedTracks[0].IsNull());
+  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
+
+  ASSERT_EQ(1U, addedTracks.size());
+  ASSERT_FALSE(addedTracks[0].IsNull());
+  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)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
+  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->mSending.GetStreamIds()[0];
+  std::string trackId = transceiver->mSending.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(removedTracks[0].IsNull());
+  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
+
+  ASSERT_EQ(1U, addedTracks.size());
+  ASSERT_FALSE(addedTracks[0].IsNull());
+  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<JsepTransceiver> origOffererTransceivers =
+    mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers =
+    mSessionAns->GetTransceivers();
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back().Stop();
+  JsepTrack removedTrack(mSessionOff->GetTransceivers().back().mSending);
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
+  ASSERT_EQ(removedTrack.GetMediaType(), removed[0].GetMediaType());
+  ASSERT_EQ(removedTrack.GetStreamIds(), removed[0].GetStreamIds());
+  ASSERT_EQ(removedTrack.GetTrackId(), removed[0].GetTrackId());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(0U, removed.size());
-
-  // First m-section should be recvonly
+  ASSERT_EQ(1U, removed.size());
+
+  // Last m-section should be disabled
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
+  const SdpMediaSection* msection =
+    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  // First audio m-section should be sendonly
+  ValidateDisabledMSection(msection);
+
+  // Last m-section should be disabled
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
+  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_FALSE(msection->IsReceiving());
-  ASSERT_TRUE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  // Will be the same size since we still have a track on one side.
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[0].mSending);
-  ASSERT_FALSE(newOffererPairs[0].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[0].mSending = nullptr;
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  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());
+
+  for (size_t i = 0; i < origOffererTransceivers.size() - 1; ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[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]));
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(origAnswererTransceivers.back().IsStopped());
+  ASSERT_TRUE(newAnswererTransceivers.back().IsStopped());
+
+  for (size_t i = 0; i < origAnswererTransceivers.size() - 1; ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
-TEST_P(JsepSessionTest, RenegotiationAnswererRemovesTrack)
+TEST_P(JsepSessionTest, RenegotiationAnswererStopsTransceiver)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+  if (types.back() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrack = GetTrackAns(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionAns->GetTransceivers().back().Stop();
+  JsepTrack removedTrack(mSessionAns->GetTransceivers().back().mSending);
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
-
-  // First m-section should be sendrecv
+  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
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
+  const SdpMediaSection* msection =
+    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
   ASSERT_TRUE(msection->IsReceiving());
   ASSERT_TRUE(msection->IsSending());
 
-  // First audio m-section should be recvonly
+  // Last m-section should be disabled
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
+  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  // Will be the same size since we still have a track on one side.
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[0].mReceiving);
-  ASSERT_FALSE(newOffererPairs[0].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[0].mReceiving = nullptr;
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  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());
+
+  for (size_t i = 0; i < origOffererTransceivers.size() - 1; ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[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]));
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(origAnswererTransceivers.back().IsStopped());
+  ASSERT_TRUE(newAnswererTransceivers.back().IsStopped());
+
+  for (size_t i = 0; i < origAnswererTransceivers.size() - 1; ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothRemoveTrack)
+TEST_P(JsepSessionTest, RenegotiationBothStopSameTransceiver)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+  if (types.back() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
-  ASSERT_TRUE(removedTrackAnswer);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrackAnswer->GetStreamId(),
-                                           removedTrackAnswer->GetTrackId()));
-
-  RefPtr<JsepTrack> removedTrackOffer = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrackOffer);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrackOffer->GetStreamId(),
-                                           removedTrackOffer->GetTrackId()));
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back().Stop();
+  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back().mSending);
+  mSessionAns->GetTransceivers().back().Stop();
+  JsepTrack removedTrackAnswer(mSessionAns->GetTransceivers().back().mSending);
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());
+  ASSERT_EQ(removedTrackOffer.GetMediaType(), removed[0].GetMediaType());
+  ASSERT_EQ(removedTrackOffer.GetStreamIds(), removed[0].GetStreamIds());
+  ASSERT_EQ(removedTrackOffer.GetTrackId(), removed[0].GetTrackId());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(1U, removed.size());
 
-  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());
-
-  // First m-section should be recvonly
+  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
   auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
+  const SdpMediaSection* msection =
+    &offer->GetMediaSection(offer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  // First m-section should be inactive, and rejected
+  ValidateDisabledMSection(msection);
+
+  // Last m-section should be disabled
   auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
+  msection = &answer->GetMediaSection(answer->GetMediaSectionCount() - 1);
   ASSERT_TRUE(msection);
-  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());
+  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());
+
+  for (size_t i = 0; i < origOffererTransceivers.size() - 1; ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[i]));
   }
 
-  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());
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(origAnswererTransceivers.back().IsStopped());
+  ASSERT_TRUE(newAnswererTransceivers.back().IsStopped());
+
+  for (size_t i = 0; i < origAnswererTransceivers.size() - 1; ++i) {
+    ASSERT_TRUE(Equals(origAnswererTransceivers[i],
+                       newAnswererTransceivers[i]));
   }
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothRemoveThenAddTrack)
+TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverThenAddTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+  if (types.back() == SdpMediaSection::kApplication) {
     return;
   }
 
-  SdpMediaSection::MediaType removedType = types.front();
+  SdpMediaSection::MediaType removedType = types.back();
 
   OfferAnswer();
 
-  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()));
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back().Stop();
+  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back().mSending);
+  mSessionOff->GetTransceivers().back().Stop();
+  JsepTrack removedTrackAnswer(mSessionOff->GetTransceivers().back().mSending);
 
   OfferAnswer(CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   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 newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size() + 1, newOffererPairs.size());
-  ASSERT_EQ(answererPairs.size() + 1, newAnswererPairs.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());
 
   // Ensure that the m-section was re-used; no gaps
-  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);
-  }
+  ASSERT_EQ(origOffererTransceivers.back().GetLevel(),
+            newOffererTransceivers.back().GetLevel());
+
+  ASSERT_EQ(origAnswererTransceivers.back().GetLevel(),
+            newAnswererTransceivers.back().GetLevel());
 }
 
-TEST_P(JsepSessionTest, RenegotiationBothRemoveTrackDifferentMsection)
+TEST_P(JsepSessionTest, RenegotiationBothStopTransceiverDifferentMsection)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
-  if (types.front() == SdpMediaSection::kApplication) {
+
+  if (types.size() < 2) {
     return;
   }
 
-  if (types.size() < 2 || types[0] != types[1]) {
-    // For simplicity, just run in cases where we have two of the same type
+  if (types[0] == SdpMediaSection::kApplication ||
+      types[1] == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrackAnswer = GetTrackAns(0, types.front());
-  ASSERT_TRUE(removedTrackAnswer);
-  ASSERT_EQ(NS_OK, mSessionAns->RemoveTrack(removedTrackAnswer->GetStreamId(),
-                                           removedTrackAnswer->GetTrackId()));
-
-  // 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()));
+  mSessionOff->GetTransceivers()[0].Stop();
+  mSessionOff->GetTransceivers()[1].Stop();
 
   OfferAnswer(CHECK_SUCCESS);
 
   auto added = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(1U, removed.size());
-
-  ASSERT_EQ(removedTrackOffer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackOffer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackOffer->GetTrackId(), removed[0]->GetTrackId());
+  ASSERT_EQ(2U, removed.size());
 
   added = mSessionOff->GetRemoteTracksAdded();
   removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
-  ASSERT_EQ(1U, removed.size());
-
-  ASSERT_EQ(removedTrackAnswer->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrackAnswer->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrackAnswer->GetTrackId(), removed[0]->GetTrackId());
-
-  // Second m-section should be recvonly
-  auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 1);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  // First m-section should be recvonly
-  auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_FALSE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[0].mReceiving);
-  ASSERT_FALSE(newOffererPairs[0].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[0].mReceiving = nullptr;
-
-  // This should be the only difference.
-  ASSERT_TRUE(offererPairs[1].mSending);
-  ASSERT_FALSE(newOffererPairs[1].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  offererPairs[1].mSending = nullptr;
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
-  }
-
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  // This should be the only difference.
-  ASSERT_TRUE(answererPairs[0].mSending);
-  ASSERT_FALSE(newAnswererPairs[0].mSending);
-
-  // Remove this difference, let loop below take care of the rest
-  answererPairs[0].mSending = nullptr;
-
-  // This should be the only difference.
-  ASSERT_TRUE(answererPairs[1].mReceiving);
-  ASSERT_FALSE(newAnswererPairs[1].mReceiving);
-
-  // Remove this difference, let loop below take care of the rest
-  answererPairs[1].mReceiving = nullptr;
-
-  for (size_t i = 0; i < newAnswererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(answererPairs[i], newAnswererPairs[i]));
-  }
+  ASSERT_EQ(2U, removed.size());
 }
 
 TEST_P(JsepSessionTest, RenegotiationOffererReplacesTrack)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   if (types.front() == SdpMediaSection::kApplication) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
-  RefPtr<JsepTrack> addedTrack(
-      new JsepTrack(types.front(), "newstream", "newtrack"));
-  ASSERT_EQ(NS_OK, mSessionOff->AddTrack(addedTrack));
+  mSessionOff->GetTransceivers()[0].mSending.UpdateSendTrack(
+      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 = mSessionAns->GetRemoteTracksAdded();
   auto removed = mSessionAns->GetRemoteTracksRemoved();
-  ASSERT_EQ(1U, added.size());
-  ASSERT_EQ(1U, removed.size());
-
-  ASSERT_EQ(removedTrack->GetMediaType(), removed[0]->GetMediaType());
-  ASSERT_EQ(removedTrack->GetStreamId(), removed[0]->GetStreamId());
-  ASSERT_EQ(removedTrack->GetTrackId(), removed[0]->GetTrackId());
-
-  ASSERT_EQ(addedTrack->GetMediaType(), added[0]->GetMediaType());
-  ASSERT_EQ(addedTrack->GetStreamId(), added[0]->GetStreamId());
-  ASSERT_EQ(addedTrack->GetTrackId(), added[0]->GetTrackId());
-
-  added = mSessionOff->GetRemoteTracksAdded();
-  removed = mSessionOff->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
   ASSERT_EQ(0U, removed.size());
-
-  // First audio m-section should be sendrecv
-  auto offer = GetParsedLocalDescription(*mSessionOff);
-  auto* msection = GetMsection(*offer, types.front(), 0);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_TRUE(msection->IsSending());
-
-  // First audio m-section should be sendrecv
-  auto answer = GetParsedLocalDescription(*mSessionAns);
-  msection = GetMsection(*answer, types.front(), 0);
-  ASSERT_TRUE(msection);
-  ASSERT_TRUE(msection->IsReceiving());
-  ASSERT_TRUE(msection->IsSending());
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-
-  ASSERT_NE(offererPairs[0].mSending->GetStreamId(),
-            newOffererPairs[0].mSending->GetStreamId());
-  ASSERT_NE(offererPairs[0].mSending->GetTrackId(),
-            newOffererPairs[0].mSending->GetTrackId());
-
-  // Skip first pair
-  for (size_t i = 1; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+}
+
+TEST_P(JsepSessionTest, RenegotiationAnswererReplacesTrack)
+{
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  if (types.front() == SdpMediaSection::kApplication) {
+    return;
   }
 
-  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]));
-  }
+  OfferAnswer();
+
+  mSessionAns->GetTransceivers()[0].mSending.UpdateSendTrack(
+      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());
 }
 
 // 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();
@@ -2084,120 +2326,117 @@ TEST_P(JsepSessionTest, RenegotiationAut
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-
-  // Make sure that DisableMsid actually worked, since it is kinda hacky
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-  ASSERT_EQ(offererPairs.size(), answererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(offererPairs[i].mReceiving);
-    ASSERT_TRUE(answererPairs[i].mSending);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), origAnswererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_FALSE(origOffererTransceivers[i].mReceiving.IsNull());
+    ASSERT_FALSE(origAnswererTransceivers[i].mSending.IsNull());
     // These should not match since we've monkeyed with the msid
-    ASSERT_NE(offererPairs[i].mReceiving->GetStreamId(),
-              answererPairs[i].mSending->GetStreamId());
-    ASSERT_NE(offererPairs[i].mReceiving->GetTrackId(),
-              answererPairs[i].mSending->GetTrackId());
+    ASSERT_NE(origOffererTransceivers[i].mReceiving.GetStreamIds(),
+              origAnswererTransceivers[i].mSending.GetStreamIds());
+    ASSERT_NE(origOffererTransceivers[i].mReceiving.GetTrackId(),
+              origAnswererTransceivers[i].mSending.GetTrackId());
   }
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererPairs = mSessionOff->GetNegotiatedTrackPairs();
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_TRUE(Equals(offererPairs[i], newOffererPairs[i]));
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(Equals(origOffererTransceivers[i], newOffererTransceivers[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
-  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::vector<JsepTrack> tracks;
+  for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+    tracks.push_back(transceiver.mSending);
+    tracks.push_back(transceiver.mReceiving);
+  }
+
+  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);
     }
   }
 
   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
-  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);
+  tracks.clear();
+  for (const auto& transceiver : mSessionOff->GetTransceivers()) {
+    tracks.push_back(transceiver.mSending);
+    tracks.push_back(transceiver.mReceiving);
+  }
+
+  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(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);
@@ -2207,93 +2446,93 @@ TEST_P(JsepSessionTest, RenegotiationAns
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererPairs = mSessionOff->GetNegotiatedTrackPairs();
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
-              newOffererPairs[i].mReceiving->GetMediaType());
-
-    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
-    ASSERT_TRUE(Equals(offererPairs[i].mRtpTransport,
-                       newOffererPairs[i].mRtpTransport));
-    ASSERT_TRUE(Equals(offererPairs[i].mRtcpTransport,
-                       newOffererPairs[i].mRtcpTransport));
-
-    if (offererPairs[i].mReceiving->GetMediaType() ==
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_EQ(origOffererTransceivers[i].mReceiving.GetMediaType(),
+              newOffererTransceivers[i].mReceiving.GetMediaType());
+
+    ASSERT_TRUE(Equals(origOffererTransceivers[i].mSending,
+                       newOffererTransceivers[i].mSending));
+    ASSERT_TRUE(Equals(origOffererTransceivers[i].mTransport,
+                       newOffererTransceivers[i].mTransport));
+
+    if (origOffererTransceivers[i].mReceiving.GetMediaType() ==
         SdpMediaSection::kApplication) {
-      ASSERT_EQ(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+      ASSERT_TRUE(Equals(origOffererTransceivers[i].mReceiving,
+                         newOffererTransceivers[i].mReceiving));
     } else {
       // This should be the only difference
-      ASSERT_NE(offererPairs[i].mReceiving, newOffererPairs[i].mReceiving);
+      ASSERT_FALSE(Equals(origOffererTransceivers[i].mReceiving,
+                          newOffererTransceivers[i].mReceiving));
     }
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererDisablesMsid)
 {
   AddTracks(*mSessionOff);
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   AddTracks(*mSessionAns);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
-  AddTracks(*mSessionAns);
   answer = CreateAnswer();
   SetLocalAnswer(answer);
 
   DisableMsid(&answer);
 
   SetRemoteAnswer(answer, CHECK_SUCCESS);
 
-  auto newOffererPairs = mSessionOff->GetNegotiatedTrackPairs();
-
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  for (size_t i = 0; i < offererPairs.size(); ++i) {
-    ASSERT_EQ(offererPairs[i].mReceiving->GetMediaType(),
-              newOffererPairs[i].mReceiving->GetMediaType());
-
-    ASSERT_EQ(offererPairs[i].mSending, newOffererPairs[i].mSending);
-    ASSERT_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);
-    }
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_EQ(origOffererTransceivers[i].mReceiving.GetMediaType(),
+              newOffererTransceivers[i].mReceiving.GetMediaType());
+
+    ASSERT_TRUE(Equals(origOffererTransceivers[i].mSending,
+                       newOffererTransceivers[i].mSending));
+    ASSERT_TRUE(Equals(origOffererTransceivers[i].mTransport,
+                       newOffererTransceivers[i].mTransport));
+
+    // If the msid is missing, we just assume it is the same
+    ASSERT_TRUE(Equals(origOffererTransceivers[i].mReceiving,
+                       newOffererTransceivers[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);
@@ -2309,178 +2548,169 @@ TEST_P(JsepSessionTest, RenegotiationOff
   DisableBundle(&offer);
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = mSessionAns->GetTransceivers();
 
   OfferAnswer();
 
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size());
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size());
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
+  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) {
     // No bundle initially
-    ASSERT_FALSE(offererPairs[i].HasBundleLevel());
-    ASSERT_FALSE(answererPairs[i].HasBundleLevel());
+    ASSERT_FALSE(origOffererTransceivers[i].HasBundleLevel());
+    ASSERT_FALSE(origAnswererTransceivers[i].HasBundleLevel());
     if (i != 0) {
-      ASSERT_NE(offererPairs[0].mRtpTransport.get(),
-                offererPairs[i].mRtpTransport.get());
-      if (offererPairs[0].mRtcpTransport) {
-        ASSERT_NE(offererPairs[0].mRtcpTransport.get(),
-                  offererPairs[i].mRtcpTransport.get());
-      }
-      ASSERT_NE(answererPairs[0].mRtpTransport.get(),
-                answererPairs[i].mRtpTransport.get());
-      if (answererPairs[0].mRtcpTransport) {
-        ASSERT_NE(answererPairs[0].mRtcpTransport.get(),
-                  answererPairs[i].mRtcpTransport.get());
-      }
+      ASSERT_NE(origOffererTransceivers[0].mTransport.get(),
+                origOffererTransceivers[i].mTransport.get());
+      ASSERT_NE(origAnswererTransceivers[0].mTransport.get(),
+                origAnswererTransceivers[i].mTransport.get());
     }
 
     // Verify that bundle worked after renegotiation
-    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());
+    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());
   }
 }
 
 TEST_P(JsepSessionTest, RenegotiationOffererDisablesBundleTransport)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
 
   if (types.size() < 2) {
     return;
   }
 
   OfferAnswer();
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  std::string reoffer = CreateOffer();
-
-  DisableMsection(&reoffer, 0);
-
-  SetLocalOffer(reoffer, CHECK_SUCCESS);
-  SetRemoteOffer(reoffer, CHECK_SUCCESS);
-  std::string reanswer = CreateAnswer();
-  SetLocalAnswer(reanswer, CHECK_SUCCESS);
-  SetRemoteAnswer(reanswer, CHECK_SUCCESS);
-
-  auto newOffererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto newAnswererPairs = GetTrackPairsByLevel(*mSessionAns);
-
-  ASSERT_EQ(newOffererPairs.size(), newAnswererPairs.size());
-  ASSERT_EQ(offererPairs.size(), newOffererPairs.size() + 1);
-  ASSERT_EQ(answererPairs.size(), newAnswererPairs.size() + 1);
-
-  for (size_t i = 0; i < newOffererPairs.size(); ++i) {
-    ASSERT_TRUE(newOffererPairs[i].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());
+  mSessionOff->GetTransceivers()[0].Stop();
+
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = 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) {
+    if (newOffererTransceivers.size() > 2) {
+      ASSERT_TRUE(newOffererTransceivers[i].HasBundleLevel());
+      ASSERT_TRUE(newAnswererTransceivers[i].HasBundleLevel());
+      ASSERT_EQ(1U, newOffererTransceivers[i].BundleLevel());
+      ASSERT_EQ(1U, newAnswererTransceivers[i].BundleLevel());
+    } else {
+      // Only one remaining m-section, no bundle will happen here.
+      ASSERT_FALSE(newOffererTransceivers[i].HasBundleLevel());
+      ASSERT_FALSE(newAnswererTransceivers[i].HasBundleLevel());
+    }
+    ASSERT_NE(newOffererTransceivers[0].mTransport.get(),
+              newOffererTransceivers[i].mTransport.get());
+    ASSERT_NE(newAnswererTransceivers[0].mTransport.get(),
+              newAnswererTransceivers[i].mTransport.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();
 
-  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());
+  std::vector<JsepTransceiver> origOffererTransceivers
+    = mSessionOff->GetTransceivers();
+  std::vector<JsepTransceiver> origAnswererTransceivers
+    = 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) {
+    if (newOffererTransceivers.size() > 2) {
+      ASSERT_TRUE(newOffererTransceivers[i].HasBundleLevel());
+      ASSERT_TRUE(newAnswererTransceivers[i].HasBundleLevel());
+      ASSERT_EQ(1U, newOffererTransceivers[i].BundleLevel());
+      ASSERT_EQ(1U, newAnswererTransceivers[i].BundleLevel());
+    } else {
+      // Only one remaining m-section, no bundle will happen here.
+      ASSERT_FALSE(newOffererTransceivers[i].HasBundleLevel());
+      ASSERT_FALSE(newAnswererTransceivers[i].HasBundleLevel());
+    }
+    ASSERT_NE(newOffererTransceivers[0].mTransport.get(),
+              newOffererTransceivers[i].mTransport.get());
+    ASSERT_NE(newAnswererTransceivers[0].mTransport.get(),
+              newAnswererTransceivers[i].mTransport.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)
 {
-  if (GetParam() == "datachannel") {
+  AddTracks(*mSessionOff);
+  if (types.front() == SdpMediaSection::MediaType::kApplication) {
     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);
 }
@@ -2791,23 +3021,23 @@ TEST_P(JsepSessionTest, RenegotiationAns
       msection.SetReceiving(false);
     }
   }
 
   answer = parsedAnswer->ToString();
 
   SetRemoteAnswer(answer);
 
-  for (const RefPtr<JsepTrack>& track : mSessionOff->GetLocalTracks()) {
-    if (track->GetMediaType() != SdpMediaSection::kApplication) {
-      ASSERT_FALSE(track->GetActive());
+  for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) {
+    if (track.GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track.GetActive());
     }
   }
 
-  ASSERT_EQ(types.size(), mSessionOff->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
 }
 
 TEST_P(JsepSessionTest, RenegotiationAnswererInactive)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
   OfferAnswer();
 
@@ -2825,23 +3055,23 @@ TEST_P(JsepSessionTest, RenegotiationAns
       msection.SetSending(false);
     }
   }
 
   answer = parsedAnswer->ToString();
 
   SetRemoteAnswer(answer, CHECK_SUCCESS); // Won't have answerer tracks
 
-  for (const RefPtr<JsepTrack>& track : mSessionOff->GetLocalTracks()) {
-    if (track->GetMediaType() != SdpMediaSection::kApplication) {
-      ASSERT_FALSE(track->GetActive());
+  for (const JsepTrack& track : GetLocalTracks(*mSessionOff)) {
+    if (track.GetMediaType() != SdpMediaSection::kApplication) {
+      ASSERT_FALSE(track.GetActive());
     }
   }
 
-  ASSERT_EQ(types.size(), mSessionOff->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(types.size(), mSessionOff->GetTransceivers().size());
 }
 
 
 INSTANTIATE_TEST_CASE_P(
     Variants,
     JsepSessionTest,
     ::testing::Values("audio",
                       "video",
@@ -2864,21 +3094,23 @@ INSTANTIATE_TEST_CASE_P(
                       "audio,video,video",
                       "audio,audio,video,video",
                       "audio,audio,video,video,datachannel"));
 
 // offerToReceiveXxx variants
 
 TEST_F(JsepSessionTest, OfferAnswerRecvOnlyLines)
 {
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some(static_cast<size_t>(1U));
-  options.mOfferToReceiveVideo = Some(static_cast<size_t>(2U));
-  options.mDontOfferDataChannel = Some(true);
-  std::string offer = CreateOffer(Some(options));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+  std::string offer = CreateOffer();
 
   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,
@@ -2927,35 +3159,32 @@ 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<JsepTrackPair> trackPairs(mSessionOff->GetNegotiatedTrackPairs());
-  ASSERT_EQ(2U, trackPairs.size());
-  for (auto pair : trackPairs) {
-    auto ssrcs = parsedOffer->GetMediaSection(pair.mLevel).GetAttributeList()
-                 .GetSsrc().mSsrcs;
+  std::vector<JsepTransceiver> transceivers(mSessionOff->GetTransceivers());
+  ASSERT_EQ(3U, transceivers.size());
+  for (auto transceiver : transceivers) {
+    auto ssrcs = parsedOffer->GetMediaSection(transceiver.GetLevel())
+                 .GetAttributeList().GetSsrc().mSsrcs;
     ASSERT_EQ(1U, ssrcs.size());
-    ASSERT_EQ(pair.mRecvonlySsrc, ssrcs.front().ssrc);
   }
 }
 
 TEST_F(JsepSessionTest, OfferAnswerSendOnlyLines)
 {
   AddTracks(*mSessionOff, "audio,video,video");
 
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some(static_cast<size_t>(0U));
-  options.mOfferToReceiveVideo = Some(static_cast<size_t>(1U));
-  options.mDontOfferDataChannel = Some(true);
-  std::string offer = CreateOffer(Some(options));
+  SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly);
+  SetDirection(*mSessionOff, 2, SdpDirectionAttribute::kSendonly);
+  std::string offer = CreateOffer();
 
   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,
@@ -2996,20 +3225,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)
 {
-  JsepOfferOptions options;
-  options.mOfferToReceiveAudio = Some<size_t>(1);
-
-  OfferAnswer(CHECK_SUCCESS, Some(options));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
+
+  OfferAnswer(CHECK_SUCCESS);
 
   UniquePtr<Sdp> offer(Parse(mSessionOff->GetLocalDescription()));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
             offer->GetMediaSection(0).GetAttributeList().GetDirection());
@@ -3020,20 +3249,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)
 {
-  JsepOfferOptions options;
-  options.mOfferToReceiveVideo = Some<size_t>(1);
-
-  OfferAnswer(CHECK_SUCCESS, Some(options));
+  mSessionOff->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+
+  OfferAnswer(CHECK_SUCCESS);
 
   UniquePtr<Sdp> offer(Parse(mSessionOff->GetLocalDescription()));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kVideo,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
             offer->GetMediaSection(0).GetAttributeList().GetDirection());
@@ -3044,23 +3273,25 @@ 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<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);
+  JsepTransceiver audio(SdpMediaSection::kAudio);
+  audio.mSending.UpdateSendTrack(
+      std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  JsepTransceiver video(SdpMediaSection::kVideo);
+  video.mSending.UpdateSendTrack(
+      std::vector<std::string>(1, "offerer_stream"), "v1");
+  mSessionOff->AddTransceiver(video);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
@@ -3069,22 +3300,23 @@ TEST_F(JsepSessionTest, CreateOfferNoDat
             outputSdp->GetMediaSection(1).GetMediaType());
 }
 
 TEST_F(JsepSessionTest, ValidateOfferedVideoCodecParams)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
 
-  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);
+  JsepTransceiver audio(SdpMediaSection::kAudio);
+  audio.mSending.UpdateSendTrack(std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  JsepTransceiver video(SdpMediaSection::kVideo);
+  video.mSending.UpdateSendTrack(std::vector<std::string>(1, "offerer_stream"), "v1");
+  mSessionOff->AddTransceiver(video);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
@@ -3196,22 +3428,23 @@ 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<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);
+  JsepTransceiver audio(SdpMediaSection::kAudio);
+  audio.mSending.UpdateSendTrack(std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  JsepTransceiver video(SdpMediaSection::kVideo);
+  video.mSending.UpdateSendTrack(std::vector<std::string>(1, "offerer_stream"), "v1");
+  mSessionOff->AddTransceiver(video);
 
   std::string offer = CreateOffer();
 
   UniquePtr<Sdp> outputSdp(Parse(offer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& audio_section = outputSdp->GetMediaSection(0);
@@ -3278,39 +3511,29 @@ 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);
 
-  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);
+  AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video");
 
   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);
 
-  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);
+  AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video");
 
   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);
@@ -3347,40 +3570,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 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());
+  auto offerTransceivers = mSessionOff->GetTransceivers();
+  ASSERT_EQ(2U, offerTransceivers.size());
+  ASSERT_FALSE(offerTransceivers[1].mSending.IsNull());
+  ASSERT_FALSE(offerTransceivers[1].mReceiving.IsNull());
+  ASSERT_TRUE(offerTransceivers[1].mSending.GetNegotiatedDetails());
+  ASSERT_TRUE(offerTransceivers[1].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(6U,
-      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1].mSending.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(6U,
-      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1].mReceiving.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
-  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());
+  auto answerTransceivers = mSessionAns->GetTransceivers();
+  ASSERT_EQ(2U, answerTransceivers.size());
+  ASSERT_FALSE(answerTransceivers[1].mSending.IsNull());
+  ASSERT_FALSE(answerTransceivers[1].mReceiving.IsNull());
+  ASSERT_TRUE(answerTransceivers[1].mSending.GetNegotiatedDetails());
+  ASSERT_TRUE(answerTransceivers[1].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(6U,
-      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[1].mSending.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(6U,
-      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[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
@@ -3400,33 +3623,23 @@ TEST_F(JsepSessionTest, ValidateAnswered
         h264->mDefaultPt = "126";
       }
     }
   }
 
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
 
-  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);
+  AddTracksToStream(*mSessionOff, "offerer_stream", "audio,video");
 
   std::string offer = CreateOffer();
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
 
-  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);
+  AddTracksToStream(*mSessionAns, "answerer_stream", "audio,video");
 
   std::string answer = CreateAnswer();
 
   UniquePtr<Sdp> outputSdp(Parse(answer));
   ASSERT_TRUE(!!outputSdp);
 
   ASSERT_EQ(2U, outputSdp->GetMediaSectionCount());
   auto& video_section = outputSdp->GetMediaSection(1);
@@ -3478,40 +3691,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 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());
+  auto offerTransceivers = mSessionOff->GetTransceivers();
+  ASSERT_EQ(2U, offerTransceivers.size());
+  ASSERT_FALSE(offerTransceivers[1].mSending.IsNull());
+  ASSERT_FALSE(offerTransceivers[1].mReceiving.IsNull());
+  ASSERT_TRUE(offerTransceivers[1].mSending.GetNegotiatedDetails());
+  ASSERT_TRUE(offerTransceivers[1].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(1U,
-      offerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1].mSending.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(1U,
-      offerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      offerTransceivers[1].mReceiving.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
 
-  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());
+  auto answerTransceivers = mSessionAns->GetTransceivers();
+  ASSERT_EQ(2U, answerTransceivers.size());
+  ASSERT_FALSE(answerTransceivers[1].mSending.IsNull());
+  ASSERT_FALSE(answerTransceivers[1].mReceiving.IsNull());
+  ASSERT_TRUE(answerTransceivers[1].mSending.GetNegotiatedDetails());
+  ASSERT_TRUE(answerTransceivers[1].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(1U,
-      answerPairs[1].mSending->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[1].mSending.GetNegotiatedDetails()->GetEncoding(0)
       .GetCodecs().size());
   ASSERT_EQ(1U,
-      answerPairs[1].mReceiving->GetNegotiatedDetails()->GetEncoding(0)
+      answerTransceivers[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);
 
@@ -3554,35 +3767,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 pairIndex,
+         size_t transceiverIndex,
          sdp::Direction direction,
          size_t encodingIndex,
          size_t codecIndex,
          const JsepCodecDescription** codecOut)
 {
   *codecOut = nullptr;
-  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(transceiverIndex, session.GetTransceivers().size());
+  JsepTransceiver& transceiver(session.GetTransceivers()[transceiverIndex]);
+  JsepTrack& track =
+      (direction == sdp::kSend) ? transceiver.mSending : transceiver.mReceiving;
+  ASSERT_FALSE(track.IsNull());
+  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") {
@@ -3655,18 +3868,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(0U, mSessionOff->GetNegotiatedTrackPairs().size());
-  ASSERT_EQ(0U, mSessionAns->GetNegotiatedTrackPairs().size());
+  ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionOff, 0));
+  ASSERT_EQ(nullptr, GetNegotiatedTransceiver(*mSessionAns, 0));
 }
 
 TEST_F(JsepSessionTest, TestH264NegotiationOffererDefault)
 {
   ForceH264(*mSessionOff, 0x42000d);
   ForceH264(*mSessionAns, 0x42000d);
 
   AddTracks(*mSessionOff, "video");
@@ -3888,17 +4101,16 @@ 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
@@ -3939,26 +4151,30 @@ 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;
 
-  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());
+  if (types.front() == SdpMediaSection::MediaType::kApplication) {
+    ASSERT_TRUE(GetDatachannelTransceiver(*mSessionOff));
+    ASSERT_FALSE(
+        GetDatachannelTransceiver(*mSessionOff)->mReceiving.GetActive());
+    ASSERT_TRUE(GetDatachannelTransceiver(*mSessionAns));
+    ASSERT_FALSE(
+        GetDatachannelTransceiver(*mSessionAns)->mReceiving.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());
+  }
 }
 
 TEST_F(JsepSessionTest, CreateOfferNoMlines)
 {
   JsepOfferOptions options;
   std::string offer;
   nsresult rv = mSessionOff->CreateOffer(options, &offer);
   ASSERT_NE(NS_OK, rv);
@@ -4058,20 +4274,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, mSessionAns->GetRemoteTracks().size());
-  RefPtr<JsepTrack> track = mSessionAns->GetRemoteTracks()[0];
-  ASSERT_TRUE(track->GetNegotiatedDetails());
-  auto* details = track->GetNegotiatedDetails();
+  ASSERT_EQ(1U, GetRemoteTracks(*mSessionAns).size());
+  JsepTrack track = GetRemoteTracks(*mSessionAns)[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]);
   }
 }
@@ -4085,55 +4301,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 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());
+  auto offerTransceivers = mSessionOff->GetTransceivers();
+  auto answerTransceivers = mSessionAns->GetTransceivers();
+  ASSERT_EQ(3U, offerTransceivers.size());
+  ASSERT_EQ(3U, answerTransceivers.size());
+
+  ASSERT_FALSE(offerTransceivers[0].mReceiving.IsNull());
+  ASSERT_TRUE(offerTransceivers[0].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      offerPairs[0].mReceiving->GetNegotiatedDetails()->
+      offerTransceivers[0].mReceiving.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(offerPairs[1].mReceiving);
-  ASSERT_TRUE(offerPairs[1].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(offerTransceivers[1].mReceiving.IsNull());
+  ASSERT_TRUE(offerTransceivers[1].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      offerPairs[1].mReceiving->GetNegotiatedDetails()->
+      offerTransceivers[1].mReceiving.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(offerPairs[2].mReceiving);
-  ASSERT_TRUE(offerPairs[2].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(offerTransceivers[2].mReceiving.IsNull());
+  ASSERT_TRUE(offerTransceivers[2].mReceiving.GetNegotiatedDetails());
   ASSERT_NE(0U,
-      offerPairs[2].mReceiving->GetNegotiatedDetails()->
+      offerTransceivers[2].mReceiving.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(answerPairs[0].mReceiving);
-  ASSERT_TRUE(answerPairs[0].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(answerTransceivers[0].mReceiving.IsNull());
+  ASSERT_TRUE(answerTransceivers[0].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      answerPairs[0].mReceiving->GetNegotiatedDetails()->
+      answerTransceivers[0].mReceiving.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(answerPairs[1].mReceiving);
-  ASSERT_TRUE(answerPairs[1].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(answerTransceivers[1].mReceiving.IsNull());
+  ASSERT_TRUE(answerTransceivers[1].mReceiving.GetNegotiatedDetails());
   ASSERT_EQ(0U,
-      answerPairs[1].mReceiving->GetNegotiatedDetails()->
+      answerTransceivers[1].mReceiving.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 
-  ASSERT_TRUE(answerPairs[2].mReceiving);
-  ASSERT_TRUE(answerPairs[2].mReceiving->GetNegotiatedDetails());
+  ASSERT_FALSE(answerTransceivers[2].mReceiving.IsNull());
+  ASSERT_TRUE(answerTransceivers[2].mReceiving.GetNegotiatedDetails());
   ASSERT_NE(0U,
-      answerPairs[2].mReceiving->GetNegotiatedDetails()->
+      answerTransceivers[2].mReceiving.GetNegotiatedDetails()->
       GetUniquePayloadTypes().size());
 }
 
 TEST_F(JsepSessionTest, UnknownFingerprintAlgorithm)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -4298,17 +4514,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(types.size(), mSessionAns->GetRemoteTracksRemoved().size());
+  ASSERT_EQ(CountRtpTypes(), mSessionAns->GetRemoteTracksRemoved().size());
 
   ASSERT_EQ(NS_OK,
             mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
   ASSERT_EQ(kJsepStateStable, mSessionOff->GetState());
 
   OfferAnswer();
 }
 
@@ -4350,20 +4566,22 @@ 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 (RefPtr<JsepTransport>& transport : transports) {
-    activeTransportCount += transport->mComponents;
+  for (const auto& transceiver : session.GetTransceivers()) {
+    if (!transceiver.HasBundleLevel() ||
+        (transceiver.BundleLevel() == transceiver.GetLevel())) {
+      activeTransportCount += transceiver.mTransport->mComponents;
+    }
   }
   return activeTransportCount;
 }
 
 TEST_P(JsepSessionTest, TestBalancedBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4389,18 +4607,18 @@ TEST_P(JsepSessionTest, TestBalancedBund
   }
 
   SetLocalOffer(offer);
   SetRemoteOffer(offer);
   std::string answer = CreateAnswer();
   SetLocalAnswer(answer);
   SetRemoteAnswer(answer);
 
-  CheckPairs(*mSessionOff, "Offerer pairs");
-  CheckPairs(*mSessionAns, "Answerer pairs");
+  CheckTransceivers(*mSessionOff, "Offerer transceivers");
+  CheckTransceivers(*mSessionAns, "Answerer transceivers");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_P(JsepSessionTest, TestMaxBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4420,18 +4638,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());
   }
 
 
-  CheckPairs(*mSessionOff, "Offerer pairs");
-  CheckPairs(*mSessionAns, "Answerer pairs");
+  CheckTransceivers(*mSessionOff, "Offerer transceivers");
+  CheckTransceivers(*mSessionAns, "Answerer transceivers");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_F(JsepSessionTest, TestNonDefaultProtocol)
 {
   AddTracks(*mSessionOff, "audio,video,datachannel");
   AddTracks(*mSessionAns, "audio,video,datachannel");
@@ -4553,60 +4771,48 @@ TEST_F(JsepSessionTest, CreateOfferDontR
 }
 
 TEST_F(JsepSessionTest, CreateOfferRemoveAudioTrack)
 {
   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));
-
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.front());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
-
-  CreateOffer(Some(options));
+  SetDirection(*mSessionOff, 1, SdpDirectionAttribute::kSendonly);
+  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
+  ASSERT_FALSE(removedTrack.IsNull());
+
+  CreateOffer();
 }
 
 TEST_F(JsepSessionTest, CreateOfferDontReceiveAudioRemoveAudioTrack)
 {
   types.push_back(SdpMediaSection::kAudio);
   types.push_back(SdpMediaSection::kVideo);
   AddTracks(*mSessionOff, "audio,video");
 
-  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));
+  SetDirection(*mSessionOff, 0, SdpDirectionAttribute::kSendonly);
+  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
+  ASSERT_FALSE(removedTrack.IsNull());
+
+  CreateOffer();
 }
 
 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));
 
-  RefPtr<JsepTrack> removedTrack = GetTrackOff(0, types.back());
-  ASSERT_TRUE(removedTrack);
-  ASSERT_EQ(NS_OK, mSessionOff->RemoveTrack(removedTrack->GetStreamId(),
-                                           removedTrack->GetTrackId()));
+  JsepTrack removedTrack = RemoveTrack(*mSessionOff, 0);
+  ASSERT_FALSE(removedTrack.IsNull());
 
   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;
@@ -5244,19 +5450,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->GetTransports()[0]->mDtls->GetRole());
+      mSessionOff->GetTransceivers()[0].mTransport->mDtls->GetRole());
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionAns->GetTransports()[0]->mDtls->GetRole());
+      mSessionAns->GetTransceivers()[0].mTransport->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");
@@ -5296,19 +5502,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->GetTransports()[0]->mDtls->GetRole());
+      mSessionOff->GetTransceivers()[0].mTransport->mDtls->GetRole());
   ASSERT_EQ(JsepDtlsTransport::kJsepDtlsClient,
-      mSessionAns->GetTransports()[0]->mDtls->GetRole());
+      mSessionAns->GetTransceivers()[0].mTransport->mDtls->GetRole());
 }
 
 // Verify that 'holdconn' gets rejected
 TEST_F(JsepSessionTest, AudioCallDtlsRoleHoldconn)
 {
   types.push_back(SdpMediaSection::kAudio);
   AddTracks(*mSessionOff, "audio");
   AddTracks(*mSessionAns, "audio");
@@ -5462,9 +5668,661 @@ 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());
+
+  // TODO: Once nils' code for switching offer/answer roles lands, switch and
+  // have the other side reoffer to negotiate the new transceivers.
+}
+
+// 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());
+  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());
+}
+
+TEST_F(JsepSessionTest, OffererRecycleNoMagicAnswererStopsTransceiver)
+{
+  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);
+
+  // 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());
+  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());
+}
+
+TEST_F(JsepSessionTest, RecycleRollback)
+{
+  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);
+
+  AddTracks(*mSessionOff, "audio");
+
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0].GetLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1].IsAssociated());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsAssociated());
+
+  std::string offer = CreateOffer();
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsAssociated());
+
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsStopped());
+  // This should now be associated
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[2].IsAssociated());
+
+  ASSERT_EQ(NS_OK,
+            mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
+
+  // Rollback should not change the levels of any of these, since those are set
+  // in CreateOffer.
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].HasLevel());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsStopped());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsStopped());
+  // This should no longer be associated
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsAssociated());
+}
+
+TEST_F(JsepSessionTest, AddTrackMagicWithNullReplaceTrack)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+
+  AddTracks(*mSessionAns, "audio");
+  AddTracks(*mSessionOff, "audio");
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+
+  // Ok, transceiver 2 is "magical". Ensure it still has this "magical"
+  // auto-matching property even if we null it out with replaceTrack.
+  mSessionAns->GetTransceivers()[2].mSending.ClearSendTrack("fake");
+  mSessionAns->GetTransceivers()[2].mJsDirection =
+    SdpDirectionAttribute::Direction::kRecvonly;
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+
+  ASSERT_EQ(3U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionOff->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[2].IsStopped());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[2].IsAssociated());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[2].HasAddTrackMagic());
+}
+
+// Flipside of AddTrackMagicWithNullReplaceTrack; we want to check that
+// auto-matching does not work for transceivers that were created without a
+// track, but were later given a track with replaceTrack.
+TEST_F(JsepSessionTest, NoAddTrackMagicReplaceTrack)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  AddTracks(*mSessionOff, "audio");
+  mSessionAns->AddTransceiver(
+      JsepTransceiver(SdpMediaSection::MediaType::kAudio));
+
+  mSessionAns->GetTransceivers()[2].mSending.UpdateSendTrack(
+      {"newstream"}, "newtrack");
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(4U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[3].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[3].IsAssociated());
+}
+
+// Check that transceivers that were created without a send track, but that
+// were subsequently given a send track with addTrack, are now "magical".
+TEST_F(JsepSessionTest, AddTrackMakesTransceiverMagical)
+{
+  types = BuildTypes("audio,video");
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+
+  OfferAnswer();
+
+  ASSERT_EQ(2U, mSessionOff->GetTransceivers().size());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers().size());
+  AddTracks(*mSessionOff, "audio");
+  mSessionAns->AddTransceiver(
+      JsepTransceiver(SdpMediaSection::MediaType::kAudio));
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+
+  // :D MAGIC! D:
+  AddTracks(*mSessionAns, "audio");
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers().size());
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[1].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+}
+
+TEST_F(JsepSessionTest, ComplicatedRemoteRollback)
+{
+  AddTracks(*mSessionOff, "audio,audio,audio,video");
+  AddTracks(*mSessionAns, "video,video");
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  // Three recvonly for audio, one sendrecv for video, and one (unmapped) for
+  // the second video track.
+  ASSERT_EQ(5U, mSessionAns->GetTransceivers().size());
+  // First video transceiver; auto matched with offer
+  ASSERT_EQ(3U, mSessionAns->GetTransceivers()[0].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].WasCreatedBySetRemote());
+
+  // Second video transceiver, not matched with offer
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].WasCreatedBySetRemote());
+
+  // Audio transceiver, created due to application of SetRemote
+  ASSERT_EQ(0U, mSessionAns->GetTransceivers()[2].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].WasCreatedBySetRemote());
+
+  // Audio transceiver, created due to application of SetRemote
+  ASSERT_EQ(1U, mSessionAns->GetTransceivers()[3].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[3].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[3].WasCreatedBySetRemote());
+
+  // Audio transceiver, created due to application of SetRemote
+  ASSERT_EQ(2U, mSessionAns->GetTransceivers()[4].GetLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4].IsStopped());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[4].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4].HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[4].WasCreatedBySetRemote());
+
+  // This will cause the first audio transceiver to become "magical", and
+  // thereby it will stick around after rollback, even though we clear it out
+  // with replaceTrack.
+  AddTracks(*mSessionAns, "audio");
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+  mSessionAns->GetTransceivers()[2].mSending.ClearSendTrack("fake");
+  mSessionAns->GetTransceivers()[2].mJsDirection =
+    SdpDirectionAttribute::Direction::kRecvonly;
+
+  // We do nothing with the second audio transceiver; when we rollback, it will
+  // disappear entirely.
+
+  // This will cause the third audio transceiver to stick around, because it
+  // has a send track, even though it isn't "magical".
+  mSessionAns->GetTransceivers()[4].mSending.UpdateSendTrack(
+      {"newstream"}, "newtrack");
+
+  // Create a fourth audio transceiver. Rollback will leave it alone, since we
+  // created it.
+  mSessionAns->AddTransceiver(JsepTransceiver(
+        SdpMediaSection::MediaType::kAudio,
+        SdpDirectionAttribute::Direction::kRecvonly));
+
+  ASSERT_EQ(NS_OK,
+            mSessionAns->SetRemoteDescription(kJsepSdpRollback, ""));
+
+  // Three recvonly for audio, one sendrecv for video, and one (unmapped) for
+  // the second video track.
+  ASSERT_EQ(5U, mSessionAns->GetTransceivers().size());
+
+  // First video transceiver
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[0].HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[0].mSending.IsNull());
+
+  // Second video transceiver
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[1].HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[1].mSending.IsNull());
+
+  // First audio transceiver, kept because AddTrack touched it, even though we
+  // removed the send track after.
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[2].IsAssociated());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[2].mSending.IsNull());
+
+  // Second audio transceiver should be gone.
+
+  // Third audio transceiver, created due to application of SetRemote, but then
+  // given a send track with replaceTrack (which preserves it).
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].HasAddTrackMagic());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[3].mSending.IsNull());
+
+  // Fourth audio transceiver, created after SetRemote
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4].HasLevel());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4].IsStopped());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4].IsAssociated());
+  ASSERT_FALSE(mSessionAns->GetTransceivers()[4].HasAddTrackMagic());
+  ASSERT_TRUE(mSessionAns->GetTransceivers()[4].mSending.IsNull());
+}
+
+TEST_F(JsepSessionTest, LocalRollback)
+{
+  AddTracks(*mSessionOff, "audio,video");
+  AddTracks(*mSessionAns, "audio,video");
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[1].IsAssociated());
+  ASSERT_EQ(NS_OK,
+            mSessionOff->SetLocalDescription(kJsepSdpRollback, ""));
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].IsAssociated());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[1].IsAssociated());
+}
+
+TEST_F(JsepSessionTest, JsStopsTransceiverBeforeAnswer)
+{
+  AddTracks(*mSessionOff, "audio,video");
+  AddTracks(*mSessionAns, "audio,video");
+
+  std::string offer = CreateOffer();
+  SetLocalOffer(offer, CHECK_SUCCESS);
+  SetRemoteOffer(offer, CHECK_SUCCESS);
+
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer, CHECK_SUCCESS);
+
+  // Now JS decides to stop a transceiver. Make sure transport stuff is still
+  // ready to go when the answer is set. This should only prevent the flow of
+  // media for that transceiver.
+
+  mSessionOff->GetTransceivers()[0].Stop();
+  SetRemoteAnswer(answer, CHECK_SUCCESS);
+
+  ASSERT_TRUE(mSessionOff->GetTransceivers()[0].IsStopped());
+  ASSERT_EQ(1U, mSessionOff->GetTransceivers()[0].mTransport->mComponents);
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].mSending.GetActive());
+  ASSERT_FALSE(mSessionOff->GetTransceivers()[0].mReceiving.GetActive());
+}
+
 } // namespace mozilla
+
--- a/media/webrtc/signaling/gtest/jsep_track_unittest.cpp
+++ b/media/webrtc/signaling/gtest/jsep_track_unittest.cpp
@@ -11,17 +11,22 @@
 #include "signaling/src/sdp/SipccSdp.h"
 #include "signaling/src/sdp/SdpHelper.h"
 
 namespace mozilla {
 
 class JsepTrackTest : public ::testing::Test
 {
   public:
-    JsepTrackTest() {}
+    JsepTrackTest() :
+      mSendOff(JsepTrack::MakeNullSendTrack(SdpMediaSection::kAudio, "fake")),
+      mRecvOff(JsepTrack::MakeNullRecvTrack(SdpMediaSection::kAudio)),
+      mSendAns(JsepTrack::MakeNullSendTrack(SdpMediaSection::kAudio, "fake")),
+      mRecvAns(JsepTrack::MakeNullRecvTrack(SdpMediaSection::kAudio))
+    {}
 
     std::vector<JsepCodecDescription*>
     MakeCodecs(bool addFecCodecs = false,
                bool preferRed = false,
                bool addDtmfCodec = false) const
     {
       std::vector<JsepCodecDescription*> results;
       results.push_back(
@@ -98,101 +103,104 @@ class JsepTrackTest : public ::testing::
 
     void InitCodecs() {
       mOffCodecs.values = MakeCodecs();
       mAnsCodecs.values = MakeCodecs();
     }
 
     void InitTracks(SdpMediaSection::MediaType type)
     {
-      mSendOff = new JsepTrack(type, "stream_id", "track_id", sdp::kSend);
-      mRecvOff = new JsepTrack(type, "stream_id", "track_id", sdp::kRecv);
-      mSendOff->PopulateCodecs(mOffCodecs.values);
-      mRecvOff->PopulateCodecs(mOffCodecs.values);
+      mSendOff = JsepTrack::MakeSendTrack(type, "stream_id", "track_id");
+      mRecvOff = JsepTrack::MakeNullRecvTrack(type);
+      mSendOff.PopulateCodecs(mOffCodecs.values);
+      mRecvOff.PopulateCodecs(mOffCodecs.values);
 
-      mSendAns = new JsepTrack(type, "stream_id", "track_id", sdp::kSend);
-      mRecvAns = new JsepTrack(type, "stream_id", "track_id", sdp::kRecv);
-      mSendAns->PopulateCodecs(mAnsCodecs.values);
-      mRecvAns->PopulateCodecs(mAnsCodecs.values);
+      mSendAns = JsepTrack::MakeSendTrack(type, "stream_id", "track_id");
+      mRecvAns = JsepTrack::MakeNullRecvTrack(type);
+      mSendAns.PopulateCodecs(mAnsCodecs.values);
+      mRecvAns.PopulateCodecs(mAnsCodecs.values);
     }
 
     void InitSdp(SdpMediaSection::MediaType type)
     {
+      std::vector<std::string> msids(1, "*");
+      std::string error;
+      SdpHelper helper(&error);
+
       mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
       mOffer->AddMediaSection(
           type,
-          SdpDirectionAttribute::kInactive,
+          SdpDirectionAttribute::kSendrecv,
           0,
           SdpHelper::GetProtocolForMediaType(type),
           sdp::kIPv4,
           "0.0.0.0");
+      // JsepTrack doesn't set msid-semantic
+      helper.SetupMsidSemantic(msids, mOffer.get());
+
       mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
       mAnswer->AddMediaSection(
           type,
-          SdpDirectionAttribute::kInactive,
+          SdpDirectionAttribute::kSendrecv,
           0,
           SdpHelper::GetProtocolForMediaType(type),
           sdp::kIPv4,
           "0.0.0.0");
+      // JsepTrack doesn't set msid-semantic
+      helper.SetupMsidSemantic(msids, mAnswer.get());
     }
 
     SdpMediaSection& GetOffer()
     {
       return mOffer->GetMediaSection(0);
     }
 
     SdpMediaSection& GetAnswer()
     {
       return mAnswer->GetMediaSection(0);
     }
 
     void CreateOffer()
     {
-      if (mSendOff) {
-        mSendOff->AddToOffer(&GetOffer());
-      }
-
-      if (mRecvOff) {
-        mRecvOff->AddToOffer(&GetOffer());
-      }
+      mSendOff.AddToOffer(mSsrcGenerator, &GetOffer());
+      mRecvOff.AddToOffer(mSsrcGenerator, &GetOffer());
     }
 
     void CreateAnswer()
     {
-      if (mSendAns && GetOffer().IsReceiving()) {
-        mSendAns->AddToAnswer(GetOffer(), &GetAnswer());
-      }
+      mRecvAns.UpdateRecvTrack(*mOffer, GetOffer());
 
-      if (mRecvAns && GetOffer().IsSending()) {
-        mRecvAns->AddToAnswer(GetOffer(), &GetAnswer());
-      }
+      mSendAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer());
+      mRecvAns.AddToAnswer(GetOffer(), mSsrcGenerator, &GetAnswer());
     }
 
     void Negotiate()
     {
       std::cerr << "Offer SDP: " << std::endl;
       mOffer->Serialize(std::cerr);
 
       std::cerr << "Answer SDP: " << std::endl;
       mAnswer->Serialize(std::cerr);
 
-      if (mSendAns && GetAnswer().IsSending()) {
-        mSendAns->Negotiate(GetAnswer(), GetOffer());
+      mRecvOff.UpdateRecvTrack(*mAnswer, GetAnswer());
+
+      if (!mSendAns.IsNull() && GetAnswer().IsSending()) {
+        mSendAns.Negotiate(GetAnswer(), GetOffer());
       }
 
-      if (mRecvAns && GetAnswer().IsReceiving()) {
-        mRecvAns->Negotiate(GetAnswer(), GetOffer());
+      if (!mRecvAns.IsNull() && GetAnswer().IsReceiving()) {
+        mRecvAns.Negotiate(GetAnswer(), GetOffer());
       }
 
-      if (mSendOff && GetAnswer().IsReceiving()) {
-        mSendOff->Negotiate(GetAnswer(), GetAnswer());
+      if (!mSendOff.IsNull() && GetAnswer().IsReceiving()) {
+        mSendOff.Negotiate(GetAnswer(), GetAnswer());
       }
 
-      if (mRecvOff && GetAnswer().IsSending()) {
-        mRecvOff->Negotiate(GetAnswer(), GetAnswer());
+      if (!mRecvOff.IsNull() && GetAnswer().IsSending()) {
+        mRecvOff.Negotiate(GetAnswer(), GetAnswer());
       }
     }
 
     void OfferAnswer()
     {
       CreateOffer();
       CreateAnswer();
       Negotiate();
@@ -204,32 +212,32 @@ class JsepTrackTest : public ::testing::
       return track->GetNegotiatedDetails()->GetEncodingCount();
     }
 
     // TODO: Look into writing a macro that wraps an ASSERT_ and returns false
     // if it fails (probably requires writing a bool-returning function that
     // takes a void-returning lambda with a bool outparam, which will in turn
     // invokes the ASSERT_)
     static void CheckEncodingCount(size_t expected,
-                                   const RefPtr<JsepTrack>& send,
-                                   const RefPtr<JsepTrack>& recv)
+                                   const JsepTrack& send,
+                                   const JsepTrack& recv)
     {
       if (expected) {
-        ASSERT_TRUE(!!send);
-        ASSERT_TRUE(send->GetNegotiatedDetails());
-        ASSERT_TRUE(!!recv);
-        ASSERT_TRUE(recv->GetNegotiatedDetails());
+        ASSERT_FALSE(send.IsNull());
+        ASSERT_TRUE(send.GetNegotiatedDetails());
+        ASSERT_FALSE(recv.IsNull());
+        ASSERT_TRUE(recv.GetNegotiatedDetails());
       }
 
-      if (send && send->GetNegotiatedDetails()) {
-        ASSERT_EQ(expected, send->GetNegotiatedDetails()->GetEncodingCount());
+      if (!send.IsNull() && send.GetNegotiatedDetails()) {
+        ASSERT_EQ(expected, send.GetNegotiatedDetails()->GetEncodingCount());
       }
 
-      if (recv && recv->GetNegotiatedDetails()) {
-        ASSERT_EQ(expected, recv->GetNegotiatedDetails()->GetEncodingCount());
+      if (!recv.IsNull() && recv.GetNegotiatedDetails()) {
+        ASSERT_EQ(expected, recv.GetNegotiatedDetails()->GetEncodingCount());
       }
     }
 
     void CheckOffEncodingCount(size_t expected) const
     {
       CheckEncodingCount(expected, mSendOff, mRecvAns);
     }
 
@@ -309,16 +317,17 @@ class JsepTrackTest : public ::testing::
 
     void SanityCheckCodecs(const JsepCodecDescription& a,
                            const JsepCodecDescription& b) const
     {
       ASSERT_EQ(a.mType, b.mType);
       if (a.mType != SdpMediaSection::kApplication) {
         ASSERT_EQ(a.mDefaultPt, b.mDefaultPt);
       }
+      std::cerr << a.mName << " vs " << b.mName << std::endl;
       ASSERT_EQ(a.mName, b.mName);
       ASSERT_EQ(a.mClock, b.mClock);
       ASSERT_EQ(a.mChannels, b.mChannels);
       ASSERT_NE(a.mDirection, b.mDirection);
       // These constraints are for fmtp and rid, which _are_ signaled
       ASSERT_EQ(a.mConstraints, b.mConstraints);
 
       if (a.mType == SdpMediaSection::kVideo) {
@@ -360,48 +369,49 @@ class JsepTrackTest : public ::testing::
       if (!a.GetNegotiatedDetails()) {
         ASSERT_FALSE(!!b.GetNegotiatedDetails());
         return;
       }
 
       ASSERT_TRUE(!!a.GetNegotiatedDetails());
       ASSERT_TRUE(!!b.GetNegotiatedDetails());
       ASSERT_EQ(a.GetMediaType(), b.GetMediaType());
-      ASSERT_EQ(a.GetStreamId(), b.GetStreamId());
+      ASSERT_EQ(a.GetStreamIds(), b.GetStreamIds());
       ASSERT_EQ(a.GetTrackId(), b.GetTrackId());
       ASSERT_EQ(a.GetCNAME(), b.GetCNAME());
       ASSERT_NE(a.GetDirection(), b.GetDirection());
       ASSERT_EQ(a.GetSsrcs().size(), b.GetSsrcs().size());
       for (size_t i = 0; i < a.GetSsrcs().size(); ++i) {
         ASSERT_EQ(a.GetSsrcs()[i], b.GetSsrcs()[i]);
       }
 
       SanityCheckNegotiatedDetails(*a.GetNegotiatedDetails(),
                                    *b.GetNegotiatedDetails());
     }
 
     void SanityCheck() const
     {
-      if (mSendOff && mRecvAns) {
-        SanityCheckTracks(*mSendOff, *mRecvAns);
+      if (!mSendOff.IsNull() && !mRecvAns.IsNull()) {
+        SanityCheckTracks(mSendOff, mRecvAns);
       }
-      if (mRecvOff && mSendAns) {
-        SanityCheckTracks(*mRecvOff, *mSendAns);
+      if (!mRecvOff.IsNull() && !mSendAns.IsNull()) {
+        SanityCheckTracks(mRecvOff, mSendAns);
       }
     }
 
   protected:
-    RefPtr<JsepTrack> mSendOff;
-    RefPtr<JsepTrack> mRecvOff;
-    RefPtr<JsepTrack> mSendAns;
-    RefPtr<JsepTrack> mRecvAns;
+    JsepTrack mSendOff;
+    JsepTrack mRecvOff;
+    JsepTrack mSendAns;
+    JsepTrack mRecvAns;
     PtrVector<JsepCodecDescription> mOffCodecs;
     PtrVector<JsepCodecDescription> mAnsCodecs;
     UniquePtr<Sdp> mOffer;
     UniquePtr<Sdp> mAnswer;
+    SsrcGenerator mSsrcGenerator;
 };
 
 TEST_F(JsepTrackTest, CreateDestroy)
 {
   Init(SdpMediaSection::kAudio);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiation)
@@ -440,30 +450,29 @@ private:
 };
 
 TEST_F(JsepTrackTest, CheckForMismatchedAudioCodecAndVideoTrack)
 {
   PtrVector<JsepCodecDescription> offerCodecs;
 
   // make codecs including telephone-event (an audio codec)
   offerCodecs.values = MakeCodecs(false, false, true);
-  RefPtr<JsepTrack> videoTrack = new JsepTrack(SdpMediaSection::kVideo,
-                                               "stream_id",
-                                               "track_id",
-                                               sdp::kSend);
+  JsepTrack videoTrack = JsepTrack::MakeSendTrack(SdpMediaSection::kVideo,
+                                                  "stream_id",
+                                                  "track_id");
   // populate codecs and then make sure we don't have any audio codecs
   // in the video track
-  videoTrack->PopulateCodecs(offerCodecs.values);
+  videoTrack.PopulateCodecs(offerCodecs.values);
 
   bool found = false;
-  videoTrack->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
 
   found = false;
-  videoTrack->ForEachCodec(CheckForCodecType(SdpMediaSection::kVideo, &found));
+  videoTrack.ForEachCodec(CheckForCodecType(SdpMediaSection::kVideo, &found));
   ASSERT_TRUE(found); // for sanity, make sure we did find video codecs
 }
 
 TEST_F(JsepTrackTest, CheckVideoTrackWithHackedDtmfSdp)
 {
   Init(SdpMediaSection::kVideo);
   CreateOffer();
   // make sure we don't find sdp containing telephone-event in video track
@@ -486,31 +495,26 @@ TEST_F(JsepTrackTest, CheckVideoTrackWit
             std::string::npos);
 
   Negotiate();
   SanityCheck();
 
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  ASSERT_TRUE(mSendOff.get());
-  ASSERT_TRUE(mRecvOff.get());
-  ASSERT_TRUE(mSendAns.get());
-  ASSERT_TRUE(mRecvAns.get());
-
   // make sure we still don't find any audio codecs in the video track after
   // hacking the sdp
   bool found = false;
-  mSendOff->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mSendOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
-  mRecvOff->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mRecvOff.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
-  mSendAns->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mSendAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
-  mRecvAns->ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
+  mRecvAns.ForEachCodec(CheckForCodecType(SdpMediaSection::kAudio, &found));
   ASSERT_FALSE(found);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationOffererDtmf)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, false);
 
@@ -525,23 +529,23 @@ TEST_F(JsepTrackTest, AudioNegotiationOf
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns)));
   ASSERT_EQ("1", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationAnswererDtmf)
 {
   mOffCodecs.values = MakeCodecs(false, false, false);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -556,23 +560,23 @@ TEST_F(JsepTrackTest, AudioNegotiationAn
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns)));
   ASSERT_EQ("1", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationOffererAnswererDtmf)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -587,32 +591,32 @@ TEST_F(JsepTrackTest, AudioNegotiationOf
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererFmtp)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -634,32 +638,32 @@ TEST_F(JsepTrackTest, AudioNegotiationDt
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererFmtpAnswererNoFmtp)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -681,32 +685,32 @@ TEST_F(JsepTrackTest, AudioNegotiationDt
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:101 0-15"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, AudioNegotiationDtmfOffererNoFmtpAnswererNoFmtp)
 {
   mOffCodecs.values = MakeCodecs(false, false, true);
   mAnsCodecs.values = MakeCodecs(false, false, true);
 
@@ -729,32 +733,32 @@ TEST_F(JsepTrackTest, AudioNegotiationDt
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:101 telephone-event"),
             std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:101"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:101"), std::string::npos);
 
   const JsepAudioCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2)));
   ASSERT_EQ("1", track->mDefaultPt);
 
-  ASSERT_TRUE((track = GetAudioCodec(*mSendOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvOff, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvOff, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mSendAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mSendAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
-  ASSERT_TRUE((track = GetAudioCodec(*mRecvAns, 2, 1)));
+  ASSERT_TRUE((track = GetAudioCodec(mRecvAns, 2, 1)));
   ASSERT_EQ("101", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererFEC)
 {
   mOffCodecs.values = MakeCodecs(true);
   mAnsCodecs.values = MakeCodecs(false);
 
@@ -769,23 +773,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns)));
   ASSERT_EQ("120", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationAnswererFEC)
 {
   mOffCodecs.values = MakeCodecs(false);
   mAnsCodecs.values = MakeCodecs(true);
 
@@ -800,23 +804,23 @@ TEST_F(JsepTrackTest, VideoNegotationAns
   ASSERT_EQ(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_EQ(mOffer->ToString().find("a=fmtp:122"), std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=fmtp:122"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns)));
   ASSERT_EQ("120", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFEC)
 {
   mOffCodecs.values = MakeCodecs(true);
   mAnsCodecs.values = MakeCodecs(true);
 
@@ -831,23 +835,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 4)));
   ASSERT_EQ("120", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECPreferred)
 {
   mOffCodecs.values = MakeCodecs(true, true);
   mAnsCodecs.values = MakeCodecs(true);
 
@@ -862,23 +866,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 4)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 4)));
   ASSERT_EQ("122", track->mDefaultPt);
 }
 
 // Make sure we only put the right things in the fmtp:122 120/.... line
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECMismatch)
 {
   mOffCodecs.values = MakeCodecs(true, true);
   mAnsCodecs.values = MakeCodecs(true);
@@ -897,23 +901,23 @@ TEST_F(JsepTrackTest, VideoNegotationOff
   ASSERT_NE(mOffer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:122 red"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtpmap:123 ulpfec"), std::string::npos);
 
   ASSERT_NE(mOffer->ToString().find("a=fmtp:122 120/126/123"), std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=fmtp:122 120/123"), std::string::npos);
 
   const JsepVideoCodecDescription* track = nullptr;
-  ASSERT_TRUE((track = GetVideoCodec(*mSendOff, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendOff, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvOff, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvOff, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mSendAns, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mSendAns, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
-  ASSERT_TRUE((track = GetVideoCodec(*mRecvAns, 3)));
+  ASSERT_TRUE((track = GetVideoCodec(mRecvAns, 3)));
   ASSERT_EQ("122", track->mDefaultPt);
 }
 
 TEST_F(JsepTrackTest, VideoNegotationOffererAnswererFECZeroVP9Codec)
 {
   mOffCodecs.values = MakeCodecs(true);
   JsepVideoCodecDescription* vp9 =
     new JsepVideoCodecDescription("0", "VP9", 90000);
@@ -959,21 +963,21 @@ TEST_F(JsepTrackTest, VideoNegotiationOf
   // make sure REMB is on offer and not on answer
   ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  CheckOtherFbsSize(*mSendOff, 0);
-  CheckOtherFbsSize(*mRecvAns, 0);
+  CheckOtherFbsSize(mSendOff, 0);
+  CheckOtherFbsSize(mRecvAns, 0);
 
-  CheckOtherFbsSize(*mSendAns, 0);
-  CheckOtherFbsSize(*mRecvOff, 0);
+  CheckOtherFbsSize(mSendAns, 0);
+  CheckOtherFbsSize(mRecvOff, 0);
 }
 
 TEST_F(JsepTrackTest, VideoNegotiationAnswerRemb)
 {
   InitCodecs();
   // enable remb on the answer codecs
   ((JsepVideoCodecDescription*)mAnsCodecs.values[2])->EnableRemb();
   InitTracks(SdpMediaSection::kVideo);
@@ -983,21 +987,21 @@ TEST_F(JsepTrackTest, VideoNegotiationAn
   // make sure REMB is not on offer and not on answer
   ASSERT_EQ(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   ASSERT_EQ(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  CheckOtherFbsSize(*mSendOff, 0);
-  CheckOtherFbsSize(*mRecvAns, 0);
+  CheckOtherFbsSize(mSendOff, 0);
+  CheckOtherFbsSize(mRecvAns, 0);
 
-  CheckOtherFbsSize(*mSendAns, 0);
-  CheckOtherFbsSize(*mRecvOff, 0);
+  CheckOtherFbsSize(mSendAns, 0);
+  CheckOtherFbsSize(mRecvOff, 0);
 }
 
 TEST_F(JsepTrackTest, VideoNegotiationOfferAnswerRemb)
 {
   InitCodecs();
   // enable remb on the offer and answer codecs
   ((JsepVideoCodecDescription*)mOffCodecs.values[2])->EnableRemb();
   ((JsepVideoCodecDescription*)mAnsCodecs.values[2])->EnableRemb();
@@ -1008,96 +1012,98 @@ TEST_F(JsepTrackTest, VideoNegotiationOf
   // make sure REMB is on offer and on answer
   ASSERT_NE(mOffer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   ASSERT_NE(mAnswer->ToString().find("a=rtcp-fb:120 goog-remb"),
             std::string::npos);
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 
-  CheckOtherFbsSize(*mSendOff, 1);
-  CheckOtherFbsSize(*mRecvAns, 1);
-  CheckOtherFbExists(*mSendOff, SdpRtcpFbAttributeList::kRemb);
-  CheckOtherFbExists(*mRecvAns, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbsSize(mSendOff, 1);
+  CheckOtherFbsSize(mRecvAns, 1);
+  CheckOtherFbExists(mSendOff, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbExists(mRecvAns, SdpRtcpFbAttributeList::kRemb);
 
-  CheckOtherFbsSize(*mSendAns, 1);
-  CheckOtherFbsSize(*mRecvOff, 1);
-  CheckOtherFbExists(*mSendAns, SdpRtcpFbAttributeList::kRemb);
-  CheckOtherFbExists(*mRecvOff, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbsSize(mSendAns, 1);
+  CheckOtherFbsSize(mRecvOff, 1);
+  CheckOtherFbExists(mSendAns, SdpRtcpFbAttributeList::kRemb);
+  CheckOtherFbExists(mRecvOff, SdpRtcpFbAttributeList::kRemb);
 }
 
 TEST_F(JsepTrackTest, AudioOffSendonlyAnsRecvonly)
 {
   Init(SdpMediaSection::kAudio);
-  mRecvOff = nullptr;
-  mSendAns = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kSendonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
 TEST_F(JsepTrackTest, VideoOffSendonlyAnsRecvonly)
 {
   Init(SdpMediaSection::kVideo);
-  mRecvOff = nullptr;
-  mSendAns = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kSendonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
 TEST_F(JsepTrackTest, AudioOffSendrecvAnsRecvonly)
 {
   Init(SdpMediaSection::kAudio);
-  mSendAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
 TEST_F(JsepTrackTest, VideoOffSendrecvAnsRecvonly)
 {
   Init(SdpMediaSection::kVideo);
-  mSendAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kRecvonly);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(0);
 }
 
-TEST_F(JsepTrackTest, AudioOffRecvonlyAnsSendrecv)
+TEST_F(JsepTrackTest, AudioOffRecvonlyAnsSendonly)
 {
   Init(SdpMediaSection::kAudio);
-  mSendOff = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
-TEST_F(JsepTrackTest, VideoOffRecvonlyAnsSendrecv)
+TEST_F(JsepTrackTest, VideoOffRecvonlyAnsSendonly)
 {
   Init(SdpMediaSection::kVideo);
-  mSendOff = nullptr;
+  GetOffer().SetDirection(SdpDirectionAttribute::kRecvonly);
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, AudioOffSendrecvAnsSendonly)
 {
   Init(SdpMediaSection::kAudio);
-  mRecvAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, VideoOffSendrecvAnsSendonly)
 {
   Init(SdpMediaSection::kVideo);
-  mRecvAns = nullptr;
+  GetAnswer().SetDirection(SdpDirectionAttribute::kSendonly);
   OfferAnswer();
   CheckOffEncodingCount(0);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, DataChannelDraft05)
 {
   Init(SdpMediaSection::kApplication);
@@ -1156,25 +1162,25 @@ TEST_F(JsepTrackTest, DataChannelDraft21
 {
   mOffCodecs.values = MakeCodecs(false, false, false);
   mAnsCodecs.values = MakeCodecs(false, false, false);
   InitTracks(SdpMediaSection::kApplication);
 
   mOffer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
   mOffer->AddMediaSection(
       SdpMediaSection::kApplication,
-      SdpDirectionAttribute::kInactive,
+      SdpDirectionAttribute::kSendrecv,
       0,
       SdpMediaSection::kUdpDtlsSctp,
       sdp::kIPv4,
       "0.0.0.0");
   mAnswer.reset(new SipccSdp(SdpOrigin("", 0, 0, sdp::kIPv4, "")));
   mAnswer->AddMediaSection(
       SdpMediaSection::kApplication,
-      SdpDirectionAttribute::kInactive,
+      SdpDirectionAttribute::kSendrecv,
       0,
       SdpMediaSection::kUdpDtlsSctp,
       sdp::kIPv4,
       "0.0.0.0");
 
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
@@ -1197,84 +1203,86 @@ MakeConstraints(const std::string& rid, 
 }
 
 TEST_F(JsepTrackTest, SimulcastRejected)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendOff->SetJsConstraints(constraints);
+  mSendOff.SetJsConstraints(constraints);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, SimulcastPrevented)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendAns->SetJsConstraints(constraints);
+  mSendAns.SetJsConstraints(constraints);
   OfferAnswer();
   CheckOffEncodingCount(1);
   CheckAnsEncodingCount(1);
 }
 
 TEST_F(JsepTrackTest, SimulcastOfferer)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendOff->SetJsConstraints(constraints);
+  mSendOff.SetJsConstraints(constraints);
   CreateOffer();
   CreateAnswer();
   // Add simulcast/rid to answer
-  JsepTrack::AddToMsection(constraints, sdp::kRecv, &GetAnswer());
+  mRecvAns.AddToMsection(
+      constraints, sdp::kRecv, mSsrcGenerator, &GetAnswer());
   Negotiate();
-  ASSERT_TRUE(mSendOff->GetNegotiatedDetails());
-  ASSERT_EQ(2U, mSendOff->GetNegotiatedDetails()->GetEncodingCount());
-  ASSERT_EQ("foo", mSendOff->GetNegotiatedDetails()->GetEncoding(0).mRid);
+  ASSERT_TRUE(mSendOff.GetNegotiatedDetails());
+  ASSERT_EQ(2U, mSendOff.GetNegotiatedDetails()->GetEncodingCount());
+  ASSERT_EQ("foo", mSendOff.GetNegotiatedDetails()->GetEncoding(0).mRid);
   ASSERT_EQ(40000U,
-      mSendOff->GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
-  ASSERT_EQ("bar", mSendOff->GetNegotiatedDetails()->GetEncoding(1).mRid);
+      mSendOff.GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
+  ASSERT_EQ("bar", mSendOff.GetNegotiatedDetails()->GetEncoding(1).mRid);
   ASSERT_EQ(10000U,
-      mSendOff->GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
+      mSendOff.GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
   ASSERT_NE(std::string::npos,
             mOffer->ToString().find("a=simulcast: send rid=foo;bar"));
   ASSERT_NE(std::string::npos,
             mAnswer->ToString().find("a=simulcast: recv rid=foo;bar"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo send"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar send"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo recv"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar recv"));
 }
 
 TEST_F(JsepTrackTest, SimulcastAnswerer)
 {
   Init(SdpMediaSection::kVideo);
   std::vector<JsepTrack::JsConstraints> constraints;
   constraints.push_back(MakeConstraints("foo", 40000));
   constraints.push_back(MakeConstraints("bar", 10000));
-  mSendAns->SetJsConstraints(constraints);
+  mSendAns.SetJsConstraints(constraints);
   CreateOffer();
   // Add simulcast/rid to offer
-  JsepTrack::AddToMsection(constraints, sdp::kRecv, &GetOffer());
+  mRecvOff.AddToMsection(
+      constraints, sdp::kRecv, mSsrcGenerator, &GetOffer());
   CreateAnswer();
   Negotiate();
-  ASSERT_TRUE(mSendAns->GetNegotiatedDetails());
-  ASSERT_EQ(2U, mSendAns->GetNegotiatedDetails()->GetEncodingCount());
-  ASSERT_EQ("foo", mSendAns->GetNegotiatedDetails()->GetEncoding(0).mRid);
+  ASSERT_TRUE(mSendAns.GetNegotiatedDetails());
+  ASSERT_EQ(2U, mSendAns.GetNegotiatedDetails()->GetEncodingCount());
+  ASSERT_EQ("foo", mSendAns.GetNegotiatedDetails()->GetEncoding(0).mRid);
   ASSERT_EQ(40000U,
-      mSendAns->GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
-  ASSERT_EQ("bar", mSendAns->GetNegotiatedDetails()->GetEncoding(1).mRid);
+      mSendAns.GetNegotiatedDetails()->GetEncoding(0).mConstraints.maxBr);
+  ASSERT_EQ("bar", mSendAns.GetNegotiatedDetails()->GetEncoding(1).mRid);
   ASSERT_EQ(10000U,
-      mSendAns->GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
+      mSendAns.GetNegotiatedDetails()->GetEncoding(1).mConstraints.maxBr);
   ASSERT_NE(std::string::npos,
             mOffer->ToString().find("a=simulcast: recv rid=foo;bar"));
   ASSERT_NE(std::string::npos,
             mAnswer->ToString().find("a=simulcast: send rid=foo;bar"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:foo recv"));
   ASSERT_NE(std::string::npos, mOffer->ToString().find("a=rid:bar recv"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:foo send"));
   ASSERT_NE(std::string::npos, mAnswer->ToString().find("a=rid:bar send"));
@@ -1309,24 +1317,24 @@ TEST_F(JsepTrackTest, SimulcastAnswerer)
   };  \
 }
 
 TEST_F(JsepTrackTest, DefaultOpusParameters)
 {
   Init(SdpMediaSection::kAudio);
   OfferAnswer();
 
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendOff,
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendOff,
       SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendAns,
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendAns,
       SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvOff, 0U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvOff, false);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvAns, 0U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvAns, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U);
+  VERIFY_OPUS_FORCE_MONO(mRecvOff, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 0U);
+  VERIFY_OPUS_FORCE_MONO(mRecvAns, false);
 }
 
 TEST_F(JsepTrackTest, NonDefaultOpusParameters)
 {
   InitCodecs();
   for (auto& codec : mAnsCodecs.values) {
     if (codec->mName == "opus") {
       JsepAudioCodecDescription* audioCodec =
@@ -1334,20 +1342,20 @@ TEST_F(JsepTrackTest, NonDefaultOpusPara
       audioCodec->mMaxPlaybackRate = 16000;
       audioCodec->mForceMono = true;
     }
   }
   InitTracks(SdpMediaSection::kAudio);
   InitSdp(SdpMediaSection::kAudio);
   OfferAnswer();
 
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendOff, 16000U);
-  VERIFY_OPUS_FORCE_MONO(*mSendOff, true);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mSendAns,
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendOff, 16000U);
+  VERIFY_OPUS_FORCE_MONO(mSendOff, true);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mSendAns,
       SdpFmtpAttributeList::OpusParameters::kDefaultMaxPlaybackRate);
-  VERIFY_OPUS_FORCE_MONO(*mSendAns, false);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvOff, 0U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvOff, false);
-  VERIFY_OPUS_MAX_PLAYBACK_RATE(*mRecvAns, 16000U);
-  VERIFY_OPUS_FORCE_MONO(*mRecvAns, true);
+  VERIFY_OPUS_FORCE_MONO(mSendAns, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvOff, 0U);
+  VERIFY_OPUS_FORCE_MONO(mRecvOff, false);
+  VERIFY_OPUS_MAX_PLAYBACK_RATE(mRecvAns, 16000U);
+  VERIFY_OPUS_FORCE_MONO(mRecvAns, true);
 }
 
 } // namespace mozilla
--- a/media/webrtc/signaling/gtest/mediapipeline_unittest.cpp
+++ b/media/webrtc/signaling/gtest/mediapipeline_unittest.cpp
@@ -368,23 +368,19 @@ class TestAgentSend : public TestAgent {
       rtp = bundle_transport_.flow_;
       rtcp = nullptr;
     }
 
     audio_pipeline_ = new mozilla::MediaPipelineTransmit(
         test_pc,
         nullptr,
         test_utils->sts_target(),
+        false,
         audio_stream_track_.get(),
-        "audio_track_fake_uuid",
-        1,
-        audio_conduit_,
-        rtp,
-        rtcp,
-        nsAutoPtr<MediaPipelineFilter>());
+        audio_conduit_);
   }
 
   void SetUsingBundle(bool use_bundle) {
     use_bundle_ = use_bundle;
   }
 
  private:
   bool use_bundle_;
@@ -410,31 +406,26 @@ class TestAgentReceive : public TestAgen
     if (aIsRtcpMux) {
       ASSERT_FALSE(audio_rtcp_transport_.flow_);
     }
 
     audio_pipeline_ = new mozilla::MediaPipelineReceiveAudio(
         test_pc,
         nullptr,
         test_utils->sts_target(),
-        new FakeSourceMediaStream(), "audio_track_fake_uuid", 1, 1,
-        static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get()),
-        audio_rtp_transport_.flow_,
-        audio_rtcp_transport_.flow_,
-        bundle_filter_);
+        static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get()));
   }
 
   void SetBundleFilter(nsAutoPtr<MediaPipelineFilter> filter) {
     bundle_filter_ = filter;
   }
 
   void UpdateFilter_s(
       nsAutoPtr<MediaPipelineFilter> filter) {
-    audio_pipeline_->UpdateTransport_s(1,
-                                       audio_rtp_transport_.flow_,
+    audio_pipeline_->UpdateTransport_s(audio_rtp_transport_.flow_,
                                        audio_rtcp_transport_.flow_,
                                        filter);
   }
 
  private:
   nsAutoPtr<MediaPipelineFilter> bundle_filter_;
 };
 
--- a/media/webrtc/signaling/signaling.gyp
+++ b/media/webrtc/signaling/signaling.gyp
@@ -112,24 +112,24 @@
         './src/common/browser_logging/CSFLog.cpp',
         './src/common/browser_logging/CSFLog.h',
         './src/common/browser_logging/WebRtcLog.cpp',
         './src/common/browser_logging/WebRtcLog.h',
         # Browser Logging
         './src/common/time_profiling/timecard.c',
         './src/common/time_profiling/timecard.h',
         # PeerConnection
-        './src/peerconnection/MediaPipelineFactory.cpp',
-        './src/peerconnection/MediaPipelineFactory.h',
         './src/peerconnection/PeerConnectionCtx.cpp',
         './src/peerconnection/PeerConnectionCtx.h',
         './src/peerconnection/PeerConnectionImpl.cpp',
         './src/peerconnection/PeerConnectionImpl.h',
         './src/peerconnection/PeerConnectionMedia.cpp',
         './src/peerconnection/PeerConnectionMedia.h',
+        './src/peerconnection/TransceiverImpl.cpp',
+        './src/peerconnection/TransceiverImpl.h',
         # Media pipeline
         './src/mediapipeline/MediaPipeline.h',
         './src/mediapipeline/MediaPipeline.cpp',
         './src/mediapipeline/MediaPipelineFilter.h',
         './src/mediapipeline/MediaPipelineFilter.cpp',
         './src/mediapipeline/RtpLogger.h',
         './src/mediapipeline/RtpLogger.cpp',
          # SDP
@@ -171,17 +171,19 @@
          # JSEP
          './src/jsep/JsepCodecDescription.h',
          './src/jsep/JsepSession.h',
          './src/jsep/JsepSessionImpl.cpp',
          './src/jsep/JsepSessionImpl.h',
          './src/jsep/JsepTrack.cpp',
          './src/jsep/JsepTrack.h',
          './src/jsep/JsepTrackEncoding.h',
-         './src/jsep/JsepTransport.h'
+         './src/jsep/JsepTransport.h',
+         './src/jsep/SsrcGenerator.cpp',
+         './src/jsep/SsrcGenerator.h'
       ],
 
       #
       # DEFINES
       #
 
       'defines' : [
         'LOG4CXX_STATIC',
@@ -256,18 +258,16 @@
             './src/media-conduit/WebrtcMediaCodecVP8VideoCodec.cpp',
           ],
           'defines' : [
             'MOZ_WEBRTC_MEDIACODEC',
           ],
         }],
         ['(build_for_test==0) and (build_for_standalone==0)', {
           'sources': [
-            './src/peerconnection/MediaStreamList.cpp',
-            './src/peerconnection/MediaStreamList.h',
             './src/peerconnection/WebrtcGlobalInformation.cpp',
             './src/peerconnection/WebrtcGlobalInformation.h',
           ],
         }],
         ['build_for_test!=0', {
           'include_dirs': [
             './test'
           ],
--- a/media/webrtc/signaling/src/jsep/JsepSession.h
+++ b/media/webrtc/signaling/src/jsep/JsepSession.h
@@ -107,67 +107,39 @@ public:
   // manipulation (which will be unwieldy), or allowing functors to be injected
   // that manipulate the data structure (still pretty unwieldy).
   virtual std::vector<JsepCodecDescription*>& Codecs() = 0;
 
   template <class UnaryFunction>
   void ForEachCodec(UnaryFunction& function)
   {
     std::for_each(Codecs().begin(), Codecs().end(), function);
-    for (RefPtr<JsepTrack>& track : GetLocalTracks()) {
-      track->ForEachCodec(function);
-    }
-    for (RefPtr<JsepTrack>& track : GetRemoteTracks()) {
-      track->ForEachCodec(function);
+    for (auto& transceiver : GetTransceivers()) {
+      transceiver.mSending.ForEachCodec(function);
+      transceiver.mReceiving.ForEachCodec(function);
     }
   }
 
   template <class BinaryPredicate>
   void SortCodecs(BinaryPredicate& sorter)
   {
     std::stable_sort(Codecs().begin(), Codecs().end(), sorter);
-    for (RefPtr<JsepTrack>& track : GetLocalTracks()) {
-      track->SortCodecs(sorter);
-    }
-    for (RefPtr<JsepTrack>& track : GetRemoteTracks()) {
-      track->SortCodecs(sorter);
+    for (auto& transceiver : GetTransceivers()) {
+      transceiver.mSending.SortCodecs(sorter);
+      transceiver.mReceiving.SortCodecs(sorter);
     }
   }
 
-  // Manage tracks. We take shared ownership of any track.
-  virtual nsresult AddTrack(const RefPtr<JsepTrack>& track) = 0;
-  virtual nsresult RemoveTrack(const std::string& streamId,
-                               const std::string& trackId) = 0;
-  virtual nsresult ReplaceTrack(const std::string& oldStreamId,
-                                const std::string& oldTrackId,
-                                const std::string& newStreamId,
-                                const std::string& newTrackId) = 0;
-  virtual nsresult SetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      const std::vector<JsepTrack::JsConstraints>& constraints) = 0;
+  // Helpful for firing events.
+  virtual std::vector<JsepTrack> GetRemoteTracksAdded() const = 0;
+  virtual std::vector<JsepTrack> GetRemoteTracksRemoved() const = 0;
 
-  virtual nsresult GetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      std::vector<JsepTrack::JsConstraints>* outConstraints) = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetLocalTracks() const = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracks() const = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracksAdded() const = 0;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracksRemoved() const = 0;
-
-  // Access the negotiated track pairs.
-  virtual std::vector<JsepTrackPair> GetNegotiatedTrackPairs() const = 0;
-
-  // Access transports.
-  virtual std::vector<RefPtr<JsepTransport>> GetTransports() const = 0;
+  virtual const std::vector<JsepTransceiver>& GetTransceivers() const = 0;
+  virtual std::vector<JsepTransceiver>& GetTransceivers() = 0;
+  virtual nsresult AddTransceiver(const JsepTransceiver& transceiver) = 0;
 
   // Basic JSEP operations.
   virtual nsresult CreateOffer(const JsepOfferOptions& options,
                                std::string* offer) = 0;
   virtual nsresult CreateAnswer(const JsepAnswerOptions& options,
                                 std::string* answer) = 0;
   virtual std::string GetLocalDescription() const = 0;
   virtual std::string GetRemoteDescription() const = 0;
@@ -206,39 +178,38 @@ public:
   {
     static const char* states[] = { "stable", "have-local-offer",
                                     "have-remote-offer", "have-local-pranswer",
                                     "have-remote-pranswer", "closed" };
 
     return states[state];
   }
 
-  virtual bool AllLocalTracksAreAssigned() const = 0;
+  virtual bool CheckNegotiationNeeded() const = 0;
 
   void
   CountTracks(uint16_t (&receiving)[SdpMediaSection::kMediaTypes],
               uint16_t (&sending)[SdpMediaSection::kMediaTypes]) const
   {
-    auto trackPairs = GetNegotiatedTrackPairs();
-
     memset(receiving, 0, sizeof(receiving));
     memset(sending, 0, sizeof(sending));
 
-    for (auto& pair : trackPairs) {
-      if (pair.mReceiving) {
-        receiving[pair.mReceiving->GetMediaType()]++;
+    for (const auto& transceiver : GetTransceivers()) {
+      if (!transceiver.mReceiving.IsNull()) {
+        receiving[transceiver.mReceiving.GetMediaType()]++;
       }
 
-      if (pair.mSending) {
-        sending[pair.mSending->GetMediaType()]++;
+      if (!transceiver.mSending.IsNull()) {
+        sending[transceiver.mSending.GetMediaType()]++;
       }
     }
   }
 
 protected:
+
   const std::string mName;
   JsepSignalingState mState;
   uint32_t mNegotiations;
 };
 
 } // namespace mozilla
 
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.cpp
@@ -16,17 +16,16 @@
 #include "logging.h"
 
 #include "mozilla/Move.h"
 #include "mozilla/UniquePtr.h"
 
 #include "webrtc/config.h"
 
 #include "signaling/src/jsep/JsepTrack.h"
-#include "signaling/src/jsep/JsepTrack.h"
 #include "signaling/src/jsep/JsepTransport.h"
 #include "signaling/src/sdp/Sdp.h"
 #include "signaling/src/sdp/SipccSdp.h"
 #include "signaling/src/sdp/SipccSdpParser.h"
 #include "mozilla/net/DataChannelProtocol.h"
 
 namespace mozilla {
 
@@ -62,125 +61,36 @@ JsepSessionImpl::Init()
   NS_ENSURE_SUCCESS(rv, rv);
 
   SetupDefaultCodecs();
   SetupDefaultRtpExtensions();
 
   return NS_OK;
 }
 
-// Helper function to find the track for a given m= section.
-template <class T>
-typename std::vector<T>::iterator
-FindTrackByLevel(std::vector<T>& tracks, size_t level)
-{
-  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
-    if (t->mAssignedMLine.isSome() &&
-        (*t->mAssignedMLine == level)) {
-      return t;
-    }
-  }
-
-  return tracks.end();
-}
-
-template <class T>
-typename std::vector<T>::iterator
-FindTrackByIds(std::vector<T>& tracks,
-               const std::string& streamId,
-               const std::string& trackId)
-{
-  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
-    if (t->mTrack->GetStreamId() == streamId &&
-        (t->mTrack->GetTrackId() == trackId)) {
-      return t;
-    }
-  }
-
-  return tracks.end();
-}
-
-template <class T>
-typename std::vector<T>::iterator
-FindUnassignedTrackByType(std::vector<T>& tracks,
-                          SdpMediaSection::MediaType type)
-{
-  for (auto t = tracks.begin(); t != tracks.end(); ++t) {
-    if (!t->mAssignedMLine.isSome() &&
-        (t->mTrack->GetMediaType() == type)) {
-      return t;
-    }
-  }
-
-  return tracks.end();
-}
-
 nsresult
-JsepSessionImpl::AddTrack(const RefPtr<JsepTrack>& track)
+JsepSessionImpl::AddTransceiver(const JsepTransceiver& transceiver)
 {
   mLastError.clear();
-  MOZ_ASSERT(track->GetDirection() == sdp::kSend);
-  MOZ_MTLOG(ML_DEBUG, "Adding track.");
-  if (track->GetMediaType() != SdpMediaSection::kApplication) {
-    track->SetCNAME(mCNAME);
-    // Establish minimum number of required SSRCs
-    // Note that AddTrack is only for send direction
-    size_t minimumSsrcCount = 0;
-    std::vector<JsepTrack::JsConstraints> constraints;
-    track->GetJsConstraints(&constraints);
-    for (auto constraint : constraints) {
-      if (constraint.rid != "") {
-        minimumSsrcCount++;
-      }
-    }
-    // We need at least 1 SSRC
-    minimumSsrcCount = std::max<size_t>(1, minimumSsrcCount);
-    size_t currSsrcCount = track->GetSsrcs().size();
-    if (currSsrcCount < minimumSsrcCount ) {
-      MOZ_MTLOG(ML_DEBUG,
-                "Adding " << (minimumSsrcCount - currSsrcCount) << " SSRCs.");
-    }
-    while (track->GetSsrcs().size() < minimumSsrcCount) {
-      uint32_t ssrc=0;
-      nsresult rv = CreateSsrc(&ssrc);
-      NS_ENSURE_SUCCESS(rv, rv);
-      // Don't add duplicate ssrcs
-      std::vector<uint32_t> ssrcs = track->GetSsrcs();
-      if (std::find(ssrcs.begin(), ssrcs.end(), ssrc) == ssrcs.end()) {
-        track->AddSsrc(ssrc);
-      }
-    }
+  JsepTransceiver copy(transceiver);
+  MOZ_MTLOG(ML_DEBUG, "Adding transceiver.");
+
+  if (copy.mSending.GetMediaType() != SdpMediaSection::kApplication) {
+    // Make sure we have an ssrc. Might already be set.
+    copy.mSending.EnsureSsrcs(mSsrcGenerator);
+    copy.mSending.SetCNAME(mCNAME);
+  } else {
+    copy.mJsDirection = SdpDirectionAttribute::Direction::kSendrecv;
   }
 
-  track->PopulateCodecs(mSupportedCodecs.values);
-
-  JsepSendingTrack strack;
-  strack.mTrack = track;
-
-  mLocalTracks.push_back(strack);
-
-  return NS_OK;
-}
+  copy.mSending.PopulateCodecs(mSupportedCodecs.values);
+  copy.mReceiving.PopulateCodecs(mSupportedCodecs.values);
+  // We do not set mLevel yet, we do that either on createOffer, or setRemote
 
-nsresult
-JsepSessionImpl::RemoveTrack(const std::string& streamId,
-                             const std::string& trackId)
-{
-  if (mState != kJsepStateStable) {
-    JSEP_SET_ERROR("Removing tracks outside of stable is unsupported.");
-    return NS_ERROR_UNEXPECTED;
-  }
-
-  auto track = FindTrackByIds(mLocalTracks, streamId, trackId);
-
-  if (track == mLocalTracks.end()) {
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  mLocalTracks.erase(track);
+  mTransceivers.push_back(copy);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetIceCredentials(const std::string& ufrag,
                                    const std::string& pwd)
 {
   mLastError.clear();
@@ -251,389 +161,92 @@ JsepSessionImpl::AddAudioRtpExtension(co
 
 nsresult
 JsepSessionImpl::AddVideoRtpExtension(const std::string& extensionName,
                                       SdpDirectionAttribute::Direction direction)
 {
   return AddRtpExtension(mVideoRtpExtensions, extensionName, direction);
 }
 
-template<class T>
-std::vector<RefPtr<JsepTrack>>
-GetTracks(const std::vector<T>& wrappedTracks)
-{
-  std::vector<RefPtr<JsepTrack>> result;
-  for (auto i = wrappedTracks.begin(); i != wrappedTracks.end(); ++i) {
-    result.push_back(i->mTrack);
-  }
-  return result;
-}
-
-nsresult
-JsepSessionImpl::ReplaceTrack(const std::string& oldStreamId,
-                              const std::string& oldTrackId,
-                              const std::string& newStreamId,
-                              const std::string& newTrackId)
+std::vector<JsepTrack>
+JsepSessionImpl::GetRemoteTracksAdded() const
 {
-  auto it = FindTrackByIds(mLocalTracks, oldStreamId, oldTrackId);
-
-  if (it == mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << oldStreamId << "/" << oldTrackId
-                   << " was never added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  if (FindTrackByIds(mLocalTracks, newStreamId, newTrackId) !=
-      mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << newStreamId << "/" << newTrackId
-                   << " was already added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  it->mTrack->SetStreamId(newStreamId);
-  it->mTrack->SetTrackId(newTrackId);
-
-  return NS_OK;
+  return mRemoteTracksAdded;
 }
 
-nsresult
-JsepSessionImpl::SetParameters(const std::string& streamId,
-                               const std::string& trackId,
-                               const std::vector<JsepTrack::JsConstraints>& constraints)
-{
-  auto it = FindTrackByIds(mLocalTracks, streamId, trackId);
-
-  if (it == mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << streamId << "/" << trackId << " was never added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  // Add RtpStreamId Extmap
-  // SdpDirectionAttribute::Direction is a bitmask
-  SdpDirectionAttribute::Direction addVideoExt = SdpDirectionAttribute::kInactive;
-  SdpDirectionAttribute::Direction addAudioExt = SdpDirectionAttribute::kInactive;
-  for (auto constraintEntry: constraints) {
-    if (constraintEntry.rid != "") {
-      switch (it->mTrack->GetMediaType()) {
-        case SdpMediaSection::kVideo: {
-          addVideoExt = static_cast<SdpDirectionAttribute::Direction>(addVideoExt
-                                                                      | it->mTrack->GetDirection());
-          break;
-        }
-        case SdpMediaSection::kAudio: {
-          addAudioExt = static_cast<SdpDirectionAttribute::Direction>(addAudioExt
-                                                                      | it->mTrack->GetDirection());
-          break;
-        }
-        default: {
-          MOZ_ASSERT(false);
-          return NS_ERROR_INVALID_ARG;
-        }
-      }
-    }
-  }
-  if (addVideoExt != SdpDirectionAttribute::kInactive) {
-    AddVideoRtpExtension("urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", addVideoExt);
-  }
-
-  it->mTrack->SetJsConstraints(constraints);
-
-  auto track = it->mTrack;
-  if (track->GetDirection() == sdp::kSend) {
-    // Establish minimum number of required SSRCs
-    // Note that AddTrack is only for send direction
-    size_t minimumSsrcCount = 0;
-    std::vector<JsepTrack::JsConstraints> constraints;
-    track->GetJsConstraints(&constraints);
-    for (auto constraint : constraints) {
-      if (constraint.rid != "") {
-        minimumSsrcCount++;
-      }
-    }
-    // We need at least 1 SSRC
-    minimumSsrcCount = std::max<size_t>(1, minimumSsrcCount);
-    size_t currSsrcCount = track->GetSsrcs().size();
-    if (currSsrcCount < minimumSsrcCount ) {
-      MOZ_MTLOG(ML_DEBUG,
-                "Adding " << (minimumSsrcCount - currSsrcCount) << " SSRCs.");
-    }
-    while (track->GetSsrcs().size() < minimumSsrcCount) {
-      uint32_t ssrc=0;
-      nsresult rv = CreateSsrc(&ssrc);
-      NS_ENSURE_SUCCESS(rv, rv);
-      // Don't add duplicate ssrcs
-      std::vector<uint32_t> ssrcs = track->GetSsrcs();
-      if (std::find(ssrcs.begin(), ssrcs.end(), ssrc) == ssrcs.end()) {
-        track->AddSsrc(ssrc);
-      }
-    }
-  }
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::GetParameters(const std::string& streamId,
-                               const std::string& trackId,
-                               std::vector<JsepTrack::JsConstraints>* outConstraints)
-{
-  auto it = FindTrackByIds(mLocalTracks, streamId, trackId);
-
-  if (it == mLocalTracks.end()) {
-    JSEP_SET_ERROR("Track " << streamId << "/" << trackId << " was never added.");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  it->mTrack->GetJsConstraints(outConstraints);
-  return NS_OK;
-}
-
-std::vector<RefPtr<JsepTrack>>
-JsepSessionImpl::GetLocalTracks() const
-{
-  return GetTracks(mLocalTracks);
-}
-
-std::vector<RefPtr<JsepTrack>>
-JsepSessionImpl::GetRemoteTracks() const
-{
-  return GetTracks(mRemoteTracks);
-}
-
-std::vector<RefPtr<JsepTrack>>
-JsepSessionImpl::GetRemoteTracksAdded() const
-{
-  return GetTracks(mRemoteTracksAdded);
-}
-
-std::vector<RefPtr<JsepTrack>>
+std::vector<JsepTrack>
 JsepSessionImpl::GetRemoteTracksRemoved() const
 {
-  return GetTracks(mRemoteTracksRemoved);
+  return mRemoteTracksRemoved;
 }
 
 nsresult
-JsepSessionImpl::SetupOfferMSections(const JsepOfferOptions& options, Sdp* sdp)
+JsepSessionImpl::CreateOfferMsection(const JsepOfferOptions& options,
+                                     JsepTransceiver& transceiver,
+                                     Sdp* local)
 {
-  // First audio, then video, then datachannel, for interop
-  // TODO(bug 1121756): We need to group these by stream-id, _then_ by media
-  // type, according to the spec. However, this is not going to interop with
-  // older versions of Firefox if a video-only stream is added before an
-  // audio-only stream.
-  // We should probably wait until 38 is ESR before trying to do this.
-  nsresult rv = SetupOfferMSectionsByType(
-      SdpMediaSection::kAudio, options.mOfferToReceiveAudio, sdp);
-
-  NS_ENSURE_SUCCESS(rv, rv);
+  JsepTrack& sendTrack(transceiver.mSending);
+  JsepTrack& recvTrack(transceiver.mReceiving);
 
-  rv = SetupOfferMSectionsByType(
-      SdpMediaSection::kVideo, options.mOfferToReceiveVideo, sdp);
-
-  NS_ENSURE_SUCCESS(rv, rv);
+  SdpMediaSection::Protocol protocol(
+      mSdpHelper.GetProtocolForMediaType(sendTrack.GetMediaType()));
 
-  if (!(options.mDontOfferDataChannel.isSome() &&
-        *options.mDontOfferDataChannel)) {
-    rv = SetupOfferMSectionsByType(
-        SdpMediaSection::kApplication, Maybe<size_t>(), sdp);
-
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  if (!sdp->GetMediaSectionCount()) {
-    JSEP_SET_ERROR("Cannot create an offer with no local tracks, "
-                   "no offerToReceiveAudio/Video, and no DataChannel.");
-    return NS_ERROR_INVALID_ARG;
+  const Sdp* answer(GetAnswer());
+  if (answer &&
+      (local->GetMediaSectionCount() < answer->GetMediaSectionCount())) {
+    // Use the protocol the answer used, even if it is not what we would have
+    // used.
+    protocol =
+      answer->GetMediaSection(local->GetMediaSectionCount()).GetProtocol();
   }
 
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::SetupOfferMSectionsByType(SdpMediaSection::MediaType mediatype,
-                                           const Maybe<size_t>& offerToReceiveMaybe,
-                                           Sdp* sdp)
-{
-  // Convert the Maybe into a size_t*, since that is more readable, especially
-  // when using it as an in/out param.
-  size_t offerToReceiveCount;
-  size_t* offerToReceiveCountPtr = nullptr;
-
-  if (offerToReceiveMaybe) {
-    offerToReceiveCount = *offerToReceiveMaybe;
-    offerToReceiveCountPtr = &offerToReceiveCount;
-  }
+  SdpMediaSection* msection = &local->AddMediaSection(
+      sendTrack.GetMediaType(),
+      transceiver.mJsDirection,
+      0,
+      protocol,
+      sdp::kIPv4,
+      "0.0.0.0");
 
-  // Make sure every local track has an m-section
-  nsresult rv = BindLocalTracks(mediatype, sdp);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // Make sure that m-sections that previously had a remote track have the
-  // recv bit set. Only matters for renegotiation.
-  rv = BindRemoteTracks(mediatype, sdp, offerToReceiveCountPtr);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // If we need more recv sections, start setting the recv bit on other
-  // msections. If not, disable msections that have no tracks.
-  rv = SetRecvAsNeededOrDisable(mediatype,
-                                sdp,
-                                offerToReceiveCountPtr);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  // If we still don't have enough recv m-sections, add some.
-  if (offerToReceiveCountPtr && *offerToReceiveCountPtr) {
-    rv = AddRecvonlyMsections(mediatype, *offerToReceiveCountPtr, sdp);
-    NS_ENSURE_SUCCESS(rv, rv);
+  if (transceiver.IsStopped()) {
+    mSdpHelper.DisableMsection(local, msection);
+    return NS_OK;
   }
 
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::BindLocalTracks(SdpMediaSection::MediaType mediatype, Sdp* sdp)
-{
-  for (JsepSendingTrack& track : mLocalTracks) {
-    if (mediatype != track.mTrack->GetMediaType()) {
-      continue;
-    }
-
-    SdpMediaSection* msection;
-    if (track.mAssignedMLine.isSome()) {
-      msection = &sdp->GetMediaSection(*track.mAssignedMLine);
-    } else {
-      nsresult rv = GetFreeMsectionForSend(track.mTrack->GetMediaType(),
-                                           sdp,
-                                           &msection);
-      NS_ENSURE_SUCCESS(rv, rv);
-      track.mAssignedMLine = Some(msection->GetLevel());
-    }
-
-    track.mTrack->AddToOffer(msection);
-  }
-  return NS_OK;
-}
+  msection->SetPort(9);
 
-nsresult
-JsepSessionImpl::BindRemoteTracks(SdpMediaSection::MediaType mediatype,
-                                  Sdp* sdp,
-                                  size_t* offerToReceive)
-{
-  for (JsepReceivingTrack& track : mRemoteTracks) {
-    if (mediatype != track.mTrack->GetMediaType()) {
-      continue;
-    }
-
-    if (!track.mAssignedMLine.isSome()) {
-      MOZ_ASSERT(false);
-      continue;
-    }
-
-    auto& msection = sdp->GetMediaSection(*track.mAssignedMLine);
-
-    if (mSdpHelper.MsectionIsDisabled(msection)) {
-      // TODO(bug 1095226) Content probably disabled this? Should we allow
-      // content to do this?
-      continue;
-    }
-
-    track.mTrack->AddToOffer(&msection);
-
-    if (offerToReceive && *offerToReceive) {
-      --(*offerToReceive);
-    }
+  // We don't do this in AddTransportAttributes because that is also used for
+  // making answers, and we don't want to unconditionally set rtcp-mux there.
+  if (mSdpHelper.HasRtcp(msection->GetProtocol())) {
+    // Set RTCP-MUX.
+    msection->GetAttributeList().SetAttribute(
+        new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
   }
 
-  return NS_OK;
-}
+  nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
+  NS_ENSURE_SUCCESS(rv, rv);
 
-nsresult
-JsepSessionImpl::SetRecvAsNeededOrDisable(SdpMediaSection::MediaType mediatype,
-                                          Sdp* sdp,
-                                          size_t* offerToRecv)
-{
-  for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
-    auto& msection = sdp->GetMediaSection(i);
+  sendTrack.AddToOffer(mSsrcGenerator, msection);
+  recvTrack.AddToOffer(mSsrcGenerator, msection);
+
+  AddExtmap(msection);
 
-    if (mSdpHelper.MsectionIsDisabled(msection) ||
-        msection.GetMediaType() != mediatype ||
-        msection.IsReceiving()) {
-      continue;
-    }
-
-    if (offerToRecv) {
-      if (*offerToRecv) {
-        SetupOfferToReceiveMsection(&msection);
-        --(*offerToRecv);
-        continue;
-      }
-    } else if (msection.IsSending()) {
-      SetupOfferToReceiveMsection(&msection);
-      continue;
-    }
-
-    if (!msection.IsSending()) {
-      // Unused m-section, and no reason to offer to recv on it
-      mSdpHelper.DisableMsection(sdp, &msection);
-    }
+  std::string mid;
+  // We do not set the mid on the transceiver, that happens when a description
+  // is set.
+  if (transceiver.IsAssociated()) {
+    mid = transceiver.GetMid();
+  } else {
+    std::ostringstream osMid;
+    osMid << "sdparta_" << msection->GetLevel();
+    mid = osMid.str();
   }
 
-  return NS_OK;
-}
-
-void
-JsepSessionImpl::SetupOfferToReceiveMsection(SdpMediaSection* offer)
-{
-  // Create a dummy recv track, and have it add codecs, set direction, etc.
-  RefPtr<JsepTrack> dummy = new JsepTrack(offer->GetMediaType(),
-                                          "",
-                                          "",
-                                          sdp::kRecv);
-  dummy->PopulateCodecs(mSupportedCodecs.values);
-  dummy->AddToOffer(offer);
-}
-
-nsresult
-JsepSessionImpl::AddRecvonlyMsections(SdpMediaSection::MediaType mediatype,
-                                      size_t count,
-                                      Sdp* sdp)
-{
-  while (count--) {
-    nsresult rv = CreateOfferMSection(
-        mediatype,
-        mSdpHelper.GetProtocolForMediaType(mediatype),
-        SdpDirectionAttribute::kRecvonly,
-        sdp);
-
-    NS_ENSURE_SUCCESS(rv, rv);
-    SetupOfferToReceiveMsection(
-        &sdp->GetMediaSection(sdp->GetMediaSectionCount() - 1));
-  }
-  return NS_OK;
-}
-
-// This function creates a skeleton SDP based on the old descriptions
-// (ie; all m-sections are inactive).
-nsresult
-JsepSessionImpl::AddReofferMsections(const Sdp& oldLocalSdp,
-                                     const Sdp& oldAnswer,
-                                     Sdp* newSdp)
-{
-  nsresult rv;
-
-  for (size_t i = 0; i < oldLocalSdp.GetMediaSectionCount(); ++i) {
-    // We do not set the direction in this function (or disable when previously
-    // disabled), that happens in |SetupOfferMSectionsByType|
-    rv = CreateOfferMSection(oldLocalSdp.GetMediaSection(i).GetMediaType(),
-                             oldLocalSdp.GetMediaSection(i).GetProtocol(),
-                             SdpDirectionAttribute::kInactive,
-                             newSdp);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = mSdpHelper.CopyStickyParams(oldAnswer.GetMediaSection(i),
-                                     &newSdp->GetMediaSection(i));
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
+  msection->GetAttributeList().SetAttribute(
+      new SdpStringAttribute(SdpAttribute::kMidAttribute, mid));
 
   return NS_OK;
 }
 
 void
 JsepSessionImpl::SetupBundle(Sdp* sdp) const
 {
   std::vector<std::string> mids;
@@ -681,92 +294,72 @@ JsepSessionImpl::SetupBundle(Sdp* sdp) c
     groupAttr->PushEntry(SdpGroupAttributeList::kBundle, mids);
     sdp->GetAttributeList().SetAttribute(groupAttr.release());
   }
 }
 
 nsresult
 JsepSessionImpl::GetRemoteIds(const Sdp& sdp,
                               const SdpMediaSection& msection,
-                              std::string* streamId,
+                              std::vector<std::string>* streamIds,
                               std::string* trackId)
 {
-  nsresult rv = mSdpHelper.GetIdsFromMsid(sdp, msection, streamId, trackId);
+  nsresult rv = mSdpHelper.GetIdsFromMsid(sdp, msection, streamIds, trackId);
   if (rv == NS_ERROR_NOT_AVAILABLE) {
-    *streamId = mDefaultRemoteStreamId;
-
-    if (!mDefaultRemoteTrackIdsByLevel.count(msection.GetLevel())) {
-      // Generate random track ids.
-      if (!mUuidGen->Generate(trackId)) {
-        JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
-        return NS_ERROR_FAILURE;
-      }
+    streamIds->push_back(mDefaultRemoteStreamId);
 
-      mDefaultRemoteTrackIdsByLevel[msection.GetLevel()] = *trackId;
-    } else {
-      *trackId = mDefaultRemoteTrackIdsByLevel[msection.GetLevel()];
+    // Generate random track ids.
+    if (!mUuidGen->Generate(trackId)) {
+      JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
+      return NS_ERROR_FAILURE;
     }
+
     return NS_OK;
   }
 
-  if (NS_SUCCEEDED(rv)) {
-    // If, for whatever reason, the other end renegotiates with an msid where
-    // there wasn't one before, don't allow the old default to pop up again
-    // later.
-    mDefaultRemoteTrackIdsByLevel.erase(msection.GetLevel());
-  }
-
   return rv;
 }
 
 nsresult
 JsepSessionImpl::CreateOffer(const JsepOfferOptions& options,
                              std::string* offer)
 {
   mLastError.clear();
 
   if (mState != kJsepStateStable) {
     JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState));
     return NS_ERROR_UNEXPECTED;
   }
 
-  // Undo track assignments from a previous call to CreateOffer
-  // (ie; if the track has not been negotiated yet, it doesn't necessarily need
-  // to stay in the same m-section that it was in)
-  for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    if (!trackWrapper.mTrack->GetNegotiatedDetails()) {
-      trackWrapper.mAssignedMLine.reset();
-    }
-  }
-
   UniquePtr<Sdp> sdp;
 
   // Make the basic SDP that is common to offer/answer.
   nsresult rv = CreateGenericSDP(&sdp);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mCurrentLocalDescription) {
-    rv = AddReofferMsections(*mCurrentLocalDescription,
-                             *GetAnswer(),
-                             sdp.get());
-    NS_ENSURE_SUCCESS(rv, rv);
+  for (size_t level = 0;
+       JsepTransceiver* transceiver = GetTransceiverForLocal(level);
+       ++level) {
+    rv = CreateOfferMsection(options, *transceiver, sdp.get());
   }
 
-  // Ensure that we have all the m-sections we need, and disable extras
-  rv = SetupOfferMSections(options, sdp.get());
-  NS_ENSURE_SUCCESS(rv, rv);
+  if (!sdp->GetMediaSectionCount()) {
+    JSEP_SET_ERROR("Cannot create offer when there are no valid transceivers.");
+    return NS_ERROR_UNEXPECTED;
+  }
 
   SetupBundle(sdp.get());
 
   if (mCurrentLocalDescription) {
     rv = CopyPreviousTransportParams(*GetAnswer(),
                                      *mCurrentLocalDescription,
                                      *sdp,
                                      sdp.get());
     NS_ENSURE_SUCCESS(rv,rv);
+    CopyPreviousMsid(*mCurrentLocalDescription, sdp.get());
   }
 
   *offer = sdp->ToString();
   mGeneratedLocalDescription = Move(sdp);
   ++mSessionVersion;
 
   return NS_OK;
 }
@@ -789,317 +382,173 @@ JsepSessionImpl::GetRemoteDescription() 
   mozilla::Sdp* sdp =  GetParsedRemoteDescription();
   if (sdp) {
     sdp->Serialize(os);
   }
   return os.str();
 }
 
 void
-JsepSessionImpl::AddExtmap(SdpMediaSection* msection) const
+JsepSessionImpl::AddExtmap(SdpMediaSection* msection)
 {
-  const auto* extensions = GetRtpExtensions(msection->GetMediaType());
+  auto extensions = GetRtpExtensions(*msection);
 
-  if (extensions && !extensions->empty()) {
+  if (!extensions.empty()) {
     SdpExtmapAttributeList* extmap = new SdpExtmapAttributeList;
-    extmap->mExtmaps = *extensions;
+    extmap->mExtmaps = extensions;
     msection->GetAttributeList().SetAttribute(extmap);
   }
 }
 
 void
 JsepSessionImpl::AddMid(const std::string& mid,
                         SdpMediaSection* msection) const
 {
   msection->GetAttributeList().SetAttribute(new SdpStringAttribute(
         SdpAttribute::kMidAttribute, mid));
 }
 
-const std::vector<SdpExtmapAttributeList::Extmap>*
-JsepSessionImpl::GetRtpExtensions(SdpMediaSection::MediaType type) const
+std::vector<SdpExtmapAttributeList::Extmap>
+JsepSessionImpl::GetRtpExtensions(const SdpMediaSection& msection)
 {
-  switch (type) {
+  std::vector<SdpExtmapAttributeList::Extmap> result;
+  switch (msection.GetMediaType()) {
     case SdpMediaSection::kAudio:
-      return &mAudioRtpExtensions;
+      result = mAudioRtpExtensions;
+      break;
     case SdpMediaSection::kVideo:
-      return &mVideoRtpExtensions;
+      result = mVideoRtpExtensions;
+      if (msection.GetAttributeList().HasAttribute(
+            SdpAttribute::kRidAttribute)) {
+        // We need RID support
+        // TODO: Would it be worth checking that the direction is sane?
+        AddRtpExtension(result,
+                        "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
+                        SdpDirectionAttribute::kSendonly);
+      }
+      break;
     default:
-      return nullptr;
+      ;
   }
+  return result;
 }
 
 void
 JsepSessionImpl::AddCommonExtmaps(const SdpMediaSection& remoteMsection,
                                   SdpMediaSection* msection)
 {
-  auto* ourExtensions = GetRtpExtensions(remoteMsection.GetMediaType());
-
-  if (ourExtensions) {
-    mSdpHelper.AddCommonExtmaps(remoteMsection, *ourExtensions, msection);
-  }
+  mSdpHelper.AddCommonExtmaps(
+      remoteMsection, GetRtpExtensions(*msection), msection);
 }
 
 nsresult
 JsepSessionImpl::CreateAnswer(const JsepAnswerOptions& options,
                               std::string* answer)
 {
   mLastError.clear();
 
   if (mState != kJsepStateHaveRemoteOffer) {
     JSEP_SET_ERROR("Cannot create answer in state " << GetStateStr(mState));
     return NS_ERROR_UNEXPECTED;
   }
 
-  // This is the heart of the negotiation code. Depressing that it's
-  // so bad.
-  //
-  // Here's the current algorithm:
-  // 1. Walk through all the m-lines on the other side.
-  // 2. For each m-line, walk through all of our local tracks
-  //    in sequence and see if any are unassigned. If so, assign
-  //    them and mark it sendrecv, otherwise it's recvonly.
-  // 3. Just replicate their media attributes.
-  // 4. Profit.
   UniquePtr<Sdp> sdp;
 
   // Make the basic SDP that is common to offer/answer.
   nsresult rv = CreateGenericSDP(&sdp);
   NS_ENSURE_SUCCESS(rv, rv);
 
   const Sdp& offer = *mPendingRemoteDescription;
 
   // Copy the bundle groups into our answer
   UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
   mSdpHelper.GetBundleGroups(offer, &groupAttr->mGroups);
   sdp->GetAttributeList().SetAttribute(groupAttr.release());
 
-  // Disable send for local tracks if the offer no longer allows it
-  // (i.e., the m-section is recvonly, inactive or disabled)
-  for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    if (!trackWrapper.mAssignedMLine.isSome()) {
-      continue;
-    }
-
-    // Get rid of all m-line assignments that have not been negotiated
-    if (!trackWrapper.mTrack->GetNegotiatedDetails()) {
-      trackWrapper.mAssignedMLine.reset();
-      continue;
+  for (size_t i = 0; i < offer.GetMediaSectionCount(); ++i) {
+    // The transceivers are already in place, due to setRemote
+    JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+    if (!transceiver) {
+      JSEP_SET_ERROR("No transceiver for level " << i);
+      MOZ_ASSERT(false);
+      return NS_ERROR_FAILURE;
     }
-
-    if (!offer.GetMediaSection(*trackWrapper.mAssignedMLine).IsReceiving()) {
-      trackWrapper.mAssignedMLine.reset();
-    }
-  }
-
-  size_t numMsections = offer.GetMediaSectionCount();
-
-  for (size_t i = 0; i < numMsections; ++i) {
-    const SdpMediaSection& remoteMsection = offer.GetMediaSection(i);
-    rv = CreateAnswerMSection(options, i, remoteMsection, sdp.get());
+    rv = CreateAnswerMsection(options,
+                              *transceiver,
+                              offer.GetMediaSection(i),
+                              sdp.get());
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   if (mCurrentLocalDescription) {
     // per discussion with bwc, 3rd parm here should be offer, not *sdp. (mjf)
     rv = CopyPreviousTransportParams(*GetAnswer(),
                                      *mCurrentRemoteDescription,
                                      offer,
                                      sdp.get());
     NS_ENSURE_SUCCESS(rv,rv);
+    CopyPreviousMsid(*mCurrentLocalDescription, sdp.get());
   }
 
   *answer = sdp->ToString();
   mGeneratedLocalDescription = Move(sdp);
   ++mSessionVersion;
 
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::CreateOfferMSection(SdpMediaSection::MediaType mediatype,
-                                     SdpMediaSection::Protocol proto,
-                                     SdpDirectionAttribute::Direction dir,
-                                     Sdp* sdp)
-{
-  SdpMediaSection* msection =
-      &sdp->AddMediaSection(mediatype, dir, 0, proto, sdp::kIPv4, "0.0.0.0");
-
-  return EnableOfferMsection(msection);
-}
-
-nsresult
-JsepSessionImpl::GetFreeMsectionForSend(
-    SdpMediaSection::MediaType type,
-    Sdp* sdp,
-    SdpMediaSection** msectionOut)
-{
-  for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
-    SdpMediaSection& msection = sdp->GetMediaSection(i);
-    // draft-ietf-rtcweb-jsep-08 says we should reclaim disabled m-sections
-    // regardless of media type. This breaks some pretty fundamental rules of
-    // SDP offer/answer, so we probably should not do it.
-    if (msection.GetMediaType() != type) {
-      continue;
-    }
-
-    if (FindTrackByLevel(mLocalTracks, i) != mLocalTracks.end()) {
-      // Not free
-      continue;
-    }
-
-    if (mSdpHelper.MsectionIsDisabled(msection)) {
-      // Was disabled; revive
-      nsresult rv = EnableOfferMsection(&msection);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-
-    *msectionOut = &msection;
-    return NS_OK;
-  }
-
-  // Ok, no pre-existing m-section. Make a new one.
-  nsresult rv = CreateOfferMSection(type,
-                                    mSdpHelper.GetProtocolForMediaType(type),
-                                    SdpDirectionAttribute::kInactive,
-                                    sdp);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  *msectionOut = &sdp->GetMediaSection(sdp->GetMediaSectionCount() - 1);
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::CreateAnswerMSection(const JsepAnswerOptions& options,
-                                      size_t mlineIndex,
+JsepSessionImpl::CreateAnswerMsection(const JsepAnswerOptions& options,
+                                      JsepTransceiver& transceiver,
                                       const SdpMediaSection& remoteMsection,
                                       Sdp* sdp)
 {
+  SdpDirectionAttribute::Direction direction =
+    reverse(remoteMsection.GetDirection()) & transceiver.mJsDirection;
   SdpMediaSection& msection =
       sdp->AddMediaSection(remoteMsection.GetMediaType(),
-                           SdpDirectionAttribute::kInactive,
+                           direction,
                            9,
                            remoteMsection.GetProtocol(),
                            sdp::kIPv4,
                            "0.0.0.0");
 
   nsresult rv = mSdpHelper.CopyStickyParams(remoteMsection, &msection);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mSdpHelper.MsectionIsDisabled(remoteMsection)) {
+  if (mSdpHelper.MsectionIsDisabled(remoteMsection) ||
+      // JS might have stopped this
+      transceiver.IsStopped()) {
     mSdpHelper.DisableMsection(sdp, &msection);
     return NS_OK;
   }
 
   SdpSetupAttribute::Role role;
   rv = DetermineAnswererSetupRole(remoteMsection, &role);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = AddTransportAttributes(&msection, role);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  rv = SetRecvonlySsrc(&msection);
-  NS_ENSURE_SUCCESS(rv, rv);
+  transceiver.mSending.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
+  transceiver.mReceiving.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
 
-  // Only attempt to match up local tracks if the offerer has elected to
-  // receive traffic.
-  if (remoteMsection.IsReceiving()) {
-    rv = BindMatchingLocalTrackToAnswer(&msection);
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
-
-  if (remoteMsection.IsSending()) {
-    BindMatchingRemoteTrackToAnswer(&msection);
-  }
-
-  // Add extmap attributes.
+  // Add extmap attributes. This logic will probably be moved to the track,
+  // since it can be specified on a per-sender basis in JS.
   AddCommonExtmaps(remoteMsection, &msection);
 
   if (msection.GetFormats().empty()) {
     // Could not negotiate anything. Disable m-section.
     mSdpHelper.DisableMsection(sdp, &msection);
   }
 
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::SetRecvonlySsrc(SdpMediaSection* msection)
-{
-  if (msection->GetMediaType() == SdpMediaSection::kApplication) {
-    return NS_OK;
-  }
-
-  // If previous m-sections are disabled, we do not call this function for them
-  while (mRecvonlySsrcs.size() <= msection->GetLevel()) {
-    uint32_t ssrc;
-    nsresult rv = CreateSsrc(&ssrc);
-    NS_ENSURE_SUCCESS(rv, rv);
-    mRecvonlySsrcs.push_back(ssrc);
-  }
-
-  std::vector<uint32_t> ssrcs;
-  ssrcs.push_back(mRecvonlySsrcs[msection->GetLevel()]);
-  msection->SetSsrcs(ssrcs, mCNAME);
-  return NS_OK;
-}
-