Bug 1290948: WIP Transceivers draft
authorByron Campen [:bwc] <docfaraday@gmail.com>
Mon, 17 Jul 2017 18:00:29 -0500
changeset 651508 c1a2972f5f6c00a6481821accea84e8ed980aca8
parent 651395 c86b7150523c10e1d1dbc0be2d8ed96f205be35f
child 727738 034a50d3f3d463b47dde6b5686a66313b79beba6
push id75755
push userbcampen@mozilla.com
push dateWed, 23 Aug 2017 20:36:00 +0000
bugs1290948
milestone57.0a1
Bug 1290948: WIP Transceivers MozReview-Commit-ID: 7K42D3bEa3Z
dom/bindings/Bindings.conf
dom/media/PeerConnection.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/templates.js
dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_bug1064223.html
dom/media/tests/mochitest/test_peerConnection_constructedStream.html
dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html
dom/media/tests/mochitest/test_peerConnection_localRollback.html
dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrack.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddAudioTrackNoBundle.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrack.html
dom/media/tests/mochitest/test_peerConnection_removeThenAddVideoTrackNoBundle.html
dom/media/tests/mochitest/test_peerConnection_removeVideoTrack.html
dom/media/tests/mochitest/test_peerConnection_replaceTrack.html
dom/media/tests/mochitest/test_peerConnection_replaceVideoThenRenegotiate.html
dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
dom/media/tests/mochitest/test_peerConnection_setParameters.html
dom/media/tests/mochitest/test_peerConnection_transceivers.html
dom/media/tests/mochitest/test_peerConnection_twoAudioTracksInOneStream.html
dom/media/tests/mochitest/test_peerConnection_twoVideoTracksInOneStream.html
dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
dom/webidl/MediaStream.webidl
dom/webidl/MediaStreamList.webidl
dom/webidl/PeerConnectionImpl.webidl
dom/webidl/PeerConnectionObserver.webidl
dom/webidl/RTCPeerConnection.webidl
dom/webidl/RTCRtpReceiver.webidl
dom/webidl/RTCRtpSender.webidl
dom/webidl/RTCRtpTransceiver.webidl
dom/webidl/RTCTrackEvent.webidl
dom/webidl/TransceiverImpl.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/JsepTransport.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/RemoteTrackSource.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/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -683,16 +683,22 @@ DOMInterfaces = {
 },
 
 'PeerConnectionImpl': {
     'nativeType': 'mozilla::PeerConnectionImpl',
     'headerFile': 'PeerConnectionImpl.h',
     'wrapperCache': False
 },
 
+'TransceiverImpl': {
+    'nativeType': 'mozilla::TransceiverImpl',
+    'headerFile': 'TransceiverImpl.h',
+    'wrapperCache': False
+},
+
 'Plugin': {
     'headerFile' : 'nsPluginArray.h',
     'nativeType': 'nsPluginElement',
 },
 
 'PluginArray': {
     'nativeType': 'nsPluginArray',
 },
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -20,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);
@@ -143,19 +145,17 @@ class GlobalPCList {
       this.handleGMPCrash(data);
     }
   }
 
   observe(subject, topic, data) {
     let cleanupPcRef = function(pcref) {
       let pc = pcref.get();
       if (pc) {
-        pc._pc.close();
-        delete pc._observer;
-        pc._pc = null;
+        pc.close();
       }
     };
 
     let cleanupWinId = function(list, winID) {
       if (list.hasOwnProperty(winID)) {
         list[winID].forEach(cleanupPcRef);
         delete list[winID];
       }
@@ -338,18 +338,18 @@ setupPrototype(RTCStatsReport, {
         "candidate-pair": "candidatepair",
         "local-candidate": "localcandidate",
         "remote-candidate": "remotecandidate"
   }
 });
 
 class RTCPeerConnection {
   constructor() {
-    this._senders = [];
-    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
@@ -580,16 +580,29 @@ class RTCPeerConnection {
 
     try {
       wrapCallback(onSucc)(await func());
     } catch (e) {
       wrapCallback(onErr)(e);
     }
   }
 
+  // This implements the fairly common "Queue a task" logic
+  async _queueTaskWithClosedCheck(func) {
+    let pc = this;
+    return new this._win.Promise(resolve => {
+      Services.tm.dispatchToMainThread({ run() {
+        if (!pc._closed) {
+          func();
+          resolve();
+        }
+      }});
+    });
+  }
+
   /**
    * An RTCConfiguration may look like this:
    *
    * { "iceServers": [ { urls: "stun:stun.example.org", },
    *                   { url: "stun:stun.example.org", }, // deprecated version
    *                   { urls: ["turn:turn1.x.org", "turn:turn2.x.org"],
    *                     username:"jib", credential:"mypass"} ] }
    *
@@ -673,16 +686,25 @@ 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._transceivers.find(t => t.sender == sender);
+    if (!transceiver) {
+      throw new this._win.DOMException("This isn't one of my senders!",
+                                       "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);
     }
   }
 
@@ -743,23 +765,87 @@ class RTCPeerConnection {
                             set(h) {
                               this.logWarning(name + " is deprecated! " + msg);
                               return this.setEH(name, h);
                             }
                           });
   }
 
   createOffer(optionsOrOnSucc, onErr, options) {
+    // Spec language implies that this needs to happen as if it were called
+    // before createOffer, so we do this as early as possible.
+    this._ensureTransceiversForOfferToReceive(optionsOrOnSucc);
+
     // 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));
     }
     return this._async(() => this._createOffer(optionsOrOnSucc));
   }
 
+  _enableReceive(transceiver) {
+    if (transceiver.direction == "sendonly") {
+      transceiver.setDirection("sendrecv");
+      return true;
+    } else if (transceiver.direction == "inactive") {
+      transceiver.setDirection("recvonly");
+      return true;
+    }
+    return false;
+  }
+
+  _enableSend(transceiver) {
+    if (transceiver.direction == "recvonly") {
+      transceiver.setDirection("sendrecv");
+      return true;
+    } else if (transceiver.direction == "inactive") {
+      transceiver.setDirection("sendonly");
+      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 this._chain(async () => {
       let haveAssertion;
       if (this._localIdp.enabled) {
         haveAssertion = this._getIdentityAssertion(origin);
       }
@@ -1051,128 +1137,238 @@ 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("This track is already set on a sender.",
+                                       "InvalidAccessError");
+    }
+
+    let transceiver = this._transceivers.find(transceiver => {
+      return transceiver.sender.track === null &&
+             transceiver.getKind() === track.kind &&
+             !transceiver.stopped &&
+             !transceiver.hasBeenUsedToSend();
     });
-    this._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];
+      this._enableSend(transceiver);
+    } else {
+      transceiver = this._addTransceiverNoEvents(
+          track,
+          {
+            streams: [stream],
+            direction: "sendrecv"
+          });
+    }
+
+    transceiver.setAddTrackMagic();
+    transceiver.sync();
+    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
+
+    sender.checkWasCreatedByPc(this.__DOM_IMPL__);
+
+    // If the transceiver was removed due to rollback, let it slide.
+    let transceiver;
+    try {
+      transceiver = this._getTransceiverWithSender(sender);
+    } catch (e) {
+      return;
+    }
+
+    // TODO(bug XXXXX): Handle in TransceiverImpl::SyncWithJS?
+    this._impl.removeTrack(sender.track);
+
+    sender.setTrack(null);
+    if (this._disableSend(transceiver)) {
+      transceiver.sync();
+      this.updateNegotiationNeeded();
     }
   }
 
-  _insertDTMF(sender, tones, duration, interToneGap) {
-    return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
+  mozGetWebrtcTrackId(track) {
+    let matchingTransceiver = this._transceivers.find(
+        transceiver => transceiver.receiver.track == track);
+    if (!matchingTransceiver) {
+      return null;
+    }
+
+    return matchingTransceiver.receiver.webrtcTrackId;
+  }
+
+  _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.TypeError("Invalid media kind");
+      }
+    } else {
+      sendTrack = sendTrackOrKind;
+      kind = sendTrack.kind;
+    }
+
+    let transceiverImpl = this._impl.createTransceiverImpl(kind, sendTrack);
+    let transceiver = this._win.RTCRtpTransceiver._create(
+        this._win,
+        new RTCRtpTransceiver(this, transceiverImpl, init, kind, sendTrack));
+    transceiver.sync();
+    this._transceivers.push(transceiver);
+    return transceiver;
+  }
+
+  _onTransceiverNeeded(kind, transceiverImpl) {
+    let init = {direction: "recvonly"};
+    let transceiver = this._win.RTCRtpTransceiver._create(
+        this._win,
+        new RTCRtpTransceiver(this, transceiverImpl, init, kind, null));
+    // We don't sync this transceiver here; we wait until SRD finishes
+    this._transceivers.push(transceiver);
+  }
+
+  addTransceiver(sendTrackOrKind, init) {
+    let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init);
+    this.updateNegotiationNeeded();
+    return transceiver;
+  }
+
+  _syncTransceivers() {
+    this._transceivers.forEach(transceiver => transceiver.sync());
+  }
+
+  clearNegotiationNeeded() {
+    this._negotiationNeeded = false;
+  }
+
+  updateNegotiationNeeded() {
+    this._checkClosed();
+    if (this.signalingState != "stable") {
+      return;
+    }
+
+    let negotiationNeeded = this._impl.checkNegotiationNeeded();
+    if (!negotiationNeeded) {
+      this.clearNegotiationNeeded();
+      return;
+    }
+
+    if (this._negotiationNeeded) {
+      return;
+    }
+
+    this._negotiationNeeded = true;
+
+    this._queueTaskWithClosedCheck(() => {
+      if (this._negotiationNeeded) {
+        this.dispatchEvent(new this._win.Event("negotiationneeded"));
+      }
+    });
+  }
+
+  _getOrCreateStream(id) {
+    if (!this._receiveStreams.has(id)) {
+      let stream = new this._win.MediaStream();
+      stream.assignId(id);
+      // Legacy event, remove eventually
+      let ev = new this._win.MediaStreamEvent("addstream", { stream });
+      this.dispatchEvent(ev);
+      this._receiveStreams.set(id, stream);
+    }
+
+    return this._receiveStreams.get(id);
+  }
+
+  _insertDTMF(transceiverImpl, tones, duration, interToneGap) {
+    return this._impl.insertDTMF(transceiverImpl, tones, duration, interToneGap);
   }
 
   _getDTMFToneBuffer(sender) {
     return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
   }
 
-  async _replaceTrack(sender, withTrack) {
+  async _replaceTrack(transceiverImpl, withTrack) {
     this._checkClosed();
-    return this._chain(() => new Promise((resolve, reject) => {
-      this._onReplaceTrackSender = sender;
-      this._onReplaceTrackWithTrack = withTrack;
+
+    return new Promise((resolve, reject) => {
       this._onReplaceTrackSuccess = resolve;
       this._onReplaceTrackFailure = reject;
-      this._impl.replaceTrack(sender.track, withTrack);
-    }));
-  }
-
-  _setParameters({ track }, parameters) {
-    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
-      return;
-    }
-    // validate parameters input
-    var encodings = parameters.encodings || [];
-
-    encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
-      if (scaleResolutionDownBy < 1.0) {
-        throw new this._win.RangeError("scaleResolutionDownBy must be >= 1.0");
-      }
-      if (!rid && encodings.length > 1) {
-        throw new this._win.DOMException("Missing rid", "TypeError");
-      }
-      if (uniqueRids[rid]) {
-        throw new this._win.DOMException("Duplicate rid", "TypeError");
-      }
-      uniqueRids[rid] = true;
-      return uniqueRids;
-    }, {});
-
-    this._impl.setParameters(track, parameters);
-  }
-
-  _getParameters({ track }) {
-    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
-      return null;
-    }
-    return this._impl.getParameters(track);
+      this._impl.replaceTrackNoRenegotiation(transceiverImpl, withTrack);
+    });
   }
 
   close() {
     if (this._closed) {
       return;
     }
     this._closed = true;
     this._inClose = true;
     this.changeIceConnectionState("closed");
     this._localIdp.close();
     this._remoteIdp.close();
     this._impl.close();
     this._inClose = false;
+    delete this._pc;
+    delete this._observer;
   }
 
   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.getTransceivers().map(transceiver => transceiver.sender);
   }
 
   getReceivers() {
-    return this._receivers;
+    return this.getTransceivers().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 });
   }
@@ -1300,19 +1496,27 @@ 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);
+
+    // Spec says to do this in the background. Close enough.
+    this._queueTaskWithClosedCheck(() => {
+      this.updateNegotiationNeeded();
+    });
+
+    return dataChannel;
   }
 }
 setupPrototype(RTCPeerConnection, {
   classID: PC_CID,
   contractID: PC_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer]),
   _actions: {
@@ -1368,20 +1572,26 @@ class PeerConnectionObserver {
     this._dompc._onCreateAnswerSuccess(sdp);
   }
 
   onCreateAnswerError(code, message) {
     this._dompc._onCreateAnswerFailure(this.newError(message, code));
   }
 
   onSetLocalDescriptionSuccess() {
+    this._dompc._syncTransceivers();
+    this._dompc.clearNegotiationNeeded();
+    this._dompc.updateNegotiationNeeded();
     this._dompc._onSetLocalDescriptionSuccess();
   }
 
   onSetRemoteDescriptionSuccess() {
+    this._dompc._syncTransceivers();
+    this._dompc.clearNegotiationNeeded();
+    this._dompc.updateNegotiationNeeded();
     this._dompc._onSetRemoteDescriptionSuccess();
   }
 
   onSetLocalDescriptionError(code, message) {
     this._localType = null;
     this._dompc._onSetLocalDescriptionFailure(this.newError(message, code));
   }
 
@@ -1408,20 +1618,16 @@ class PeerConnectionObserver {
     } else {
       candidate = null;
 
     }
     this.dispatchEvent(new win.RTCPeerConnectionIceEvent("icecandidate",
                                                          { candidate }));
   }
 
-  onNegotiationNeeded() {
-    this.dispatchEvent(new this._win.Event("negotiationneeded"));
-  }
-
   // This method is primarily responsible for updating iceConnectionState.
   // This state is defined in the WebRTC specification as follows:
   //
   // iceConnectionState:
   // -------------------
   //   new           Any of the RTCIceTransports are in the new state and none
   //                 of them are in the checking, failed or disconnected state.
   //
@@ -1543,71 +1749,97 @@ 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) {
+  _getTransceiverWithRecvTrack(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._getTransceiverWithRecvTrack(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. The mediacapture spec says
+          // this needs to be queued, also.
+          pc._queueTaskWithClosedCheck(() => {
+            stream.dispatchEvent(
+                new pc._win.MediaStreamTrackEvent(
+                  "addtrack", { track: matchingTransceiver.receiver.track }));
+          });
+        });
+
+
+    let ev = new pc._win.RTCTrackEvent("track", {
+      receiver: matchingTransceiver.receiver,
+      track: matchingTransceiver.receiver.track,
+      streams,
+      transceiver: matchingTransceiver });
+    // TODO(bug XXXXX): Queue a task for this, and fix the tests, unless the
+    // spec changes.
     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 });
+    // TODO(bug XXXXX): Queue a task for this, and fix the tests, unless the
+    // spec changes.
     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, transceiverImpl) {
+    this._dompc._onTransceiverNeeded(kind, transceiverImpl);
   }
 
   onReplaceTrackSuccess() {
     var pc = this._dompc;
-    pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack;
-    pc._onReplaceTrackWithTrack = null;
-    pc._onReplaceTrackSender = null;
     pc._onReplaceTrackSuccess();
   }
 
   onReplaceTrackError(code, message) {
     var pc = this._dompc;
-    pc._onReplaceTrackWithTrack = null;
-    pc._onReplaceTrackSender = null;
     pc._onReplaceTrackFailure(this.newError(message, code));
   }
 
   notifyDataChannel(channel) {
     this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
                                                                 { channel }));
   }
 
-  onDTMFToneChange(trackId, tone) {
+  onDTMFToneChange(track, tone) {
     var pc = this._dompc;
-    var sender = pc._senders.find(({track}) => track.id == trackId);
+    var sender = pc.getSenders().find(sender => sender.track == track);
     sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
                                                                  { tone }));
   }
 }
 setupPrototype(PeerConnectionObserver, {
   classID: PC_OBS_CID,
   contractID: PC_OBS_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
@@ -1646,86 +1878,326 @@ 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",
-                                                   "InvalidStateError");
-    }
-
-    duration = Math.max(40, Math.min(duration, 6000));
-    if (interToneGap < 30) interToneGap = 30;
+    let transceiver =
+      this._sender._pc._getTransceiverWithSender(this._sender.__DOM_IMPL__);
 
-    tones = tones.toUpperCase();
-
-    if (tones.match(/[^0-9A-D#*,]/)) {
-      throw new this._sender._pc._win.DOMException("Invalid DTMF characters",
-                                                   "InvalidCharacterError");
-    }
-
-    this._sender._pc._insertDTMF(this._sender, tones, duration, interToneGap);
+    transceiver.insertDTMF(tones, duration, interToneGap);
   }
 }
 setupPrototype(RTCDTMFSender, {
   classID: PC_DTMF_SENDER_CID,
   contractID: PC_DTMF_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpSender {
-  constructor(pc, track, stream) {
-    let dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
-    Object.assign(this, { _pc: pc, track, _stream: stream, dtmf });
+  constructor(pc, transceiverImpl, track, streams) {
+    let dtmf = pc._win.RTCDTMFSender._create(
+        pc._win, new RTCDTMFSender(this));
+
+    Object.assign(this, {
+      _pc: pc,
+      _transceiverImpl: transceiverImpl,
+      track,
+      _streams: streams,
+      dtmf });
   }
 
   replaceTrack(withTrack) {
-    return this._pc._async(() => this._pc._replaceTrack(this, withTrack));
+    // async functions in here return a chrome promise, which is not something
+    // content can use. This wraps that promise in something content can use.
+    return this._pc._win.Promise.resolve(this._replaceTrack(withTrack));
+  }
+
+  async _replaceTrack(withTrack) {
+    this._pc._checkClosed();
+
+    let transceiver = this._pc._getTransceiverWithSender(this.__DOM_IMPL__);
+
+    if (transceiver.stopped) {
+      throw new this._pc._win.DOMException(
+          "Cannot call replaceTrack when transceiver is stopped",
+          "InvalidStateError");
+    }
+
+    if (withTrack && (withTrack.kind != transceiver.getKind())) {
+      throw new this._pc._win.DOMException(
+          "Cannot replaceTrack with a different kind!",
+          "TypeError");
+    }
+
+    // TODO (bug XXXXX): Technically, if the transceiver is not yet associated,
+    // we're supposed to synchronously set the track and return a resolved
+    // promise. However, PeerConnectionImpl::ReplaceTrackNoNegotiation does
+    // stuff like updating principal change observers. We might want to set
+    // that stuff up later than CreateTransceiverInternal so we don't need to
+    // do this for tracks that haven't sent anything yet.
+
+    await this._pc._replaceTrack(this._transceiverImpl, withTrack);
+
+    // We're supposed to queue a task to do these next steps.
+    await this._pc._queueTaskWithClosedCheck(() => {
+      this.track = withTrack;
+      transceiver.sync();
+    });
   }
 
   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(this._setParameters(copy));
+  }
+
+  async _setParameters(parameters) {
+    this._pc._checkClosed();
+
+    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
+      return;
+    }
+
+    parameters.encodings = parameters.encodings || [];
+
+    parameters.encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
+      if (scaleResolutionDownBy < 1.0) {
+        throw new this._pc._win.RangeError("scaleResolutionDownBy must be >= 1.0");
+      }
+      if (!rid && parameters.encodings.length > 1) {
+        throw new this._pc._win.DOMException("Missing rid", "TypeError");
+      }
+      if (uniqueRids[rid]) {
+        throw new this._pc._win.DOMException("Duplicate rid", "TypeError");
+      }
+      uniqueRids[rid] = true;
+      return uniqueRids;
+    }, {});
+
+    let transceiver = this._pc._getTransceiverWithSender(this.__DOM_IMPL__);
+
+    if (transceiver.stopped) {
+      throw new this._pc._win.DOMException(
+          "This sender's transceiver is stopped", "InvalidStateError");
+    }
+
+    // TODO: transaction ids
+
+    // Spec says this stuff needs to be done async. May change.
+    await this._pc._queueTaskWithClosedCheck(() => {
+      this.parameters = parameters;
+      transceiver.sync();
+    });
   }
 
   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));
   }
+
+  checkWasCreatedByPc(pc) {
+    if (pc != this._pc.__DOM_IMPL__) {
+      throw new this._pc._win.DOMException(
+          "This sender was not created by this PeerConnection",
+          "InvalidAccessError");
+    }
+  }
 }
 setupPrototype(RTCRtpSender, {
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpReceiver {
-  constructor(pc, track) {
-    Object.assign(this, { _pc: pc, track });
+  constructor(pc, transceiverImpl) {
+    // We do not set the track here; that is done when _transceiverImpl is set
+    Object.assign(this,
+        {
+          _pc: pc,
+          _transceiverImpl: transceiverImpl,
+          track: transceiverImpl.getReceiveTrack(),
+          webrtcTrackId: ""
+        });
   }
 
+  // TODO(bug XXXXX): Create a getStats binding on TransceiverImpl, and use
+  // that here.
   getStats() {
     return this._pc._async(
       async () => this._pc.getStats(this.track));
   }
 }
 setupPrototype(RTCRtpReceiver, {
   classID: PC_RECEIVER_CID,
   contractID: PC_RECEIVER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
+class RTCRtpTransceiver {
+  constructor(pc, transceiverImpl, init, kind, sendTrack) {
+    let receiver = pc._win.RTCRtpReceiver._create(
+        pc._win, new RTCRtpReceiver(pc, transceiverImpl, kind));
+    let streams = (init && init.streams) || [];
+    let sender = pc._win.RTCRtpSender._create(
+        pc._win, new RTCRtpSender(pc, transceiverImpl, sendTrack, streams));
+
+    let direction = (init && init.direction) || "sendrecv";
+    Object.assign(this,
+        {
+          _pc: pc,
+          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,
+          _transceiverImpl: transceiverImpl
+        });
+  }
+
+  setDirection(direction) {
+    if (this.direction == direction) {
+      return;
+    }
+
+    this.direction = direction;
+    this.sync();
+    this._pc.updateNegotiationNeeded();
+  }
+
+  stop() {
+    if (this.stopped) {
+      return;
+    }
+
+    this.setStopped();
+    this.sync();
+    this._pc.updateNegotiationNeeded();
+  }
+
+  setStopped() {
+    this.stopped = true;
+    this.currentDirection = null;
+  }
+
+  remove() {
+    var index = this._pc._transceivers.indexOf(this.__DOM_IMPL__);
+    if (index != -1) {
+      this._pc._transceivers.splice(index, 1);
+    }
+  }
+
+  getKind() {
+    return this._kind;
+  }
+
+  hasBeenUsedToSend() {
+    return this._hasBeenUsedToSend;
+  }
+
+  setAddTrackMagic() {
+    this.addTrackMagic = true;
+  }
+
+  sync() {
+    if (this._syncing) {
+      throw new this._pc._win.DOMException(
+          "Reentrant sync! This is a bug!",
+          "InternalError");
+    }
+    this._syncing = true;
+    this._transceiverImpl.syncWithJS(this.__DOM_IMPL__);
+    this._syncing = false;
+  }
+
+  // Used by _transceiverImpl.syncWithJS, don't call sync again!
+  setCurrentDirection(direction) {
+    if (this.stopped) {
+      return;
+    }
+
+    switch (direction) {
+      case "sendrecv":
+      case "sendonly":
+        this._hasBeenUsedToSend = true;
+        break;
+      default:
+    }
+
+    this.currentDirection = direction;
+  }
+
+  // Used by _transceiverImpl.syncWithJS, don't call sync again!
+  setMid(mid) {
+    this.mid = mid;
+  }
+
+  // Used by _transceiverImpl.syncWithJS, don't call sync again!
+  unsetMid() {
+    this.mid = null;
+  }
+
+  insertDTMF(tones, duration, interToneGap) {
+    if (this.stopped) {
+      throw new this._pc._win.DOMException("Transceiver is stopped!",
+                                           "InvalidStateError");
+    }
+
+    if (!this.sender.track) {
+      throw new this._pc._win.DOMException("RTCRtpSender has no track",
+                                           "InvalidStateError");
+    }
+
+    duration = Math.max(40, Math.min(duration, 6000));
+    if (interToneGap < 30) interToneGap = 30;
+
+    tones = tones.toUpperCase();
+
+    if (tones.match(/[^0-9A-D#*,]/)) {
+      throw new this._pc._win.DOMException("Invalid DTMF characters",
+                                           "InvalidCharacterError");
+    }
+
+    // TODO (bug XXXXX): Move this API to TransceiverImpl so we don't need the
+    // extra hops through RTCPeerConnection and PeerConnectionImpl
+    this._pc._insertDTMF(this._transceiverImpl, tones, duration, interToneGap);
+  }
+}
+
+setupPrototype(RTCRtpTransceiver, {
+  classID: PC_TRANSCEIVER_CID,
+  contractID: PC_TRANSCEIVER_CONTRACT,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
+});
+
 class CreateOfferRequest {
   constructor(windowID, innerWindowID, callID, isSecure) {
     Object.assign(this, { windowID, innerWindowID, callID, isSecure });
   }
 }
 setupPrototype(CreateOfferRequest, {
   classID: PC_COREQUEST_CID,
   contractID: PC_COREQUEST_CONTRACT,
@@ -1736,12 +2208,13 @@ this.NSGetFactory = XPCOMUtils.generateN
   [GlobalPCList,
    RTCDTMFSender,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCPeerConnectionStatic,
    RTCRtpReceiver,
    RTCRtpSender,
+   RTCRtpTransceiver,
    RTCStatsReport,
    PeerConnectionObserver,
    CreateOfferRequest]
 );
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -87,16 +87,17 @@ skip-if = toolkit == 'android' # no scre
 [test_getUserMedia_stopVideoStream.html]
 [test_getUserMedia_stopVideoStreamWithFollowupVideo.html]
 [test_getUserMedia_trackCloneCleanup.html]
 [test_getUserMedia_trackEnded.html]
 [test_getUserMedia_peerIdentity.html]
 [test_peerConnection_addIceCandidate.html]
 [test_peerConnection_addtrack_removetrack_events.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_transceivers.html]
 [test_peerConnection_basicAudio.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicAudioNATSrflx.html]
 skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
 [test_peerConnection_basicAudioNATRelay.html]
 skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
 [test_peerConnection_basicAudioNATRelayTCP.html]
 skip-if = toolkit == 'android' # websockets don't work on android (bug 1266217)
--- 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,20 @@ 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.observedRemoteTrackInfoById = {};
 
   this.disableRtpCountChecking = false;
 
   this.iceConnectedResolve;
   this.iceConnectedReject;
   this.iceConnected = new Promise((resolve, reject) => {
     this.iceConnectedResolve = resolve;
@@ -890,36 +892,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 : (track, direction) =>
+  {
     const idPrefix = [this.label, direction].join('_');
-    var element = getMediaElementForTrack(track, idPrefix);
+    return getMediaElementForTrack(track, idPrefix);
+  },
 
+  createMediaElementForTrack : (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 +966,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,75 +1005,101 @@ 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");
     });
+
+    return this.observedNegotiationNeeded;
   },
 
   removeSender : function(index) {
     var sender = this._pc.getSenders()[index];
     delete this.expectedLocalTrackInfoById[sender.track.id];
     this.expectNegotiationNeeded();
     this._pc.removeTrack(sender);
     return this.observedNegotiationNeeded;
   },
 
-  senderReplaceTrack : function(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);
   },
 
+  getUserMedia : async function(constraints) {
+    var stream = await getUserMedia(constraints);
+    if (constraints.audio) {
+      stream.getAudioTracks().forEach(track => {
+        info(this + " gUM local stream " + stream.id +
+          " with audio track " + track.id);
+      });
+    }
+    if (constraints.video) {
+      stream.getVideoTracks().forEach(track => {
+        info(this + " gUM local stream " + stream.id +
+          " with video track " + track.id);
+      });
+    }
+    return stream;
+  },
+
   /**
    * Requests all the media streams as specified in the constrains property.
    *
    * @param {array} constraintsList
    *        Array of constraints for GUM calls
    */
   getAllUserMedia : function(constraintsList) {
     if (constraintsList.length === 0) {
       info("Skipping GUM: no UserMedia requested");
       return Promise.resolve();
     }
 
     info("Get " + constraintsList.length + " local streams");
-    return Promise.all(constraintsList.map(constraints => {
-      return getUserMedia(constraints).then(stream => {
-        if (constraints.audio) {
-          stream.getAudioTracks().forEach(track => {
-            info(this + " gUM local stream " + stream.id +
-              " with audio track " + track.id);
-          });
-        }
-        if (constraints.video) {
-          stream.getVideoTracks().forEach(track => {
-            info(this + " gUM local stream " + stream.id +
-              " with video track " + track.id);
-          });
-        }
-        return this.attachLocalStream(stream);
-      });
-    }));
+
+    return Promise.all(
+      constraintsList.map(constraints => this.getUserMedia(constraints)));
+  },
+
+  getAllUserMediaAndAddTracks : async function(constraintsList) {
+    var streams = await this.getAllUserMedia(constraintsList);
+    if (!streams) {
+      return;
+    }
+    return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
+  },
+
+  getAllUserMediaAndAddTransceivers : async function(constraintsList) {
+    var streams = await this.getAllUserMedia(constraintsList);
+    if (!streams) {
+      return;
+    }
+    return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
   },
 
   /**
    * Create a new data channel instance.  Also creates a promise called
    * `this.nextDataChannel` that resolves when the next data channel arrives.
    */
   expectDataChannel: function(message) {
     this.nextDataChannel = new Promise(resolve => {
@@ -1179,44 +1239,55 @@ 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);
+      // TODO(bug XXXXX): Checking for remote tracks needs to be completely
+      // reworked, because with the latest spec the identifiers aren't the same
+      // as they are on the other end. Ultimately, what we need to check is
+      // whether the _transceivers_ are in line with what is expected, and
+      // whether the callbacks are consistent with the transceivers.
+      let trackId = this._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 +1410,47 @@ 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));
-
-    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");
   },
 
   markRemoteTracksAsNegotiated: function() {
     Object.values(this.observedRemoteTrackInfoById).forEach(
         trackInfo => trackInfo.negotiated = true);
   },
 
   rollbackRemoteTracksIfNotNegotiated: function() {
@@ -1476,32 +1541,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 +1635,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 +1770,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 +1804,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 +1889,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 +1970,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();
@@ -178,21 +177,21 @@ var commandsPeerConnectionInitial = [
   function PC_REMOTE_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
     is(test.pcRemote._pc.canTrickleIceCandidates, null,
        "Remote trickle status should start out unknown");
   },
 ];
 
 var commandsGetUserMedia = [
   function PC_LOCAL_GUM(test) {
-    return test.pcLocal.getAllUserMedia(test.pcLocal.constraints);
+    return test.pcLocal.getAllUserMediaAndAddTracks(test.pcLocal.constraints);
   },
 
   function PC_REMOTE_GUM(test) {
-    return test.pcRemote.getAllUserMedia(test.pcRemote.constraints);
+    return test.pcRemote.getAllUserMediaAndAddTracks(test.pcRemote.constraints);
   },
 ];
 
 var commandsPeerConnectionOfferAnswer = [
   function PC_LOCAL_SETUP_ICE_HANDLER(test) {
     test.pcLocal.setupIceCandidateHandler(test);
   },
 
@@ -209,42 +208,16 @@ var commandsPeerConnectionOfferAnswer = 
 
   function PC_REMOTE_STEEPLECHASE_SIGNAL_EXPECTED_LOCAL_TRACKS(test) {
     if (test.testOptions.steeplechase) {
       send_message({"type": "remote_expected_tracks",
                     "expected_tracks": test.pcRemote.expectedLocalTrackInfoById});
     }
   },
 
-  function PC_LOCAL_GET_EXPECTED_REMOTE_TRACKS(test) {
-    if (test.testOptions.steeplechase) {
-      return test.getSignalingMessage("remote_expected_tracks").then(
-          message => {
-            test.pcLocal.expectedRemoteTrackInfoById = message.expected_tracks;
-          });
-    }
-
-    // Deep copy, as similar to steeplechase as possible
-    test.pcLocal.expectedRemoteTrackInfoById =
-      JSON.parse(JSON.stringify(test.pcRemote.expectedLocalTrackInfoById));
-  },
-
-  function PC_REMOTE_GET_EXPECTED_REMOTE_TRACKS(test) {
-    if (test.testOptions.steeplechase) {
-      return test.getSignalingMessage("local_expected_tracks").then(
-          message => {
-            test.pcRemote.expectedRemoteTrackInfoById = message.expected_tracks;
-          });
-    }
-
-    // Deep copy, as similar to steeplechase as possible
-    test.pcRemote.expectedRemoteTrackInfoById =
-      JSON.parse(JSON.stringify(test.pcLocal.expectedLocalTrackInfoById));
-  },
-
   function PC_LOCAL_CREATE_OFFER(test) {
     return test.createOffer(test.pcLocal).then(offer => {
       is(test.pcLocal.signalingState, STABLE,
          "Local create offer does not change signaling state");
     });
   },
 
   function PC_LOCAL_STEEPLECHASE_SIGNAL_OFFER(test) {
@@ -430,29 +403,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_addSecondAudioStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
@@ -13,17 +13,17 @@
 
   runNetworkTest(function (options) {
     const test = new PeerConnectionTest(options);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{audio: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
           // We test both tracks to avoid an ordering problem
           is(test.pcRemote._pc.getReceivers().length, 2,
              "pcRemote should have two receivers");
           return Promise.all(test.pcRemote._pc.getReceivers().map(r => {
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
@@ -17,17 +17,17 @@
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
           // Since this is a NoBundle variant, adding a track will cause us to
           // go back to checking.
           test.pcLocal.expectIceChecking();
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{audio: true}]);
         },
         function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
           test.pcRemote.expectIceChecking();
         },
       ],
       [
         function PC_REMOTE_CHECK_ADDED_TRACK(test) {
           // We test both tracks to avoid an ordering problem
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
@@ -16,17 +16,17 @@
     const test = new PeerConnectionTest(options);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{video: true}, {video: true}],
                                    [{video: true}]);
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{video: true, fake: true}]);
         },
       ],
       [
         function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
           const h = new VideoStreamHelper();
           is(test.pcRemote.remoteMediaElements.length, 2,
              "Should have two remote media elements after renegotiation");
           return Promise.all(test.pcRemote.remoteMediaElements.map(video =>
--- a/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
@@ -20,17 +20,17 @@
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{video: true}, {video: true}],
                                    [{video: true}]);
           // Since this is a NoBundle variant, adding a track will cause us to
           // go back to checking.
           test.pcLocal.expectIceChecking();
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{video: true, fake: true}]);
         },
         function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
           test.pcRemote.expectIceChecking();
         },
       ],
       [
         function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
           const h = new VideoStreamHelper();
--- a/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
+++ b/dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
@@ -15,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_answererAddSecondAudioStream.html
+++ b/dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
@@ -14,17 +14,17 @@
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
     addRenegotiationAnswerer(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{audio: true}]);
         },
       ]
     );
 
     test.setMediaConstraints([{audio: true}], [{audio: true}]);
     test.run();
   });
 </script>
--- 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
@@ -45,17 +45,17 @@ runNetworkTest(() => {
     ok(receivedStream, "We should receive a stream with with the sent stream's id (" + sentStreamId + ")");
     if (!receivedStream) {
       return;
     }
 
     is(receivedStream.getTracks().length, sentTracks.length,
        "Should receive same number of tracks as were sent");
     sentTracks.forEach(t =>
-      ok(receivedStream.getTracks().find(t2 => t.id == 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_localReofferRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_localReofferRollback.html
@@ -13,17 +13,17 @@
 
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
     addRenegotiation(test.chain, [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{audio: true}]);
         },
 
         function PC_REMOTE_SETUP_ICE_HANDLER(test) {
           test.pcRemote.setupIceCandidateHandler(test);
           if (test.testOptions.steeplechase) {
             test.pcRemote.endOfTrickleIce.then(() => {
               send_message({"type": "end_of_trickle_ice"});
             });
@@ -32,16 +32,19 @@
 
         function PC_REMOTE_CREATE_AND_SET_OFFER(test) {
           return test.createOffer(test.pcRemote).then(offer => {
             return test.setLocalDescription(test.pcRemote, offer, HAVE_LOCAL_OFFER);
           });
         },
 
         function PC_REMOTE_ROLLBACK(test) {
+          // the negotiationNeeded slot should have been true both before and
+          // after this SLD, so the event should fire again.
+          test.pcRemote.expectNegotiationNeeded();
           return test.setLocalDescription(test.pcRemote,
                                           { type: "rollback", sdp: "" },
                                           STABLE);
         },
 
         // Rolling back should shut down gathering
         function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
           return test.pcRemote.endOfTrickleIce;
--- a/dom/media/tests/mochitest/test_peerConnection_localRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_localRollback.html
@@ -18,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_remoteReofferRollback.html
+++ b/dom/media/tests/mochitest/test_peerConnection_remoteReofferRollback.html
@@ -14,17 +14,17 @@
   var test;
   runNetworkTest(function (options) {
     test = new PeerConnectionTest(options);
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}, {audio: true}],
                                    [{audio: true}]);
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{audio: true}]);
         },
       ]
     );
     test.chain.replaceAfter('PC_REMOTE_SET_REMOTE_DESCRIPTION',
       [
         function PC_LOCAL_SETUP_ICE_HANDLER(test) {
           test.pcLocal.setupIceCandidateHandler(test);
           if (test.testOptions.steeplechase) {
--- a/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
+++ b/dom/media/tests/mochitest/test_peerConnection_removeAudioTrack.html
@@ -32,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
@@ -24,31 +24,40 @@
         function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
           return test.pcLocal.removeSender(0);
         },
         function PC_LOCAL_ADD_AUDIO_TRACK(test) {
           // The new track's pipeline will start with a packet count of
           // 0, but the remote side will keep its old pipeline and packet
           // count.
           test.pcLocal.disableRtpCountChecking = true;
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{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;
@@ -24,31 +26,46 @@
         function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
           // The new track's pipeline will start with a packet count of
           // 0, but the remote side will keep its old pipeline and packet
           // count.
           test.pcLocal.disableRtpCountChecking = true;
           return test.pcLocal.removeSender(0);
         },
         function PC_LOCAL_ADD_AUDIO_TRACK(test) {
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{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
@@ -28,33 +28,38 @@
           // 0, but the remote side will keep its old pipeline and packet
           // count.
           test.pcLocal.disableRtpCountChecking = true;
           return test.pcLocal.removeSender(0);
         },
         function PC_LOCAL_ADD_VIDEO_TRACK(test) {
           // Use fake:true here since the native fake device on linux doesn't
           // change color as needed by checkVideoPlaying() below.
-          return test.pcLocal.getAllUserMedia([{video: true, fake: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{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");
@@ -28,33 +30,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_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}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{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,64 @@
         // (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");
+            // TODO: Should PC remove local streams when there are no senders
+            // associated with it? getLocalStreams() isn't in the spec anymore,
+            // so I guess it is pretty arbitrary?
             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_scaleResolution.html
+++ b/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
@@ -17,71 +17,78 @@
   var mustRejectWith = (msg, reason, f) =>
     f().then(() => ok(false, msg),
              e => is(e.name, reason, msg));
 
   var removeAllButCodec = (d, codec) =>
     (d.sdp = d.sdp.replace(/m=video (\w) UDP\/TLS\/RTP\/SAVPF \w.*\r\n/,
                            "m=video $1 UDP/TLS/RTP/SAVPF " + codec + "\r\n"), d);
 
-  function testScale(codec) {
+  async function testScale(codec) {
     var pc1 = new RTCPeerConnection();
     var pc2 = new RTCPeerConnection();
 
     var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
     pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
     pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
 
     info("testing scaling with " + codec);
 
-    pc1.onnegotiationneeded = e =>
-      pc1.createOffer()
-      .then(d => pc1.setLocalDescription(codec == "VP8" ? d : removeAllButCodec(d, 126)))
-      .then(() => pc2.setRemoteDescription(pc1.localDescription))
-      .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(d))
-      .then(() => pc1.setRemoteDescription(pc2.localDescription))
-      .catch(generateErrorCallback());
+
+    let stream = await navigator.mediaDevices.getUserMedia({ video: true });
+
+    var v1 = createMediaElement('video', 'v1');
+    var v2 = createMediaElement('video', 'v2');
+
+    var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e));
+    var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve);
+
+    is(v2.currentTime, 0, "v2.currentTime is zero at outset");
 
-    return navigator.mediaDevices.getUserMedia({ video: true })
-    .then(stream => {
-      var v1 = createMediaElement('video', 'v1');
-      var v2 = createMediaElement('video', 'v2');
+    v1.srcObject = stream;
+    var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
 
-      is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+    await mustRejectWith(
+        "Invalid scaleResolutionDownBy must reject", "RangeError",
+        () => sender.setParameters(
+            { encodings:[{ scaleResolutionDownBy: 0.5 } ] })
+    );
 
-      v1.srcObject = stream;
-      var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+    await sender.setParameters({ encodings: [{ maxBitrate: 60000,
+                                               scaleResolutionDownBy: 2 }] });
 
-      return mustRejectWith("Invalid scaleResolutionDownBy must reject", "RangeError",
-                            () => sender.setParameters({ encodings:
-                                                       [{ scaleResolutionDownBy: 0.5 } ] }))
-      .then(() => sender.setParameters({ encodings: [{ maxBitrate: 60000,
-                                                       scaleResolutionDownBy: 2 }] }))
-      .then(() => new Promise(resolve => pc2.ontrack = e => resolve(e)))
-      .then(e => v2.srcObject = e.streams[0])
-      .then(() => new Promise(resolve => v2.onloadedmetadata = resolve))
-      .then(() => waitUntil(() => v2.currentTime > 0 && v2.srcObject.currentTime > 0))
-      .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
-      .then(() => wait(3000)) // TODO: Bug 1248154
-      .then(() => {
-        ok(v1.videoWidth > 0, "source width is positive");
-        ok(v1.videoHeight > 0, "source height is positive");
-        if (v2.videoWidth == 640 && v2.videoHeight == 480) { // TODO: Bug 1248154
-          info("Skipping test due to Bug 1248154");
-        } else {
-          is(v2.videoWidth, v1.videoWidth / 2, "sink is half the width of source");
-          is(v2.videoHeight, v1.videoHeight / 2, "sink is half the height of source");
-        }
-      })
-      .then(() => {
-        stream.getTracks().forEach(track => track.stop());
-        v1.srcObject = v2.srcObject = null;
-      })
-    })
-    .catch(generateErrorCallback());
+    let offer = await pc1.createOffer();
+    await pc1.setLocalDescription(
+        codec == "VP8" ? offer : removeAllButCodec(offer, 126));
+    await pc2.setRemoteDescription(pc1.localDescription);
+
+    let answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(pc2.localDescription);
+    let trackevent = await ontrackfired;
+
+    v2.srcObject = trackevent.streams[0];
+
+    await v2loadedmetadata;
+
+    await waitUntil(() => v2.currentTime > 0 && v2.srcObject.currentTime > 0);
+    ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
+
+    await wait(3000); // TODO: Bug 1248154
+
+    ok(v1.videoWidth > 0, "source width is positive");
+    ok(v1.videoHeight > 0, "source height is positive");
+    if (v2.videoWidth == 640 && v2.videoHeight == 480) { // TODO: Bug 1248154
+      info("Skipping test due to Bug 1248154");
+    } else {
+      is(v2.videoWidth, v1.videoWidth / 2, "sink is half the width of source");
+      is(v2.videoHeight, v1.videoHeight / 2, "sink is half the height of source");
+    }
+    stream.getTracks().forEach(track => track.stop());
+    v1.srcObject = v2.srcObject = null;
   }
 
   pushPrefs(['media.peerconnection.video.lock_scaling', true]).then(() => {
     if (!navigator.appVersion.includes("Android")) {
       runNetworkTest(() => testScale("VP8").then(() => testScale("H264"))
                     .then(networkTestFinished));
     } else {
       // No support for H.264 on Android in automation, see Bug 1355786
--- a/dom/media/tests/mochitest/test_peerConnection_setParameters.html
+++ b/dom/media/tests/mochitest/test_peerConnection_setParameters.html
@@ -12,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]));
     };
 
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_transceivers.html
@@ -0,0 +1,976 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    bug: "1290948",
+    title: "Transceivers API tests"
+  });
+
+  var logExpected = (expected) => {
+    info("(expected " + JSON.stringify(expected) + ")");
+  };
+
+  var dictCompare = (observed, expected) => {
+
+    if (observed === expected) {
+      return true;
+    }
+
+    // If we are expecting an array, iterate over it
+    if (Array.isArray(expected)) {
+      if (!Array.isArray(observed)) {
+        ok(false, "Expected an array, but didn't get one.");
+        logExpected(expected);
+        return false;
+      }
+
+      if (observed.length !== expected.length) {
+        ok(false, "Expected array to be " + expected.length + " long, but it was " + observed.length + " long instead");
+        logExpected(expected);
+        return false;
+      }
+
+      for (var i = 0; i < expected.length; i++) {
+        if (!dictCompare(observed[i], expected[i])) {
+          logExpected(expected);
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+    // If we are expecting an object, check its props
+    if (typeof expected === "object" && expected !== null) {
+      var propsWeCareAbout = Object.getOwnPropertyNames(expected);
+      for (var i in propsWeCareAbout) {
+        var prop = propsWeCareAbout[i];
+        if (!dictCompare(observed[prop], expected[prop])) {
+          logExpected(expected);
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+    ok(false, "Expected (" + JSON.stringify(expected) + ") did not match " +
+              "observed (" + JSON.stringify(observed) + ")");
+    return false;
+  };
+
+  var checkAddTransceiverNoTrack = async function(options) {
+    var pc = new RTCPeerConnection();
+    dictCompare(pc.getTransceivers(), []);
+
+    pc.addTransceiver("audio");
+    pc.addTransceiver("video");
+
+    dictCompare(pc.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        },
+        {
+          receiver: {track: {kind: "video"}},
+          sender: {track: null},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        }
+      ]);
+
+    pc.close();
+  };
+
+  var checkAddTransceiverWithTrack = async function(options) {
+    var pc = new RTCPeerConnection();
+    dictCompare(pc.getTransceivers(), []);
+
+    var stream = await getUserMedia({audio: true, video: true});
+    var audio = stream.getAudioTracks()[0];
+    var video = stream.getVideoTracks()[0];
+
+    pc.addTransceiver(audio);
+    pc.addTransceiver(video);
+
+    dictCompare(pc.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: audio},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        },
+        {
+          receiver: {track: {kind: "video"}},
+          sender: {track: video},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        }
+      ]);
+
+    pc.close();
+  };
+
+  var checkAddTransceiverWithAddTrack = async function(options) {
+    var pc = new RTCPeerConnection();
+    dictCompare(pc.getTransceivers(), []);
+
+    var stream = await getUserMedia({audio: true, video: true});
+    var audio = stream.getAudioTracks()[0];
+    var video = stream.getVideoTracks()[0];
+
+    pc.addTrack(audio, stream);
+    pc.addTrack(video, stream);
+
+    dictCompare(pc.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: audio},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        },
+        {
+          receiver: {track: {kind: "video"}},
+          sender: {track: video},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        }
+      ]);
+
+    pc.close();
+  };
+
+  var checkAddTransceiverWithDirection = async function(options) {
+    var pc = new RTCPeerConnection();
+    dictCompare(pc.getTransceivers(), []);
+
+    pc.addTransceiver("audio", {direction: "recvonly"});
+    pc.addTransceiver("video", {direction: "recvonly"});
+
+    dictCompare(pc.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          direction: "recvonly",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        },
+        {
+          receiver: {track: {kind: "video"}},
+          sender: {track: null},
+          direction: "recvonly",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        }
+      ]);
+
+    pc.close();
+  };
+
+  var checkAddTransceiverWithStream = async function(options) {
+    var pc = new RTCPeerConnection();
+    dictCompare(pc.getTransceivers(), []);
+
+    var audioStream = await getUserMedia({audio: true});
+    var videoStream = await getUserMedia({video: true});
+    var audio = audioStream.getAudioTracks()[0];
+    var video = videoStream.getVideoTracks()[0];
+
+    pc.addTransceiver(audio, {streams: [audioStream]});
+    pc.addTransceiver(video, {streams: [videoStream]});
+
+    dictCompare(pc.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: audio},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        },
+        {
+          receiver: {track: {kind: "video"}},
+          sender: {track: video},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        }
+      ]);
+
+    var offer = await pc.createOffer();
+    ok(offer.sdp.includes("a=msid:" + audioStream.id + " " + audio.id),
+      "offer contains the expected audio msid");
+    ok(offer.sdp.includes("a=msid:" + videoStream.id + " " + video.id),
+      "offer contains the expected video msid");
+
+    pc.close();
+  };
+
+  var checkAddTransceiverWithOfferToReceive = async function(options, kind) {
+    var pc = new RTCPeerConnection();
+
+    if (kind == "audio") {
+      pc.createOffer({offerToReceiveAudio: true});
+    } else {
+      pc.createOffer({offerToReceiveVideo: true});
+    }
+
+    dictCompare(pc.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: kind}},
+          sender: {track: null},
+          direction: "recvonly",
+          mid: null,
+          currentDirection: null,
+          stopped: false
+        }
+      ]);
+
+    pc.close();
+  };
+
+  var checkAddTransceiverWithSetRemoteOfferSending = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc1.addTransceiver(track);
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+
+    dictCompare(pc2.getTransceivers(), 
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          // rtcweb-jsep says this is recvonly, w3c-webrtc does not...
+          direction: "recvonly",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkAddTransceiverWithSetRemoteOfferNoSend = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc1.addTransceiver(track);
+    pc1.getTransceivers()[0].setDirection("recvonly");
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+
+    dictCompare(pc2.getTransceivers(), 
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          // rtcweb-jsep says this is recvonly, w3c-webrtc does not...
+          direction: "recvonly",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkAddTransceiverBadKind = async function(options) {
+    var pc = new RTCPeerConnection();
+    try {
+      pc.addTransceiver("foo");
+      ok(false, 'addTransceiver("foo") throws');
+    }
+    catch (e if e instanceof TypeError) {
+      ok(true, 'addTransceiver("foo") throws a TypeError');
+    }
+    catch (e) {
+      ok(false, 'addTransceiver("foo") throws a TypeError');
+    }
+
+    dictCompare(pc.getTransceivers(), []);
+
+    pc.close();
+  };
+
+  var checkAddTransceiverNoTrackDoesntPair = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+    pc1.addTransceiver("audio");
+    pc2.addTransceiver("audio");
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {mid: null}, // no addTrack magic, doesn't auto-pair
+        {mid: "sdparta_0"} // Created by SRD
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkAddTransceiverWithTrackDoesntPair = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+    pc1.addTransceiver("audio");
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc2.addTransceiver(track);
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {mid: null, sender: {track: track}},
+        {mid: "sdparta_0", sender: {track: null}} // Created by SRD
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkAddTransceiverThenReplaceTrackDoesntPair = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+    pc1.addTransceiver("audio");
+    pc2.addTransceiver("audio");
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc2.getTransceivers()[0].sender.replaceTrack(track);
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {mid: null, sender: {track: track}},
+        {mid: "sdparta_0", sender: {track: null}} // Created by SRD
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkAddTransceiverThenAddTrackPairs = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+    pc1.addTransceiver("audio");
+    pc2.addTransceiver("audio");
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc2.addTrack(track, stream);
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {mid: "sdparta_0", sender: {track: track}}
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkAddTrackPairs = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+    pc1.addTransceiver("audio");
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc2.addTrack(track, stream);
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {mid: "sdparta_0", sender: {track: track}}
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkReplaceTrackNullDoesntPreventPairing = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+    pc1.addTransceiver("audio");
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc2.addTrack(track, stream);
+    pc2.getTransceivers()[0].sender.replaceTrack(null);
+
+    var offer = await pc1.createOffer();
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {mid: "sdparta_0", sender: {track: track}}
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkSetDirection = async function(options) {
+    var pc = new RTCPeerConnection();
+    pc.addTransceiver("audio");
+
+    pc.getTransceivers()[0].setDirection("sendonly");
+    dictCompare(pc.getTransceivers(),[{direction: "sendonly"}]);
+    pc.getTransceivers()[0].setDirection("recvonly");
+    dictCompare(pc.getTransceivers(),[{direction: "recvonly"}]);
+    pc.getTransceivers()[0].setDirection("inactive");
+    dictCompare(pc.getTransceivers(),[{direction: "inactive"}]);
+    pc.getTransceivers()[0].setDirection("sendrecv");
+    dictCompare(pc.getTransceivers(),[{direction: "sendrecv"}]);
+
+    pc.close();
+  };
+
+  var checkCurrentDirection = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc1.addTrack(track, stream);
+    pc2.addTrack(track, stream);
+    dictCompare(pc1.getTransceivers(), [{currentDirection: null}]);
+
+    var offer = await pc1.createOffer();
+    dictCompare(pc1.getTransceivers(), [{currentDirection: null}]);
+
+    await pc1.setLocalDescription(offer);
+    // Debatable, spec is kinda vague
+    dictCompare(pc1.getTransceivers(), [{currentDirection: "inactive"}]);
+
+    await pc2.setRemoteDescription(offer);
+    // Debatable, spec is kinda vague
+    dictCompare(pc2.getTransceivers(), [{currentDirection: "inactive"}]);
+
+    var answer = await pc2.createAnswer();
+    dictCompare(pc2.getTransceivers(), [{currentDirection: "inactive"}]);
+
+    await pc2.setLocalDescription(answer);
+    dictCompare(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+    await pc1.setRemoteDescription(answer);
+    dictCompare(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+    pc2.getTransceivers()[0].setDirection("sendonly");
+
+    offer = await pc2.createOffer();
+    dictCompare(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+    await pc2.setLocalDescription(offer);
+    // Debatable, spec is kinda vague
+    dictCompare(pc2.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+    await pc1.setRemoteDescription(offer);
+    // Debatable, spec is kinda vague
+    dictCompare(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+    answer = await pc1.createAnswer();
+    dictCompare(pc1.getTransceivers(), [{currentDirection: "sendrecv"}]);
+
+    await pc1.setLocalDescription(answer);
+    dictCompare(pc1.getTransceivers(), [{currentDirection: "recvonly"}]);
+
+    await pc2.setRemoteDescription(answer);
+    dictCompare(pc2.getTransceivers(), [{currentDirection: "sendonly"}]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkStop = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc1.addTrack(track, stream);
+
+    var offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+
+    var pc2 = new RTCPeerConnection();
+    await pc2.setRemoteDescription(offer);
+
+    stream = await getUserMedia({audio: true});
+    track = stream.getAudioTracks()[0];
+    pc2.addTrack(track, stream);
+
+    var answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    pc1.getTransceivers()[0].stop();
+
+    dictCompare(pc1.getTransceivers(),
+      [
+        {
+          sender: {track: {kind: "audio"}},
+          receiver: {track: {kind: "audio"}},
+          stopped: true,
+          mid: "sdparta_0",
+          currentDirection: null,
+          direction: "sendrecv" // Debatable?
+        }
+      ]);
+
+    offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          sender: {track: {kind: "audio"}},
+          receiver: {track: {kind: "audio"}},
+          stopped: true,
+          mid: null,
+          currentDirection: null,
+          direction: "sendrecv"
+        }
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkRollback = async function(options) {
+    var pc1 = new RTCPeerConnection();
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc1.addTrack(track, stream);
+
+    var offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+
+    dictCompare(pc1.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: track},
+          direction: "sendrecv",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    // Verify that rollback doesn't stomp things it should not
+    pc1.getTransceivers()[0].setDirection("sendonly");
+    stream = await getUserMedia({audio: true});
+    track = stream.getAudioTracks()[0];
+    await pc1.getTransceivers()[0].sender.replaceTrack(track);
+
+    await pc1.setLocalDescription({sdp: "", type: "rollback"});
+
+    dictCompare(pc1.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: track},
+          direction: "sendonly",
+          mid: null,
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    var pc2 = new RTCPeerConnection();
+
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          direction: "recvonly",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    await pc2.setRemoteDescription({sdp: "", type: "rollback"});
+
+    // Transceiver should be _gone_
+    dictCompare(pc2.getTransceivers(), []);
+
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          direction: "recvonly",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    stream = await getUserMedia({audio: true});
+    track = stream.getAudioTracks()[0];
+    await pc2.getTransceivers()[0].sender.replaceTrack(track);
+    pc2.getTransceivers()[0].setDirection("sendrecv");
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: track},
+          direction: "sendrecv",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    await pc2.setRemoteDescription({sdp: "", type: "rollback"});
+
+    // Transceiver should be _gone_, again. replaceTrack doesn't prevent this,
+    // nor does setDirection.
+    dictCompare(pc2.getTransceivers(), []);
+
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: null},
+          direction: "recvonly",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    pc2.addTrack(track, stream);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: track},
+          direction: "sendrecv",
+          mid: "sdparta_0",
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    await pc2.setRemoteDescription({sdp: "", type: "rollback"});
+    // Transceiver should _not_ be gone this time, because addTrack touched it.
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: track},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: "inactive",
+          stopped: false
+        }
+      ]);
+
+    // Complete negotiation so we can test interactions with transceiver.stop()
+    offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+    var answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    // Don't bother waiting for ICE and such
+
+    offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    pc1.getTransceivers()[0].stop();
+    await pc1.setLocalDescription({sdp: "", type: "rollback"});
+    dictCompare(pc1.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: {kind: "audio"}},
+          direction: "sendonly",
+          mid: "sdparta_0",
+          currentDirection: null,
+          stopped: true
+        }
+      ]);
+
+    offer = await pc1.createOffer();
+
+    await pc2.setRemoteDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: {kind: "audio"}},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: true
+        }
+      ]);
+
+    // stop() cannot be rolled back!
+    await pc2.setRemoteDescription({sdp: "", type: "rollback"});
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          receiver: {track: {kind: "audio"}},
+          sender: {track: {kind: "audio"}},
+          direction: "sendrecv",
+          mid: null,
+          currentDirection: null,
+          stopped: true
+        }
+      ]);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  var checkMsectionReuse = async function(options) {
+    var pc1 = new RTCPeerConnection();
+    var pc2 = new RTCPeerConnection();
+
+    var stream = await getUserMedia({audio: true});
+    var track = stream.getAudioTracks()[0];
+    pc1.addTrack(track, stream);
+
+    var offer = await pc1.createOffer();
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+
+    // answerer stops transceiver to reject m-section
+    pc2.getTransceivers()[0].stop();
+
+    var answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    dictCompare(pc1.getTransceivers(),
+      [
+        {
+          mid: null,
+          currentDirection: null,
+          stopped: true
+        }
+      ]);
+
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          mid: null,
+          currentDirection: null,
+          stopped: true
+        }
+      ]);
+
+    // Check that m-section is reused on both ends
+    stream = await getUserMedia({audio: true});
+    track = stream.getAudioTracks()[0];
+
+    pc1.addTrack(track, stream);
+    offer = await pc1.createOffer();
+    // Exactly one m= line
+    is(1, offer.sdp.match(/m=/g).length);
+    dictCompare(pc1.getTransceivers(),
+      [
+        {
+          stopped: true
+        },
+        {
+          sender: {track: track}
+        }
+      ]);
+
+
+    pc2.addTrack(track, stream);
+    offer = await pc2.createOffer();
+    // Exactly one m= line
+    is(1, offer.sdp.match(/m=/g).length);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {
+          stopped: true
+        },
+        {
+          sender: {track: track}
+        }
+      ]);
+
+    await pc2.setLocalDescription(offer);
+    await pc1.setRemoteDescription(offer);
+    answer = await pc1.createAnswer();
+    await pc1.setLocalDescription(answer);
+    await pc2.setRemoteDescription(answer);
+    dictCompare(pc1.getTransceivers(),
+      [
+        {},
+        {
+          sender: {track: track},
+          currentDirection: "sendrecv"
+        }
+      ]);
+
+    dictCompare(pc2.getTransceivers(),
+      [
+        {},
+        {
+          sender: {track: track},
+          currentDirection: "sendrecv"
+        }
+      ]);
+
+    // stop the transceiver, and add a track. Verify that we don't reuse
+    // prematurely in our offer. (There should be one rejected m-section, and a
+    // new one for the new track)
+    pc1.getTransceivers()[1].stop();
+    stream = await getUserMedia({audio: true});
+    track = stream.getAudioTracks()[0];
+    pc1.addTrack(track, stream);
+    offer = await pc1.createOffer();
+    // Exactly two m= lines
+    is(2, offer.sdp.match(/m=/g).length);
+
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(offer);
+    answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(answer);
+
+    dictCompare(pc2.getTransceivers(),
+      [
+        {},
+        {
+          stopped: true
+        },
+        {
+          mid: "sdparta_1",
+          sender: {track: null},
+          currentDirection: "recvonly"
+        }
+      ]);
+
+    pc2.addTrack(track, stream);
+    // There are two ways to handle this new track; reuse the recvonly
+    // transceiver created above, or create a new transceiver and reuse the
+    // disabled m-section. We're supposed to do the former.
+    offer = await pc2.createOffer();
+    // Exactly two m= lines, as before
+    is(2, offer.sdp.match(/m=/g).length);
+
+    dictCompare(pc2.getTransceivers(),
+      [
+        {},
+        {
+          stopped: true
+        },
+        {
+          mid: "sdparta_1",
+          sender: {track: track},
+          currentDirection: "recvonly",
+          direction: "sendrecv"
+        }
+      ]);
+
+    // Add _another_ track; this should reuse the disabled m-section
+    stream = await getUserMedia({audio: true});
+    track = stream.getAudioTracks()[0];
+    pc2.addTrack(track, stream);
+    offer = await pc2.createOffer();
+    await pc2.setLocalDescription(offer);
+    dictCompare(pc2.getTransceivers(),
+      [
+        {}, {},
+        {
+          mid: "sdparta_1",
+        },
+        {
+          sender: {track: track},
+          mid: "sdparta_0"
+        }
+      ]);
+    // Exactly two m= lines, as before
+    is(2, offer.sdp.match(/m=/g).length);
+
+    pc1.close();
+    pc2.close();
+  };
+
+  runNetworkTest(async function (options) {
+    await checkAddTransceiverNoTrack(options);
+    await checkAddTransceiverWithTrack(options);
+    await checkAddTransceiverWithAddTrack(options);
+    await checkAddTransceiverWithDirection(options);
+    await checkAddTransceiverWithStream(options);
+    await checkAddTransceiverWithOfferToReceive(options, "audio");
+    await checkAddTransceiverWithOfferToReceive(options, "video");
+    await checkAddTransceiverWithSetRemoteOfferSending(options);
+    await checkAddTransceiverWithSetRemoteOfferNoSend(options);
+    await checkAddTransceiverBadKind(options);
+    await checkSetDirection(options);
+    await checkCurrentDirection(options);
+    await checkAddTransceiverNoTrackDoesntPair(options);
+    await checkAddTransceiverWithTrackDoesntPair(options);
+    await checkAddTransceiverThenReplaceTrackDoesntPair(options);
+    await checkAddTransceiverThenAddTrackPairs(options);
+    await checkAddTrackPairs(options);
+    await checkReplaceTrackNullDoesntPreventPairing(options);
+    await checkStop(options);
+    await checkRollback(options);
+    await checkMsectionReuse(options);
+    return networkTestFinished();
+  });
+</script>
+</pre>
+</body>
+</html>
--- 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>
--- a/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyAudioAfterRenegotiation.html
@@ -43,17 +43,17 @@
       }
     ]);
 
     addRenegotiation(test.chain,
       [
         function PC_LOCAL_ADD_SECOND_STREAM(test) {
           test.setMediaConstraints([{audio: true}],
                                    []);
-          return test.pcLocal.getAllUserMedia([{audio: true}]);
+          return test.pcLocal.getAllUserMediaAndAddTracks([{audio: true}]);
         },
       ]
     );
 
     test.chain.append([
       function CHECK_ASSUMPTIONS2() {
         is(test.pcLocal.localMediaElements.length, 2,
            "pcLocal should have two media elements");
--- a/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
@@ -69,17 +69,17 @@ runNetworkTest(() => {
 
   addRenegotiation(test.chain,
     [
       function PC_LOCAL_ADD_SECOND_STREAM(test) {
         canvas2 = h2.createAndAppendElement('canvas', 'source_canvas2');
         h2.drawColor(canvas2, h2.blue);
         stream2 = canvas2.captureStream(0);
 
-        // can't use test.pcLocal.getAllUserMedia([{video: true}]);
+        // can't use test.pcLocal.getAllUserMediaAndAddTracks([{video: true}]);
         // because it doesn't let us substitute the capture stream
         test.pcLocal.attachLocalStream(stream2);
       }
     ]
   );
 
   test.chain.append([
     function FIND_REMOTE2_VIDEO() {
--- a/dom/webidl/MediaStream.webidl
+++ b/dom/webidl/MediaStream.webidl
@@ -35,9 +35,14 @@ interface MediaStream : EventTarget {
     MediaStreamTrack?          getTrackById (DOMString trackId);
     void                       addTrack (MediaStreamTrack track);
     void                       removeTrack (MediaStreamTrack track);
     MediaStream                clone ();
     readonly    attribute boolean      active;
                 attribute EventHandler onaddtrack;
     //             attribute EventHandler onremovetrack;
     readonly attribute double currentTime;
+
+    // Webrtc allows the remote side to name a stream whatever it wants, and we
+    // need to surface this to content.
+    [ChromeOnly]
+    void assignId(DOMString id);
 };
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,32 @@ 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 insertDTMF(RTCRtpSender sender, DOMString tones,
+  TransceiverImpl createTransceiverImpl(DOMString kind,
+                                        MediaStreamTrack? track);
+  [Throws]
+  boolean checkNegotiationNeeded();
+  [Throws]
+  void insertDTMF(TransceiverImpl transceiver, 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(TransceiverImpl transceiverImpl,
+                                   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
@@ -18,33 +18,34 @@ interface PeerConnectionObserver
   void onCreateAnswerError(unsigned long name, DOMString message);
   void onSetLocalDescriptionSuccess();
   void onSetRemoteDescriptionSuccess();
   void onSetLocalDescriptionError(unsigned long name, DOMString message);
   void onSetRemoteDescriptionError(unsigned long name, DOMString message);
   void onAddIceCandidateSuccess();
   void onAddIceCandidateError(unsigned long name, DOMString message);
   void onIceCandidate(unsigned short level, DOMString mid, DOMString candidate);
-  void onNegotiationNeeded();
 
   /* Stats callbacks */
   void onGetStatsSuccess(optional RTCStatsReportInternal report);
   void onGetStatsError(unsigned long name, DOMString message);
 
   /* replaceTrack callbacks */
   void onReplaceTrackSuccess();
   void onReplaceTrackError(unsigned long name, DOMString message);
 
   /* Data channel callbacks */
   void notifyDataChannel(DataChannel channel);
 
   /* Notification of one of several types of state changed */
   void onStateChange(PCObserverStateType state);
 
   /* Changes to MediaStreamTracks */
-  void onAddStream(MediaStream stream);
   void onRemoveStream(MediaStream stream);
-  void 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, TransceiverImpl transceiverImpl);
 
   /* DTMF callback */
-  void onDTMFToneChange(DOMString trackId, DOMString tone);
+  void onDTMFToneChange(MediaStreamTrack track, DOMString tone);
 };
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -107,18 +107,25 @@ 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);
+
+  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,12 @@
  * 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]
+  attribute DOMString? webrtcTrackId;
 };
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -64,16 +64,21 @@ 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<void> replaceTrack(MediaStreamTrack? track);
   Promise<RTCStatsReport> getStats();
   [Pref="media.peerconnection.dtmf.enabled"]
   readonly attribute RTCDTMFSender? dtmf;
+  sequence<MediaStream> mozGetStreams();
+  [ChromeOnly]
+  void setTrack(MediaStreamTrack? track);
+  [ChromeOnly]
+  void checkWasCreatedByPc(RTCPeerConnection pc);
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCRtpTransceiver.webidl
@@ -0,0 +1,65 @@
+/* -*- 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;
+    void setDirection(RTCRtpTransceiverDirection direction);
+    void stop();
+    // void setCodecPreferences(sequence<RTCRtpCodecCapability> codecs);
+
+    [ChromeOnly]
+    void setAddTrackMagic();
+    [ChromeOnly]
+    readonly attribute boolean addTrackMagic;
+    [ChromeOnly]
+    void setCurrentDirection(RTCRtpTransceiverDirection direction);
+    [ChromeOnly]
+    void setMid(DOMString mid);
+    [ChromeOnly]
+    void unsetMid();
+    [ChromeOnly]
+    void setStopped();
+    [ChromeOnly]
+    void remove();
+
+    [ChromeOnly]
+    DOMString getKind();
+    [ChromeOnly]
+    boolean hasBeenUsedToSend();
+    [ChromeOnly]
+    void sync();
+
+    [ChromeOnly]
+    void insertDTMF(DOMString tones,
+                    optional unsigned long duration = 100,
+                    optional unsigned long interToneGap = 70);
+};
+
--- a/dom/webidl/RTCTrackEvent.webidl
+++ b/dom/webidl/RTCTrackEvent.webidl
@@ -6,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;
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/TransceiverImpl.webidl
@@ -0,0 +1,25 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * PeerConnection.js' interface to the C++ TransceiverImpl.
+ *
+ * Do not confuse with RTCRtpTransceiver. This interface is purely for
+ * communication between the PeerConnection JS DOM binding and the C++
+ * implementation.
+ *
+ * See media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
+ *
+ */
+
+interface nsISupports;
+
+// Constructed by PeerConnectionImpl::CreateTransceiverImpl.
+[ChromeOnly]
+interface TransceiverImpl {
+  MediaStreamTrack getReceiveTrack();
+  [Throws]
+  void syncWithJS(RTCRtpTransceiver transceiver);
+};
+
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -200,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")
@@ -981,32 +978,33 @@ WEBIDL_FILES = [
     'XULDocument.webidl',
     'XULElement.webidl',
     'XULTemplateBuilder.webidl',
 ]
 
 if CONFIG['MOZ_WEBRTC']:
     WEBIDL_FILES += [
         'DataChannel.webidl',
-        'MediaStreamList.webidl',
         'PeerConnectionImpl.webidl',
         'PeerConnectionImplEnums.webidl',
         'PeerConnectionObserver.webidl',
         'PeerConnectionObserverEnums.webidl',
         'RTCCertificate.webidl',
         'RTCConfiguration.webidl',
         'RTCDTMFSender.webidl',
         'RTCIceCandidate.webidl',
         'RTCIdentityAssertion.webidl',
         'RTCIdentityProvider.webidl',
         'RTCPeerConnection.webidl',
         'RTCPeerConnectionStatic.webidl',
         'RTCRtpReceiver.webidl',
         'RTCRtpSender.webidl',
+        'RTCRtpTransceiver.webidl',
         'RTCSessionDescription.webidl',
+        'TransceiverImpl.webidl',
         'WebrtcDeprecated.webidl',
         'WebrtcGlobalInformation.webidl',
     ]
 
 if CONFIG['MOZ_WEBSPEECH']:
     WEBIDL_FILES += [
         'SpeechGrammar.webidl',
         'SpeechGrammarList.webidl',
--- a/media/mtransport/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,158 @@ 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<RefPtr<JsepTransceiver>>& oldTransceivers,
+      const std::vector<RefPtr<JsepTransceiver>>& newTransceivers)
+  {
+    ASSERT_LE(oldTransceivers.size(), newTransceivers.size());
+    std::set<size_t> levels;
+
+    for (const RefPtr<JsepTransceiver>& newTransceiver : newTransceivers) {
+      if (newTransceiver->HasLevel()) {
+        ASSERT_FALSE(levels.count(newTransceiver->GetLevel()))
+                     << "Two new transceivers are mapped to level "
+                     << newTransceiver->GetLevel();
+        levels.insert(newTransceiver->GetLevel());
+      }
+    }
+
+    auto last = levels.rbegin();
+    if (last != levels.rend()) {
+      ASSERT_LE(*last, levels.size())
+          << "Max level observed in transceivers was " << *last
+          << ", but there are only " << levels.size() << " levels in the "
+          "transceivers.";
+    }
+
+    for (const RefPtr<JsepTransceiver>& oldTransceiver : oldTransceivers) {
+      if (oldTransceiver->HasLevel()) {
+        ASSERT_TRUE(levels.count(oldTransceiver->GetLevel()))
+                    << "Level " << oldTransceiver->GetLevel()
+                    << " had a transceiver in the old, but not the new (or, "
+                    "perhaps this level had more than one transceiver in the "
+                    "old)";
+        levels.erase(oldTransceiver->GetLevel());
+      }
+    }
+  }
+
+  std::vector<RefPtr<JsepTransceiver>>
+  DeepCopy(const std::vector<RefPtr<JsepTransceiver>>& transceivers)
+  {
+    std::vector<RefPtr<JsepTransceiver>> copy;
+    for (const RefPtr<JsepTransceiver>& transceiver : transceivers) {
+      copy.push_back(new JsepTransceiver(*transceiver));
+    }
+    return copy;
+  }
+
   std::string
   CreateOffer(const Maybe<JsepOfferOptions>& options = Nothing())
   {
+    std::vector<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionOff->GetTransceivers());
     JsepOfferOptions defaultOptions;
     const JsepOfferOptions& optionsRef = options ? *options : defaultOptions;
     std::string offer;
     nsresult rv;
     rv = mSessionOff->CreateOffer(optionsRef, &offer);
     EXPECT_EQ(NS_OK, rv) << mSessionOff->GetLastError();
 
     std::cerr << "OFFER: " << offer << std::endl;
 
     ValidateTransport(*mOffererTransport, offer);
 
+    if (transceiversBefore.size() != mSessionOff->GetTransceivers().size()) {
+      EXPECT_TRUE(false) << "CreateOffer changed number of transceivers!";
+      return offer;
+    }
+
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionOff->GetTransceivers());
+
+    for (size_t i = 0; i < transceiversBefore.size(); ++i) {
+      RefPtr<JsepTransceiver>& oldTransceiver = transceiversBefore[i];
+      RefPtr<JsepTransceiver>& newTransceiver = mSessionOff->GetTransceivers()[i];
+      EXPECT_EQ(oldTransceiver->IsStopped(), newTransceiver->IsStopped());
+
+      if (oldTransceiver->IsStopped()) {
+        if (!newTransceiver->HasLevel()) {
+          // Tolerate unmapping of stopped transceivers by removing this
+          // difference.
+          oldTransceiver->ClearLevel();
+        }
+      } else if (!oldTransceiver->HasLevel()) {
+        EXPECT_TRUE(newTransceiver->HasLevel());
+        // Tolerate new mappings.
+        oldTransceiver->SetLevel(newTransceiver->GetLevel());
+      }
+
+      EXPECT_TRUE(Equals(*oldTransceiver, *newTransceiver));
+    }
+
     return offer;
   }
 
+  typedef enum {
+    NO_ADDTRACK_MAGIC,
+    ADDTRACK_MAGIC
+  } AddTrackMagic;
+
   void
-  AddTracks(JsepSessionImpl& side)
+  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(SdpMediaSection::kAudio, sdp::kSend);
+    }
+
+    RefPtr<JsepTransceiver>& transceiver(side.GetTransceivers()[index]);
+    JsepTrack& track = transceiver->mSending;
+    EXPECT_FALSE(track.GetTrackId().empty()) << "No track at index " << index;
+
+    JsepTrack original(track);
+    track.ClearTrack();
+    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 +279,177 @@ 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);
+  }
+
+  // A bit of a hack. JsepSessionImpl populates the track-id automatically, just
+  // in case, because the w3c spec requires msid to be set even when there's no
+  // send track.
+  bool IsNull(const JsepTrack& track) const {
+    return track.GetStreamIds().empty() &&
+           (track.GetMediaType() != SdpMediaSection::MediaType::kApplication);
   }
 
   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<RefPtr<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 (IsNull(transceivers[i]->mSending) ||
+              type == SdpMediaSection::MediaType::kApplication) {
+            break;
+          }
+        }
+      }
+
+      if (i == transceivers.size()) {
+        side.AddTransceiver(new JsepTransceiver(type));
+        MOZ_ASSERT(i < transceivers.size());
+      }
+
+      std::cerr << "Updating send track for transceiver " << i << std::endl;
+      if (magic == ADDTRACK_MAGIC) {
+        transceivers[i]->SetAddTrackMagic();
+      }
+      transceivers[i]->mJsDirection |=
+        SdpDirectionAttribute::Direction::kSendonly;
+      transceivers[i]->mSending.UpdateTrack(
+          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 (!IsNull(transceiver->mSending)) {
+        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 (!IsNull(transceiver->mReceiving)) {
+        result.push_back(transceiver->mReceiving);
+      }
+    }
+    return result;
+  }
+
+  JsepTransceiver*
+  GetDatachannelTransceiver(JsepSession& side) {
+    for (const auto& transceiver : side.GetTransceivers()) {
+      if (transceiver->mSending.GetMediaType() ==
+            SdpMediaSection::MediaType::kApplication) {
+        return transceiver.get();
+      }
+    }
+
+    return nullptr;
+  }
+
+  JsepTransceiver*
+  GetNegotiatedTransceiver(JsepSession& side, size_t index) {
+    for (RefPtr<JsepTransceiver>& transceiver : side.GetTransceivers()) {
+      if (transceiver->mSending.GetNegotiatedDetails() ||
+          transceiver->mReceiving.GetNegotiatedDetails()) {
+        if (index) {
+          --index;
+          continue;
+        }
+
+        return transceiver.get();
+      }
+    }
+
+    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 +460,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 (IsNull(transceiver->mSending) ||
+          transceiver->mSending.GetMediaType() != type) {
         continue;
       }
 
       if (index != 0) {
         --index;
         continue;
       }
 
-      return *i;
+      return transceiver->mSending;
     }
 
-    return RefPtr<JsepTrack>(nullptr);
+    return JsepTrack(type, sdp::kSend);
   }
 
-  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 +566,126 @@ 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.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 (!Equals(p1.mSending, p2.mSending)) {
+      std::cerr << "Send track differs" << std::endl;
+      return false;
+    }
+
+    if (!Equals(p1.mReceiving, p2.mReceiving)) {
+      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(kJsepDescriptionCurrent));
@@ -497,44 +734,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<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(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 +786,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<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionOff->GetTransceivers());
+
     nsresult rv = mSessionOff->SetLocalDescription(kJsepSdpOffer, offer);
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionOff->GetTransceivers());
+
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
     if (checkFlags & CHECK_TRACKS) {
-      // 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(IsNull(track));
+        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<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionAns->GetTransceivers());
+
     nsresult rv = mSessionAns->SetRemoteDescription(kJsepSdpOffer, offer);
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionAns->GetTransceivers());
+
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
     if (checkFlags & CHECK_TRACKS) {
-      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(IsNull(track));
+        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<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionAns->GetTransceivers());
+
     nsresult rv = mSessionAns->SetLocalDescription(kJsepSdpAnswer, answer);
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionAns->GetTransceivers());
+
     if (checkFlags & CHECK_TRACKS) {
       // Verify that the right stuff is in the tracks.
-      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(IsNull(sendTrack));
+        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(IsNull(recvTrack));
+        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<RefPtr<JsepTransceiver>> transceiversBefore =
+      DeepCopy(mSessionOff->GetTransceivers());
+
     nsresult rv = mSessionOff->SetRemoteDescription(kJsepSdpAnswer, answer);
     if (checkFlags & CHECK_SUCCESS) {
       ASSERT_EQ(NS_OK, rv);
     }
 
+    CheckTransceiverInvariants(transceiversBefore,
+                               mSessionOff->GetTransceivers());
+
     if (checkFlags & CHECK_TRACKS) {
       // Verify that the right stuff is in the tracks.
-      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(IsNull(sendTrack));
+        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(IsNull(recvTrack));
+        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 {
@@ -935,23 +1205,22 @@ 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 CheckTransceiversAreBundled(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;
+    for (const auto& transceiver : session.GetTransceivers()) {
+      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
@@ -1029,16 +1298,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) {
@@ -1049,17 +1323,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) {
@@ -1068,28 +1347,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 (!IsNull(transceiver->mSending)) {
         std::cerr << "Sending-->" << std::endl;
-        DumpTrack(*i->mSending);
+        DumpTrack(transceiver->mSending);
       }
-      if (i->mReceiving) {
+      if (!IsNull(transceiver->mReceiving)) {
         std::cerr << "Receiving-->" << std::endl;
-        DumpTrack(*i->mReceiving);
+        DumpTrack(transceiver->mReceiving);
       }
     }
   }
 
   UniquePtr<Sdp>
   Parse(const std::string& sdp) const
   {
     SipccSdpParser parser;
@@ -1301,34 +1588,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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
   removed = mSessionAns->GetRemoteTracksRemoved();
   ASSERT_EQ(0U, added.size());
@@ -1341,27 +1630,29 @@ 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();
@@ -1381,18 +1672,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 = DeepCopy(mSessionOff->GetTransceivers());
+  auto answererTransceivers = DeepCopy(mSessionAns->GetTransceivers());
 
   SwapOfferAnswerRoles();
 
   std::string reoffer = CreateOffer();
   SetLocalOffer(reoffer);
   SetRemoteOffer(reoffer);
 
   added = mSessionAns->GetRemoteTracksAdded();
@@ -1407,108 +1698,114 @@ 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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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(new JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::Direction::kRecvonly));
+  mSessionOff->AddTransceiver(new 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);
@@ -1518,45 +1815,50 @@ 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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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());
 
@@ -1564,591 +1866,551 @@ 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).empty());
   ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty());
 
   auto firstOffId = GetFirstLocalStreamId(*mSessionOff);
   auto firstAnsId = GetFirstLocalStreamId(*mSessionAns);
 
-  auto offererPairs = GetTrackPairsByLevel(*mSessionOff);
-  auto answererPairs = GetTrackPairsByLevel(*mSessionAns);
+  auto offererTransceivers = DeepCopy(mSessionOff->GetTransceivers());
+  auto answererTransceivers = DeepCopy(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).empty());
   ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   ASSERT_EQ(aHasStream, !GetRemoteUniqueStreamIds(*mSessionOff).empty());
   ASSERT_EQ(oHasStream, !GetRemoteUniqueStreamIds(*mSessionAns).empty());
   if (oHasStream) {
     ASSERT_STREQ(firstOffId.c_str(),
                  GetFirstLocalStreamId(*mSessionOff).c_str());
   }
   if (aHasStream) {
     ASSERT_STREQ(firstAnsId.c_str(),
                  GetFirstLocalStreamId(*mSessionAns).c_str());
 
-  auto oHasStream = HasMediaStream(mSessionOff->GetLocalTracks());
-  auto aHasStream = HasMediaStream(mSessionAns->GetLocalTracks());
-  ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
-  ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
+    auto oHasStream = HasMediaStream(GetLocalTracks(*mSessionOff));
+    auto aHasStream = HasMediaStream(GetLocalTracks(*mSessionAns));
+    ASSERT_EQ(oHasStream, !GetLocalUniqueStreamIds(*mSessionOff).empty());
+    ASSERT_EQ(aHasStream, !GetLocalUniqueStreamIds(*mSessionAns).empty());
   }
 }
 
-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(IsNull(removedTracks[0]));
+  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
+
+  ASSERT_EQ(1U, addedTracks.size());
+  ASSERT_FALSE(IsNull(addedTracks[0]));
+  ASSERT_EQ("foo", addedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ("bar", addedTracks[0].GetTrackId());
+
+  std::string answer = CreateAnswer();
+  SetLocalAnswer(answer);
+  SetRemoteAnswer(answer);
+}
+
+// The JSEP draft explicitly forbids changing the msid on an m-section, but
+// that is a new restriction that older versions of Firefox do not follow.
+TEST_P(JsepSessionTest, RenegotiationAnswererChangesMsid)
 {
   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(IsNull(removedTracks[0]));
+  ASSERT_EQ(streamId, removedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ(trackId, removedTracks[0].GetTrackId());
+
+  ASSERT_EQ(1U, addedTracks.size());
+  ASSERT_FALSE(IsNull(addedTracks[0]));
+  ASSERT_EQ("foo", addedTracks[0].GetStreamIds()[0]);
+  ASSERT_EQ("bar", addedTracks[0].GetTrackId());
+}
+
+TEST_P(JsepSessionTest, RenegotiationOffererStopsTransceiver)
+{
+  AddTracks(*mSessionOff);
+  AddTracks(*mSessionAns);
+  if (types.back() == SdpMediaSection::kApplication) {
+    return;
+  }
+
+  OfferAnswer();
+
+  std::vector<RefPtr<JsepTransceiver>> origOffererTransceivers =
+    DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers =
+    DeepCopy(mSessionAns->GetTransceivers());
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back()->Stop();
+  JsepTrack removedTrack(mSessionOff->GetTransceivers().back()->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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionAns->GetTransceivers().back()->Stop();
+  JsepTrack removedTrack(mSessionAns->GetTransceivers().back()->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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  // Avoid bundle transport side effects; don't stop the BUNDLE-tag!
+  mSessionOff->GetTransceivers().back()->Stop();
+  JsepTrack removedTrackOffer(mSessionOff->GetTransceivers().back()->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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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.UpdateTrack(
+      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.UpdateTrack(
+      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();
@@ -2157,120 +2419,118 @@ 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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  ASSERT_EQ(origOffererTransceivers.size(), origAnswererTransceivers.size());
+  for (size_t i = 0; i < origOffererTransceivers.size(); ++i) {
+    ASSERT_FALSE(IsNull(origOffererTransceivers[i]->mReceiving));
+    ASSERT_FALSE(IsNull(origAnswererTransceivers[i]->mSending));
     // These should not match since we've monkeyed with the msid
-    ASSERT_NE(offererPairs[i].mReceiving->GetStreamId(),
-              answererPairs[i].mSending->GetStreamId());
-    ASSERT_NE(offererPairs[i].mReceiving->GetTrackId(),
-              answererPairs[i].mSending->GetTrackId());
+    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);
@@ -2280,93 +2540,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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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);
@@ -2382,178 +2642,163 @@ 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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(newOffererTransceivers[0]->HasBundleLevel());
+  ASSERT_FALSE(newAnswererTransceivers[0]->HasBundleLevel());
+
+  ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+            origOffererTransceivers[0]->mTransport.get());
+  ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+            origAnswererTransceivers[0]->mTransport.get());
+
+  ASSERT_EQ(0U, newOffererTransceivers[0]->mTransport->mComponents);
+  ASSERT_EQ(0U, newAnswererTransceivers[0]->mTransport->mComponents);
+
+  for (size_t i = 1; i < newOffererTransceivers.size(); ++i) {
+    ASSERT_TRUE(newOffererTransceivers[i]->HasBundleLevel());
+    ASSERT_TRUE(newAnswererTransceivers[i]->HasBundleLevel());
+    ASSERT_EQ(1U, newOffererTransceivers[i]->BundleLevel());
+    ASSERT_EQ(1U, newAnswererTransceivers[i]->BundleLevel());
+    ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+              newOffererTransceivers[i]->mTransport.get());
+    ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+              newAnswererTransceivers[i]->mTransport.get());
   }
-
-  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<RefPtr<JsepTransceiver>> origOffererTransceivers
+    = DeepCopy(mSessionOff->GetTransceivers());
+  std::vector<RefPtr<JsepTransceiver>> origAnswererTransceivers
+    = DeepCopy(mSessionAns->GetTransceivers());
+
+  mSessionAns->GetTransceivers()[0]->Stop();
+
+  OfferAnswer(CHECK_SUCCESS);
+
+  auto newOffererTransceivers = mSessionOff->GetTransceivers();
+  auto newAnswererTransceivers = mSessionAns->GetTransceivers();
+
+  ASSERT_EQ(newOffererTransceivers.size(), newAnswererTransceivers.size());
+  ASSERT_EQ(origOffererTransceivers.size(), newOffererTransceivers.size());
+  ASSERT_EQ(origAnswererTransceivers.size(), newAnswererTransceivers.size());
+
+  ASSERT_FALSE(newOffererTransceivers[0]->HasBundleLevel());
+  ASSERT_FALSE(newAnswererTransceivers[0]->HasBundleLevel());
+
+  ASSERT_NE(newOffererTransceivers[0]->mTransport.get(),
+            origOffererTransceivers[0]->mTransport.get());
+  ASSERT_NE(newAnswererTransceivers[0]->mTransport.get(),
+            origAnswererTransceivers[0]->mTransport.get());
+
+  ASSERT_EQ(0U, newOffererTransceivers[0]->mTransport->mComponents);
+  ASSERT_EQ(0U, newAnswererTransceivers[0]->mTransport->mComponents);
+
+  for (size_t i = 1; i < newOffererTransceivers.size(); ++i) {
+    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);
 }
@@ -2872,23 +3117,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();
 
@@ -2906,23 +3151,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",
@@ -2945,20 +3190,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));
-  std::string offer = CreateOffer(Some(options));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+  mSessionOff->AddTransceiver(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+  std::string offer = CreateOffer();
 
   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,
@@ -3007,34 +3255,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<RefPtr<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));
-  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,
@@ -3075,20 +3321,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(new JsepTransceiver(
+        SdpMediaSection::kAudio, SdpDirectionAttribute::kRecvonly));
+
+  OfferAnswer(CHECK_SUCCESS);
 
   UniquePtr<Sdp> offer(Parse(
         mSessionOff->GetLocalDescription(kJsepDescriptionCurrent)));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kAudio,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3101,20 +3347,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(new JsepTransceiver(
+        SdpMediaSection::kVideo, SdpDirectionAttribute::kRecvonly));
+
+  OfferAnswer(CHECK_SUCCESS);
 
   UniquePtr<Sdp> offer(Parse(
         mSessionOff->GetLocalDescription(kJsepDescriptionCurrent)));
   ASSERT_TRUE(offer.get());
   ASSERT_EQ(1U, offer->GetMediaSectionCount());
   ASSERT_EQ(SdpMediaSection::kVideo,
             offer->GetMediaSection(0).GetMediaType());
   ASSERT_EQ(SdpDirectionAttribute::kRecvonly,
@@ -3127,23 +3373,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);
+  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
+  audio->mSending.UpdateTrack(
+      std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
+  video->mSending.UpdateTrack(
+      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,
@@ -3152,22 +3400,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);
+  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
+  audio->mSending.UpdateTrack(std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
+  video->mSending.UpdateTrack(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);
@@ -3279,22 +3528,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);
+  RefPtr<JsepTransceiver> audio(new JsepTransceiver(SdpMediaSection::kAudio));
+  audio->mSending.UpdateTrack(std::vector<std::string>(1, "offerer_stream"), "a1");
+  mSessionOff->AddTransceiver(audio);
+
+  RefPtr<JsepTransceiver> video(new JsepTransceiver(SdpMediaSection::kVideo));
+  video->mSending.UpdateTrack(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);
@@ -3361,39 +3611,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);
@@ -3430,40 +3670,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(IsNull(offerTransceivers[1]->mSending));
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mReceiving));
+  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(IsNull(answerTransceivers[1]->mSending));
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mReceiving));
+  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
@@ -3483,33 +3723,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);
@@ -3561,40 +3791,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(IsNull(offerTransceivers[1]->mSending));
+  ASSERT_FALSE(IsNull(offerTransceivers[1]->mReceiving));
+  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(IsNull(answerTransceivers[1]->mSending));
+  ASSERT_FALSE(IsNull(answerTransceivers[1]->mReceiving));
+  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);
 
@@ -3637,35 +3867,34 @@ 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());
+  RefPtr<JsepTransceiver> transceiver(session.GetTransceivers()[transceiverIndex]);
+  JsepTrack& track =
+      (direction == sdp::kSend) ? transceiver->mSending : transceiver->mReceiving;
+  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") {
@@ -3738,18 +3967,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");
@@ -3971,17 +4200,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
@@ -4022,26 +4250,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);
@@ -4141,20 +4373,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]);
   }
 }
@@ -4168,55 +4400,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(IsNull(offerTransceivers[0]->mReceiving));
+  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(IsNull(offerTransceivers[1]->mReceiving));
+  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(IsNull(offerTransceivers[2]->mReceiving));
+  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(IsNull(answerTransceivers[0]->mReceiving));
+  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(IsNull(answerTransceivers[1]->mReceiving));
+  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(IsNull(answerTransceivers[2]->mReceiving));
+  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");
@@ -4381,17 +4613,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();
 }
 
@@ -4433,20 +4665,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);
@@ -4472,18 +4706,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");
+  CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers");
+  CheckTransceiversAreBundled(*mSessionAns, "Answerer transceivers");
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionOff));
   EXPECT_EQ(1U, GetActiveTransportCount(*mSessionAns));
 }
 
 TEST_P(JsepSessionTest, TestMaxBundle)
 {
   AddTracks(*mSessionOff);
   AddTracks(*mSessionAns);
@@ -4503,18 +4737,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");
+  CheckTransceiversAreBundled(*mSessionOff, "Offerer transceivers");
+  CheckTransceiversAreBundled(*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");
@@ -4636,60 +4870,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(IsNull(removedTrack));
+
+  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(IsNull(removedTrack));
+
+  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(IsNull(removedTrack));
 
   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;
@@ -5336,19 +5558,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");
@@ -5388,19 +5610,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");
@@ -5554,9 +5776,656 @@ 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.ClearTrack();
+  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(
+      new JsepTransceiver(SdpMediaSection::MediaType::kAudio));
+
+  mSessionAns->GetTransceivers()[2]->mSending.UpdateTrack(
+      {"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(
+      new 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.ClearTrack();
+  mSessionAns->GetTransceivers()[2]->mJsDirection =
+    SdpDirectionAttribute::Direction::kRecvonly;
+
+  // We do nothing with the second audio transceiver; when we rollback, it will
+  // disappear entirely.
+
+  // This will not cause the third audio transceiver to stick around; having a
+  // track is _not_ enough to preserve it. It must have addTrack "magic"!
+  mSessionAns->GetTransceivers()[4]->mSending.UpdateTrack(
+      {"newstream"}, "newtrack");
+
+  // Create a fourth audio transceiver. Rollback will leave it alone, since we
+  // created it.
+  mSessionAns->AddTransceiver(new 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(4U, 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(IsNull(mSessionAns->GetTransceivers()[0]->mSending));
+
+  // 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(IsNull(mSessionAns->GetTransceivers()[1]->mSending));
+
+  // 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(IsNull(mSessionAns->GetTransceivers()[2]->mSending));
+
+  // Second audio transceiver should be gone.
+
+  // Third audio transceiver should also be gone.
+
+  // Fourth audio transceiver, created after SetRemote
+  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_TRUE(
+      mSessionAns->GetTransceivers()[3]->mSending.GetStreamIds().empty());
+}
+
+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(SdpMediaSection::kAudio, sdp::kSend),
+      mRecvOff(SdpMediaSection::kAudio, sdp::kRecv),
+      mSendAns(SdpMediaSection::kAudio, sdp::kSend),
+      mRecvAns(SdpMediaSection::kAudio, sdp::kRecv)
+    {}
 
     std::vector<JsepCodecDescription*>
     MakeCodecs(bool addFecCodecs = false,
                bool preferRed = false,
                bool addDtmfCodec = false) const
     {
       std::vector<JsepCodecDescription*> results;
       results.push_back(
@@ -98,101 +103,112 @@ 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(type, sdp::kSend);
+      if (type != SdpMediaSection::MediaType::kApplication) {
+        mSendOff.UpdateTrack(
+            std::vector<std::string>(1, "stream_id"), "track_id");
+      }
+      mRecvOff = JsepTrack(type, sdp::kRecv);
+      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(type, sdp::kSend);
+      if (type != SdpMediaSection::MediaType::kApplication) {
+        mSendAns.UpdateTrack(
+            std::vector<std::string>(1, "stream_id"), "track_id");
+      }
+      mRecvAns = JsepTrack(type, sdp::kRecv);
+      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 (GetAnswer().IsSending()) {
+        mSendAns.Negotiate(GetAnswer(), GetOffer());
       }
 
-      if (mRecvAns && GetAnswer().IsReceiving()) {
-        mRecvAns->Negotiate(GetAnswer(), GetOffer());
+      if (GetAnswer().IsReceiving()) {
+        mRecvAns.Negotiate(GetAnswer(), GetOffer());
       }
 
-      if (mSendOff && GetAnswer().IsReceiving()) {
-        mSendOff->Negotiate(GetAnswer(), GetAnswer());
+      if (GetAnswer().IsReceiving()) {
+        mSendOff.Negotiate(GetAnswer(), GetAnswer());
       }
 
-      if (mRecvOff && GetAnswer().IsSending()) {
-        mRecvOff->Negotiate(GetAnswer(), GetAnswer());
+      if (GetAnswer().IsSending()) {
+        mRecvOff.Negotiate(GetAnswer(), GetAnswer());
       }
     }
 
     void OfferAnswer()
     {
       CreateOffer();
       CreateAnswer();
       Negotiate();
@@ -204,32 +220,30 @@ 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_TRUE(send.GetNegotiatedDetails());
+        ASSERT_TRUE(recv.GetNegotiatedDetails());
       }
 
-      if (send && send->GetNegotiatedDetails()) {
-        ASSERT_EQ(expected, send->GetNegotiatedDetails()->GetEncodingCount());
+      if (!send.GetTrackId().empty() && send.GetNegotiatedDetails()) {
+        ASSERT_EQ(expected, send.GetNegotiatedDetails()->GetEncodingCount());
       }
 
-      if (recv && recv->GetNegotiatedDetails()) {
-        ASSERT_EQ(expected, recv->GetNegotiatedDetails()->GetEncodingCount());
+      if (!recv.GetTrackId().empty() && recv.GetNegotiatedDetails()) {
+        ASSERT_EQ(expected, recv.GetNegotiatedDetails()->GetEncodingCount());
       }
     }
 
     void CheckOffEncodingCount(size_t expected) const
     {
       CheckEncodingCount(expected, mSendOff, mRecvAns);
     }
 
@@ -309,16 +323,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 +375,45 @@ 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 (mRecvOff && mSendAns) {
-        SanityCheckTracks(*mRecvOff, *mSendAns);
-      }
+      SanityCheckTracks(mSendOff, mRecvAns);
+      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 +452,28 @@ 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(SdpMediaSection::kVideo, sdp::kSend);
+  videoTrack.UpdateTrack(std::vector<std::string>(1, "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 +496,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 +530,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 +561,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 +592,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 +639,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 +686,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 +734,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 +774,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 +805,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 +836,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 +867,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 +902,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 +964,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 +988,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 +1013,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 +1163,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 +1204,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 +1318,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 +1343,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
@@ -238,17 +238,18 @@ class TransportInfo {
   TransportLayerDtls *dtls_;
 };
 
 class TestAgent {
  public:
   TestAgent() :
       audio_config_(109, "opus", 48000, 960, 2, 64000, false),
       audio_conduit_(mozilla::AudioSessionConduit::Create()),
-      audio_pipeline_() {
+      audio_pipeline_(),
+      use_bundle_(false) {
   }
 
   static void ConnectRtp(TestAgent *client, TestAgent *server) {
     TransportInfo::InitAndConnect(client->audio_rtp_transport_,
                                   server->audio_rtp_transport_);
   }
 
   static void ConnectRtcp(TestAgent *client, TestAgent *server) {
@@ -256,22 +257,17 @@ class TestAgent {
                                   server->audio_rtcp_transport_);
   }
 
   static void ConnectBundle(TestAgent *client, TestAgent *server) {
     TransportInfo::InitAndConnect(client->bundle_transport_,
                                   server->bundle_transport_);
   }
 
-  virtual void CreatePipelines_s(bool aIsRtcpMux) = 0;
-
-  void Start() {
-    MOZ_MTLOG(ML_DEBUG, "Starting");
-    audio_pipeline_->Init();
-  }
+  virtual void CreatePipeline(bool aIsRtcpMux) = 0;
 
   void StopInt() {
   }
 
   void Stop() {
     MOZ_MTLOG(ML_DEBUG, "Stopping");
 
     if (audio_pipeline_)
@@ -324,117 +320,125 @@ class TestAgent {
   int GetAudioRtcpCountSent() {
     return audio_pipeline_->rtcp_packets_sent();
   }
 
   int GetAudioRtcpCountReceived() {
     return audio_pipeline_->rtcp_packets_received();
   }
 
+
+  void SetUsingBundle(bool use_bundle) {
+    use_bundle_ = use_bundle;
+  }
+
  protected:
   mozilla::AudioCodecConfig audio_config_;
   RefPtr<mozilla::MediaSessionConduit> audio_conduit_;
   RefPtr<FakeAudioStreamTrack> audio_stream_track_;
   // TODO(bcampen@mozilla.com): Right now this does not let us test RTCP in
   // both directions; only the sender's RTCP is sent, but the receiver should
   // be sending it too.
   RefPtr<mozilla::MediaPipeline> audio_pipeline_;
   TransportInfo audio_rtp_transport_;
   TransportInfo audio_rtcp_transport_;
   TransportInfo bundle_transport_;
+  bool use_bundle_;
 };
 
 class TestAgentSend : public TestAgent {
  public:
-  TestAgentSend() : use_bundle_(false) {
+  TestAgentSend() {
     mozilla::MediaConduitErrorCode err =
         static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get())->
         ConfigureSendMediaCodec(&audio_config_);
     EXPECT_EQ(mozilla::kMediaConduitNoError, err);
 
     audio_stream_track_ = new FakeAudioStreamTrack();
   }
 
-  virtual void CreatePipelines_s(bool aIsRtcpMux) {
+  virtual void CreatePipeline(bool aIsRtcpMux) {
 
     std::string test_pc("PC");
 
     if (aIsRtcpMux) {
       ASSERT_FALSE(audio_rtcp_transport_.flow_);
     }
 
+    RefPtr<MediaPipelineTransmit> audio_pipeline =
+      new mozilla::MediaPipelineTransmit(
+        test_pc,
+        nullptr,
+        test_utils->sts_target(),
+        false,
+        audio_stream_track_.get(),
+        audio_conduit_);
+
+    audio_pipeline->AttachToTrack();
+
+    audio_pipeline_ = audio_pipeline;
+
     RefPtr<TransportFlow> rtp(audio_rtp_transport_.flow_);
     RefPtr<TransportFlow> rtcp(audio_rtcp_transport_.flow_);
 
     if (use_bundle_) {
       rtp = bundle_transport_.flow_;
       rtcp = nullptr;
     }
 
-    audio_pipeline_ = new mozilla::MediaPipelineTransmit(
-        test_pc,
-        nullptr,
-        test_utils->sts_target(),
-        audio_stream_track_.get(),
-        "audio_track_fake_uuid",
-        1,
-        audio_conduit_,
-        rtp,
-        rtcp,
-        nsAutoPtr<MediaPipelineFilter>());
+    audio_pipeline_->UpdateTransport_m(
+        rtp, rtcp, nsAutoPtr<MediaPipelineFilter>(nullptr));
   }
-
-  void SetUsingBundle(bool use_bundle) {
-    use_bundle_ = use_bundle;
-  }
-
- private:
-  bool use_bundle_;
 };
 
 
 class TestAgentReceive : public TestAgent {
  public:
 
   TestAgentReceive() {
     std::vector<mozilla::AudioCodecConfig *> codecs;
     codecs.push_back(&audio_config_);
 
     mozilla::MediaConduitErrorCode err =
         static_cast<mozilla::AudioSessionConduit *>(audio_conduit_.get())->
         ConfigureRecvMediaCodecs(codecs);
     EXPECT_EQ(mozilla::kMediaConduitNoError, err);
   }
 
-  virtual void CreatePipelines_s(bool aIsRtcpMux) {
-      std::string test_pc("PC");
+  virtual void CreatePipeline(bool aIsRtcpMux) {
+    std::string test_pc("PC");
 
     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()));
+
+    RefPtr<TransportFlow> rtp(audio_rtp_transport_.flow_);
+    RefPtr<TransportFlow> rtcp(audio_rtcp_transport_.flow_);
+
+    if (use_bundle_) {
+      rtp = bundle_transport_.flow_;
+      rtcp = nullptr;
+    }
+
+    audio_pipeline_->UpdateTransport_m(rtp, rtcp, bundle_filter_);
   }
 
   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_;
 };
 
@@ -489,26 +493,18 @@ class MediaPipelineTest : public ::testi
     // make any sense.
     ASSERT_FALSE(!aIsRtcpMux && bundle);
 
     p2_.SetBundleFilter(initialFilter);
 
     // Setup transport flows
     InitTransports(aIsRtcpMux);
 
-    mozilla::SyncRunnable::DispatchToThread(
-      test_utils->sts_target(),
-      WrapRunnable(&p1_, &TestAgent::CreatePipelines_s, aIsRtcpMux), NS_DISPATCH_SYNC);
-
-    mozilla::SyncRunnable::DispatchToThread(
-      test_utils->sts_target(),
-      WrapRunnable(&p2_, &TestAgent::CreatePipelines_s, aIsRtcpMux), NS_DISPATCH_SYNC);
-
-    p2_.Start();
-    p1_.Start();
+    p1_.CreatePipeline(aIsRtcpMux);
+    p2_.CreatePipeline(aIsRtcpMux);
 
     if (bundle) {
       PR_Sleep(ms_until_filter_update);
 
       // Leaving refinedFilter not set implies we want to just update with
       // the other side's SSRC
       if (!refinedFilter) {
         refinedFilter = new MediaPipelineFilter;
--- a/media/webrtc/signaling/signaling.gyp
+++ b/media/webrtc/signaling/signaling.gyp
@@ -103,24 +103,25 @@
         './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/RemoteTrackSource.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
@@ -162,17 +163,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',
@@ -226,18 +229,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
@@ -112,67 +112,40 @@ 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<RefPtr<JsepTransceiver>>&
+    GetTransceivers() const = 0;
+  virtual std::vector<RefPtr<JsepTransceiver>>& GetTransceivers() = 0;
+  virtual nsresult AddTransceiver(RefPtr<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(JsepDescriptionPendingOrCurrent type)
                                           const = 0;
@@ -213,39 +186,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.GetTrackId().empty()) {
+        receiving[transceiver->mReceiving.GetMediaType()]++;
       }
 
-      if (pair.mSending) {
-        sending[pair.mSending->GetMediaType()]++;
+      if (!transceiver->mSending.GetTrackId().empty()) {
+        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
@@ -1,14 +1,15 @@
 /* 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/. */
 
 #include "signaling/src/jsep/JsepSessionImpl.h"
 
+#include <iterator>
 #include <string>
 #include <set>
 #include <bitset>
 #include <stdlib.h>
 
 #include "nspr.h"
 #include "nss.h"
 #include "pk11pub.h"
@@ -16,17 +17,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 +62,47 @@ 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(RefPtr<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.empty()) {
-        minimumSsrcCount++;
+  MOZ_MTLOG(ML_DEBUG, "Adding transceiver.");
+
+  if (transceiver->mSending.GetMediaType() != SdpMediaSection::kApplication) {
+    // Make sure we have an ssrc. Might already be set.
+    transceiver->mSending.EnsureSsrcs(mSsrcGenerator);
+    transceiver->mSending.SetCNAME(mCNAME);
+
+    // Make sure we have identifiers for send track, just in case.
+    // (man I hate this)
+    if (transceiver->mSending.GetTrackId().empty()) {
+      std::string trackId;
+      if (!mUuidGen->Generate(&trackId)) {
+        JSEP_SET_ERROR("Failed to generate UUID for JsepTrack");
+        return NS_ERROR_FAILURE;
       }
+
+      transceiver->mSending.UpdateTrack(std::vector<std::string>(), trackId);
     }
-    // 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);
-      }
-    }
+  } else {
+    transceiver->mJsDirection = SdpDirectionAttribute::Direction::kSendrecv;
   }
 
-  track->PopulateCodecs(mSupportedCodecs.values);
-
-  JsepSendingTrack strack;
-  strack.mTrack = track;
-
-  mLocalTracks.push_back(strack);
-
-  return NS_OK;
-}
+  transceiver->mSending.PopulateCodecs(mSupportedCodecs.values);
+  transceiver->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(transceiver);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetIceCredentials(const std::string& ufrag,
                                    const std::string& pwd)
 {
   mLastError.clear();
@@ -251,386 +173,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.empty()) {
-      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.empty()) {
-        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()));
 
-  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;
@@ -678,92 +306,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;
 }
@@ -786,317 +394,173 @@ JsepSessionImpl::GetRemoteDescription(Js
   mozilla::Sdp* sdp =  GetParsedRemoteDescription(type);
   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;
-}
-
-nsresult
-JsepSessionImpl::BindMatchingLocalTrackToAnswer(SdpMediaSection* msection)
-{
-  auto track = FindTrackByLevel(mLocalTracks, msection->GetLevel());
-
-  if (track == mLocalTracks.end()) {
-    track = FindUnassignedTrackByType(mLocalTracks, msection->GetMediaType());
-  }
-
-  if (track == mLocalTracks.end() &&
-      msection->GetMediaType() == SdpMediaSection::kApplication) {
-    // If we are offered datachannel, we need to play along even if no track
-    // for it has been added yet.
-    std::string streamId;
-    std::string trackId;
-
-    if (!mUuidGen->Generate(&streamId) || !mUuidGen->Generate(&trackId)) {
-      JSEP_SET_ERROR("Failed to generate UUIDs for datachannel track");
-      return NS_ERROR_FAILURE;
-    }
-
-    AddTrack(RefPtr<JsepTrack>(
-          new JsepTrack(SdpMediaSection::kApplication, streamId, trackId)));
-    track = FindUnassignedTrackByType(mLocalTracks, msection->GetMediaType());
-    MOZ_ASSERT(track != mLocalTracks.end());
-  }
-
-  if (track != mLocalTracks.end()) {
-    track->mAssignedMLine = Some(msection->GetLevel());
-    track->mTrack->AddToAnswer(
-        mPendingRemoteDescription->GetMediaSection(msection->GetLevel()),
-        msection);
-  }
-
-  return NS_OK;
-}
-
-nsresult
-JsepSessionImpl::BindMatchingRemoteTrackToAnswer(SdpMediaSection* msection)
-{
-  auto it = FindTrackByLevel(mRemoteTracks, msection->GetLevel());
-  if (it == mRemoteTracks.end()) {
-    MOZ_ASSERT(false);
-    JSEP_SET_ERROR("Failed to find remote track for local answer m-section");
-    return NS_ERROR_FAILURE;
-  }
-
-  it->mTrack->AddToAnswer(
-      mPendingRemoteDescription->GetMediaSection(msection->GetLevel()),
-      msection);
-  return NS_OK;
-}
-
-nsresult
 JsepSessionImpl::DetermineAnswererSetupRole(
     const SdpMediaSection& remoteMsection,
     SdpSetupAttribute::Role* rolep)
 {
   // Determine the role.
   // RFC 5763 says:
   //
   //   The endpoint MUST use the setup attribute defined in [RFC4145].
@@ -1149,18 +613,17 @@ JsepSessionImpl::SetLocalDescription(Jse
     if (mState != kJsepStateHaveLocalOffer) {
       JSEP_SET_ERROR("Cannot rollback local description in "
                      << GetStateStr(mState));
       return NS_ERROR_UNEXPECTED;
     }
 
     mPendingLocalDescription.reset();
     SetState(kJsepStateStable);
-    mTransports = mOldTransports;
-    mOldTransports.clear();
+    RollbackLocalOffer();
     return NS_OK;
   }
 
   switch (mState) {
     case kJsepStateStable:
       if (type != kJsepSdpOffer) {
         JSEP_SET_ERROR("Cannot set local answer in state "
                        << GetStateStr(mState));
@@ -1184,22 +647,35 @@ JsepSessionImpl::SetLocalDescription(Jse
   UniquePtr<Sdp> parsed;
   nsresult rv = ParseSdp(sdp, &parsed);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Check that content hasn't done anything unsupported with the SDP
   rv = ValidateLocalDescription(*parsed);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Create transport objects.
-  mOldTransports = mTransports; // Save in case we need to rollback
-  mTransports.clear();
-  for (size_t t = 0; t < parsed->GetMediaSectionCount(); ++t) {
-    mTransports.push_back(RefPtr<JsepTransport>(new JsepTransport));
-    InitTransport(parsed->GetMediaSection(t), mTransports[t].get());
+  if (type == kJsepSdpOffer) {
+    // For rollback
+    mOldTransceivers.clear();
+    for (const auto& transceiver : mTransceivers) {
+      mOldTransceivers.push_back(new JsepTransceiver(*transceiver));
+    }
+  }
+
+  for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+    JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+    if (!transceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << i);
+      return NS_ERROR_FAILURE;
+    }
+    transceiver->Associate(
+        parsed->GetMediaSection(i).GetAttributeList().GetMid());
+    transceiver->mTransport = new JsepTransport;
+    InitTransport(parsed->GetMediaSection(i), transceiver->mTransport.get());
   }
 
   switch (type) {
     case kJsepSdpOffer:
       rv = SetLocalDescriptionOffer(Move(parsed));
       break;
     case kJsepSdpAnswer:
     case kJsepSdpPranswer:
@@ -1243,34 +719,32 @@ JsepSessionImpl::SetLocalDescriptionAnsw
   SetState(kJsepStateStable);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetRemoteDescription(JsepSdpType type, const std::string& sdp)
 {
   mLastError.clear();
-  mRemoteTracksAdded.clear();
-  mRemoteTracksRemoved.clear();
 
   MOZ_MTLOG(ML_DEBUG, "SetRemoteDescription type=" << type << "\nSDP=\n"
                                                    << sdp);
 
   if (type == kJsepSdpRollback) {
     if (mState != kJsepStateHaveRemoteOffer) {
       JSEP_SET_ERROR("Cannot rollback remote description in "
                      << GetStateStr(mState));
       return NS_ERROR_UNEXPECTED;
     }
 
     mPendingRemoteDescription.reset();
     SetState(kJsepStateStable);
+    RollbackRemoteOffer();
 
-    // Update the remote tracks to what they were before the SetRemote
-    return SetRemoteTracksFromDescription(mCurrentRemoteDescription.get());
+    return NS_OK;
   }
 
   switch (mState) {
     case kJsepStateStable:
       if (type != kJsepSdpOffer) {
         JSEP_SET_ERROR("Cannot set remote answer in state "
                        << GetStateStr(mState));
         return NS_ERROR_UNEXPECTED;
@@ -1325,16 +799,33 @@ JsepSessionImpl::SetRemoteDescription(Js
   }
 
   std::vector<std::string> iceOptions;
   if (parsed->GetAttributeList().HasAttribute(
           SdpAttribute::kIceOptionsAttribute)) {
     iceOptions = parsed->GetAttributeList().GetIceOptions().mValues;
   }
 
+  // For rollback.
+  if (type == kJsepSdpOffer) {
+    mOldTransceivers.clear();
+    for (const auto& transceiver : mTransceivers) {
+      mOldTransceivers.push_back(new JsepTransceiver(*transceiver));
+    }
+  }
+
+  // TODO(bug 1095780): Note that we create remote tracks even when
+  // They contain only codecs we can't negotiate or other craziness.
+  rv = UpdateTransceiversFromRemoteDescription(*parsed);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+    MOZ_ASSERT(GetTransceiverForLevel(i));
+  }
+
   switch (type) {
     case kJsepSdpOffer:
       rv = SetRemoteDescriptionOffer(Move(parsed));
       break;
     case kJsepSdpAnswer:
     case kJsepSdpPranswer:
       rv = SetRemoteDescriptionAnswer(type, Move(parsed));
       break;
@@ -1361,174 +852,152 @@ JsepSessionImpl::HandleNegotiatedSession
   mIceControlling = remoteIceLite || mIsOfferer;
 
   const Sdp& answer = mIsOfferer ? *remote : *local;
 
   SdpHelper::BundledMids bundledMids;
   nsresult rv = mSdpHelper.GetBundledMids(answer, &bundledMids);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  if (mTransports.size() < local->GetMediaSectionCount()) {
-    JSEP_SET_ERROR("Fewer transports set up than m-lines");
-    MOZ_ASSERT(false);
-    return NS_ERROR_FAILURE;
-  }
-
-  for (JsepSendingTrack& trackWrapper : mLocalTracks) {
-    trackWrapper.mTrack->ClearNegotiatedDetails();
-  }
+  // Now walk through the m-sections, perform negotiation, and update the
+  // transceivers.
+  for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) {
+    JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+    if (!transceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << i);
+      return NS_ERROR_FAILURE;
+    }
 
-  for (JsepReceivingTrack& trackWrapper : mRemoteTracks) {
-    trackWrapper.mTrack->ClearNegotiatedDetails();
-  }
-
-  std::vector<JsepTrackPair> trackPairs;
-
-  // Now walk through the m-sections, make sure they match, and create
-  // track pairs that describe the media to be set up.
-  for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) {
     // Skip disabled m-sections.
     if (answer.GetMediaSection(i).GetPort() == 0) {
-      mTransports[i]->Close();
+      transceiver->mTransport->Close();
+      transceiver->Stop();
+      transceiver->Disassociate();
+      transceiver->ClearBundleLevel();
+      transceiver->mSending.SetActive(false);
+      transceiver->mReceiving.SetActive(false);
+      // Do not clear mLevel yet! That will happen on the next negotiation.
       continue;
     }
 
     // The transport details are not necessarily on the m-section we're
     // currently processing.
     size_t transportLevel = i;
     bool usingBundle = false;
     {
       const SdpMediaSection& answerMsection(answer.GetMediaSection(i));
       if (answerMsection.GetAttributeList().HasAttribute(
             SdpAttribute::kMidAttribute)) {
         if (bundledMids.count(answerMsection.GetAttributeList().GetMid())) {
           const SdpMediaSection* masterBundleMsection =
             bundledMids[answerMsection.GetAttributeList().GetMid()];
           transportLevel = masterBundleMsection->GetLevel();
           usingBundle = true;
-          if (i != transportLevel) {
-            mTransports[i]->Close();
-          }
         }
       }
     }
 
-    RefPtr<JsepTransport> transport = mTransports[transportLevel];
-
-    rv = FinalizeTransport(
-        remote->GetMediaSection(transportLevel).GetAttributeList(),
-        answer.GetMediaSection(transportLevel).GetAttributeList(),
-        transport);
+    rv = MakeNegotiatedTransceiver(remote->GetMediaSection(i),
+                                   local->GetMediaSection(i),
+                                   usingBundle,
+                                   transportLevel,
+                                   transceiver);
     NS_ENSURE_SUCCESS(rv, rv);
-
-    JsepTrackPair trackPair;
-    rv = MakeNegotiatedTrackPair(remote->GetMediaSection(i),
-                                 local->GetMediaSection(i),
-                                 transport,
-                                 usingBundle,
-                                 transportLevel,
-                                 &trackPair);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    trackPairs.push_back(trackPair);
   }
 
-  JsepTrack::SetUniquePayloadTypes(GetTracks(mRemoteTracks));
-
-  // Ouch, this probably needs some dirty bit instead of just clearing
-  // stuff for renegotiation.
-  mNegotiatedTrackPairs = trackPairs;
+  std::vector<JsepTrack*> remoteTracks;
+  for (const RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    remoteTracks.push_back(&transceiver->mReceiving);
+  }
+  JsepTrack::SetUniquePayloadTypes(remoteTracks);
 
   mGeneratedLocalDescription.reset();
 
   mNegotiations++;
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::MakeNegotiatedTrackPair(const SdpMediaSection& remote,
-                                         const SdpMediaSection& local,
-                                         const RefPtr<JsepTransport>& transport,
-                                         bool usingBundle,
-                                         size_t transportLevel,
-                                         JsepTrackPair* trackPairOut)
+JsepSessionImpl::MakeNegotiatedTransceiver(const SdpMediaSection& remote,
+                                           const SdpMediaSection& local,
+                                           bool usingBundle,
+                                           size_t transportLevel,
+                                           JsepTransceiver* transceiver)
 {
-  MOZ_ASSERT(transport->mComponents);
   const SdpMediaSection& answer = mIsOfferer ? remote : local;
 
-  bool sending;
-  bool receiving;
+  bool sending = false;
+  bool receiving = false;
 
-  if (mIsOfferer) {
-    receiving = answer.IsSending();
-    sending = answer.IsReceiving();
-  } else {
-    sending = answer.IsSending();
-    receiving = answer.IsReceiving();
+  // JS could stop the transceiver after the answer was created.
+  if (!transceiver->IsStopped()) {
+    if (mIsOfferer) {
+      receiving = answer.IsSending();
+      sending = answer.IsReceiving();
+    } else {
+      sending = answer.IsSending();
+      receiving = answer.IsReceiving();
+    }
   }
 
   MOZ_MTLOG(ML_DEBUG, "Negotiated m= line"
                           << " index=" << local.GetLevel()
                           << " type=" << local.GetMediaType()
                           << " sending=" << sending
                           << " receiving=" << receiving);
 
-  trackPairOut->mLevel = local.GetLevel();
-
-  if (local.GetMediaType() != SdpMediaSection::kApplication) {
-    MOZ_ASSERT(mRecvonlySsrcs.size() > local.GetLevel(),
-               "Failed to set the default ssrc for an active m-section");
-    trackPairOut->mRecvonlySsrc = mRecvonlySsrcs[local.GetLevel()];
-  }
-
   if (usingBundle) {
-    trackPairOut->SetBundleLevel(transportLevel);
+    transceiver->SetBundleLevel(transportLevel);
+  } else {
+    transceiver->ClearBundleLevel();
   }
 
-  auto sendTrack = FindTrackByLevel(mLocalTracks, local.GetLevel());
-  if (sendTrack != mLocalTracks.end()) {
-    sendTrack->mTrack->Negotiate(answer, remote);
-    sendTrack->mTrack->SetActive(sending);
-    trackPairOut->mSending = sendTrack->mTrack;
-  } else if (sending) {
-    JSEP_SET_ERROR("Failed to find local track for level " <<
-                   local.GetLevel()
-                   << " in local SDP. This should never happen.");
-    NS_ASSERTION(false, "Failed to find local track for level");
-    return NS_ERROR_FAILURE;
+  if (transportLevel != remote.GetLevel()) {
+    JsepTransceiver* bundleTransceiver(GetTransceiverForLevel(transportLevel));
+    if (!bundleTransceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << transportLevel);
+      return NS_ERROR_FAILURE;
+    }
+    transceiver->mTransport = bundleTransceiver->mTransport;
+  } else {
+    // Ensures we only finalize once, when we process the master level
+    nsresult rv = FinalizeTransport(
+        remote.GetAttributeList(),
+        answer.GetAttributeList(),
+        transceiver->mTransport);
+    NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  auto recvTrack = FindTrackByLevel(mRemoteTracks, local.GetLevel());
-  if (recvTrack != mRemoteTracks.end()) {
-    recvTrack->mTrack->Negotiate(answer, remote);
-    recvTrack->mTrack->SetActive(receiving);
-    trackPairOut->mReceiving = recvTrack->mTrack;
+  transceiver->mSending.SetActive(sending);
+  if (sending) {
+    transceiver->mSending.Negotiate(answer, remote);
+  }
 
-    if (receiving &&
-        trackPairOut->HasBundleLevel() &&
-        recvTrack->mTrack->GetSsrcs().empty() &&
-        recvTrack->mTrack->GetMediaType() != SdpMediaSection::kApplication) {
+  JsepTrack& recvTrack = transceiver->mReceiving;
+  recvTrack.SetActive(receiving);
+  if (receiving) {
+    recvTrack.Negotiate(answer, remote);
+
+    if (transceiver->HasBundleLevel() &&
+        recvTrack.GetSsrcs().empty() &&
+        recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
+      // TODO(bug 1105005): Once we have urn:ietf:params:rtp-hdrext:sdes:mid
+      // support, we should only fire this warning if that extension was not
+      // negotiated.
       MOZ_MTLOG(ML_ERROR, "Bundled m-section has no ssrc attributes. "
                           "This may cause media packets to be dropped.");
     }
-  } else if (receiving) {
-    JSEP_SET_ERROR("Failed to find remote track for level "
-                   << local.GetLevel()
-                   << " in remote SDP. This should never happen.");
-    NS_ASSERTION(false, "Failed to find remote track for level");
-    return NS_ERROR_FAILURE;
   }
 
-  trackPairOut->mRtpTransport = transport;
-
-  if (transport->mComponents == 2) {
+  if (transceiver->mTransport->mComponents == 2) {
     // RTCP MUX or not.
     // TODO(bug 1095743): verify that the PTs are consistent with mux.
     MOZ_MTLOG(ML_DEBUG, "RTCP-MUX is off");
-    trackPairOut->mRtcpTransport = transport;
   }
 
   return NS_OK;
 }
 
 void
 JsepSessionImpl::InitTransport(const SdpMediaSection& msection,
                                JsepTransport* transport)
@@ -1639,28 +1108,52 @@ JsepSessionImpl::CopyPreviousTransportPa
                                               offerersPreviousSdp,
                                               newOffer,
                                               i) &&
         !mRemoteIceIsRestarting
        ) {
       // If newLocal is an offer, this will be the number of components we used
       // last time, and if it is an answer, this will be the number of
       // components we've decided we're using now.
-      size_t numComponents = mTransports[i]->mComponents;
+      JsepTransceiver* transceiver(GetTransceiverForLevel(i));
+      if (!transceiver) {
+        MOZ_ASSERT(false);
+        JSEP_SET_ERROR("No transceiver for level " << i);
+        return NS_ERROR_FAILURE;
+      }
+      size_t numComponents = transceiver->mTransport->mComponents;
       nsresult rv = mSdpHelper.CopyTransportParams(
           numComponents,
           mCurrentLocalDescription->GetMediaSection(i),
           &newLocal->GetMediaSection(i));
       NS_ENSURE_SUCCESS(rv, rv);
     }
   }
 
   return NS_OK;
 }
 
+void
+JsepSessionImpl::CopyPreviousMsid(const Sdp& oldLocal, Sdp* newLocal)
+{
+  for (size_t i = 0; i < oldLocal.GetMediaSectionCount(); ++i) {
+    const SdpMediaSection& oldMsection(oldLocal.GetMediaSection(i));
+    SdpMediaSection& newMsection(newLocal->GetMediaSection(i));
+    if (oldMsection.GetAttributeList().HasAttribute(
+          SdpAttribute::kMsidAttribute) &&
+        !mSdpHelper.MsectionIsDisabled(newMsection)) {
+      // JSEP says this cannot change, no matter what is happening in JS land.
+      // It can only be updated if there is an intermediate SDP that clears the
+      // msid.
+      newMsection.GetAttributeList().SetAttribute(new SdpMsidAttributeList(
+            oldMsection.GetAttributeList().GetMsid()));
+    }
+  }
+}
+
 nsresult
 JsepSessionImpl::ParseSdp(const std::string& sdp, UniquePtr<Sdp>* parsedp)
 {
   UniquePtr<Sdp> parsed = mParser.Parse(sdp);
   if (!parsed) {
     std::string error = "Failed to parse SDP: ";
     mSdpHelper.appendSdpParseErrors(mParser.GetParseErrors(), &error);
     JSEP_SET_ERROR(error);
@@ -1710,21 +1203,21 @@ JsepSessionImpl::ParseSdp(const std::str
     if (mediaAttrs.HasAttribute(SdpAttribute::kSetupAttribute, true) &&
         mediaAttrs.GetSetup().mRole == SdpSetupAttribute::kHoldconn) {
       JSEP_SET_ERROR("Description has illegal setup attribute "
                      "\"holdconn\" in m-section at level "
                      << i);
       return NS_ERROR_INVALID_ARG;
     }
 
-    std::string streamId;
+    std::vector<std::string> streamIds;
     std::string trackId;
     nsresult rv = mSdpHelper.GetIdsFromMsid(*parsed,
                                             parsed->GetMediaSection(i),
-                                            &streamId,
+                                            &streamIds,
                                             &trackId);
 
     if (NS_SUCCEEDED(rv)) {
       if (trackIds.count(trackId)) {
         JSEP_SET_ERROR("track id:" << trackId
                        << " appears in more than one m-section at level " << i);
         return NS_ERROR_INVALID_ARG;
       }
@@ -1767,21 +1260,16 @@ JsepSessionImpl::ParseSdp(const std::str
 nsresult
 JsepSessionImpl::SetRemoteDescriptionOffer(UniquePtr<Sdp> offer)
 {
   MOZ_ASSERT(mState == kJsepStateStable);
 
   nsresult rv = ValidateOffer(*offer);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // TODO(bug 1095780): Note that we create remote tracks even when
-  // They contain only codecs we can't negotiate or other craziness.
-  rv = SetRemoteTracksFromDescription(offer.get());
-  NS_ENSURE_SUCCESS(rv, rv);
-
   mPendingRemoteDescription = Move(offer);
 
   SetState(kJsepStateHaveRemoteOffer);
   return NS_OK;
 }
 
 nsresult
 JsepSessionImpl::SetRemoteDescriptionAnswer(JsepSdpType type,
@@ -1791,98 +1279,266 @@ JsepSessionImpl::SetRemoteDescriptionAns
              mState == kJsepStateHaveRemotePranswer);
 
   mPendingRemoteDescription = Move(answer);
 
   nsresult rv = ValidateAnswer(*mPendingLocalDescription,
                                *mPendingRemoteDescription);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // TODO(bug 1095780): Note that this creates remote tracks even if
-  // we offered sendonly and other side offered sendrecv or recvonly.
-  rv = SetRemoteTracksFromDescription(mPendingRemoteDescription.get());
-  NS_ENSURE_SUCCESS(rv, rv);
-
   rv = HandleNegotiatedSession(mPendingLocalDescription,
                                mPendingRemoteDescription);
   NS_ENSURE_SUCCESS(rv, rv);
 
   mCurrentRemoteDescription = Move(mPendingRemoteDescription);
   mCurrentLocalDescription = Move(mPendingLocalDescription);
   mWasOffererLastTime = mIsOfferer;
 
   SetState(kJsepStateStable);
   return NS_OK;
 }
 
-nsresult
-JsepSessionImpl::SetRemoteTracksFromDescription(const Sdp* remoteDescription)
+static bool
+TrackIdCompare(const JsepTrack& t1, const JsepTrack& t2)
 {
-  // Unassign all remote tracks
-  for (auto& remoteTrack : mRemoteTracks) {
-    remoteTrack.mAssignedMLine.reset();
+  return t1.GetTrackId() < t2.GetTrackId();
+}
+
+JsepTransceiver*
+JsepSessionImpl::GetTransceiverForLevel(size_t level)
+{
+  for (RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    if (transceiver->HasLevel() && (transceiver->GetLevel() == level)) {
+      return transceiver.get();
+    }
   }
 
-  // This will not exist if we're rolling back the first remote description
-  if (remoteDescription) {
-    size_t numMlines = remoteDescription->GetMediaSectionCount();
-    nsresult rv;
-
-    // Iterate over the sdp, re-assigning or creating remote tracks as we go
-    for (size_t i = 0; i < numMlines; ++i) {
-      const SdpMediaSection& msection = remoteDescription->GetMediaSection(i);
-
-      if (mSdpHelper.MsectionIsDisabled(msection) || !msection.IsSending()) {
-        continue;
-      }
-
-      std::vector<JsepReceivingTrack>::iterator track;
+  return nullptr;
+}
 
-      if (msection.GetMediaType() == SdpMediaSection::kApplication) {
-        // Datachannel doesn't have msid, just search by type
-        track = FindUnassignedTrackByType(mRemoteTracks,
-                                          msection.GetMediaType());
-      } else {
-        std::string streamId;
-        std::string trackId;
-        rv = GetRemoteIds(*remoteDescription, msection, &streamId, &trackId);
-        NS_ENSURE_SUCCESS(rv, rv);
-
-        track = FindTrackByIds(mRemoteTracks, streamId, trackId);
+JsepTransceiver*
+JsepSessionImpl::GetTransceiverForLocal(size_t level)
+{
+  if (JsepTransceiver* transceiver = GetTransceiverForLevel(level)) {
+    if (WasMsectionDisabledLastNegotiation(level) && transceiver->IsStopped()) {
+      // Attempt to recycle. If this fails, the old transceiver stays put.
+      transceiver->Disassociate();
+      JsepTransceiver* newTransceiver = FindUnassociatedTransceiver(
+          transceiver->mSending.GetMediaType(), false);
+      if (newTransceiver) {
+        newTransceiver->SetLevel(level);
+        transceiver->ClearLevel();
+        return newTransceiver;
       }
+    }
 
-      if (track == mRemoteTracks.end()) {
-        RefPtr<JsepTrack> track;
-        rv = CreateReceivingTrack(i, *remoteDescription, msection, &track);
-        NS_ENSURE_SUCCESS(rv, rv);
+    return transceiver;
+  }
+
+  // There is no transceiver for |level| right now.
 
-        JsepReceivingTrack rtrack;
-        rtrack.mTrack = track;
-        rtrack.mAssignedMLine = Some(i);
-        mRemoteTracks.push_back(rtrack);
-        mRemoteTracksAdded.push_back(rtrack);
-      } else {
-        track->mAssignedMLine = Some(i);
-      }
+  for (RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    if (!transceiver->IsStopped() && !transceiver->HasLevel()) {
+      transceiver->SetLevel(level);
+      return transceiver.get();
     }
   }
 
-  // Remove any unassigned remote track ids
-  for (size_t i = 0; i < mRemoteTracks.size();) {
-    if (!mRemoteTracks[i].mAssignedMLine.isSome()) {
-      mRemoteTracksRemoved.push_back(mRemoteTracks[i]);
-      mRemoteTracks.erase(mRemoteTracks.begin() + i);
+  return nullptr;
+}
+
+JsepTransceiver*
+JsepSessionImpl::GetTransceiverForRemote(const SdpMediaSection& msection)
+{
+  size_t level = msection.GetLevel();
+  if (JsepTransceiver* transceiver = GetTransceiverForLevel(level)) {
+    if (!WasMsectionDisabledLastNegotiation(level) ||
+        !transceiver->IsStopped()) {
+      return transceiver;
+    }
+    transceiver->Disassociate();
+    transceiver->ClearLevel();
+  }
+
+  // No transceiver for |level|
+
+  JsepTransceiver* transceiver = FindUnassociatedTransceiver(
+      msection.GetMediaType(), true /*magic!*/);
+  if (transceiver) {
+    transceiver->SetLevel(level);
+    return transceiver;
+  }
+
+  // Make a new transceiver
+  RefPtr<JsepTransceiver> newTransceiver(
+      new JsepTransceiver(msection.GetMediaType(),
+                          SdpDirectionAttribute::kRecvonly));
+  newTransceiver->SetLevel(level);
+  newTransceiver->SetCreatedBySetRemote();
+  nsresult rv = AddTransceiver(newTransceiver);
+  NS_ENSURE_SUCCESS(rv, nullptr);
+  return mTransceivers.back().get();
+}
+
+nsresult
+JsepSessionImpl::UpdateTransceiversFromRemoteDescription(const Sdp& remote)
+{
+  std::vector<JsepTrack> oldRemoteTracks;
+  std::vector<JsepTrack> newRemoteTracks;
+
+  // Iterate over the sdp, updating remote tracks as we go
+  for (size_t i = 0; i < remote.GetMediaSectionCount(); ++i) {
+    const SdpMediaSection& msection = remote.GetMediaSection(i);
+
+    JsepTransceiver* transceiver(GetTransceiverForRemote(msection));
+    if (!transceiver) {
+      return NS_ERROR_FAILURE;
+    }
+
+    if (!transceiver->mReceiving.GetTrackId().empty()) {
+      oldRemoteTracks.push_back(transceiver->mReceiving);
+    }
+
+    if (!mSdpHelper.MsectionIsDisabled(msection)) {
+      transceiver->Associate(msection.GetAttributeList().GetMid());
     } else {
-      ++i;
+      transceiver->Disassociate();
+      // This cannot be rolled back.
+      transceiver->Stop();
+      continue;
+    }
+
+    // Interop workaround for endpoints that don't support msid.
+    // If the receiver has no ids, set some initial values, one way or another.
+    if (msection.IsSending() &&
+        msection.GetMediaType() != SdpMediaSection::MediaType::kApplication &&
+        transceiver->mReceiving.GetTrackId().empty()) {
+      std::vector<std::string> streamIds;
+      std::string trackId;
+
+      nsresult rv = GetRemoteIds(remote, msection, &streamIds, &trackId);
+      NS_ENSURE_SUCCESS(rv, rv);
+      transceiver->mReceiving.UpdateTrack(streamIds, trackId);
+    }
+
+    transceiver->mReceiving.UpdateRecvTrack(remote, msection);
+
+    if (!transceiver->mReceiving.GetTrackId().empty()) {
+      newRemoteTracks.push_back(transceiver->mReceiving);
     }
   }
 
+  std::sort(oldRemoteTracks.begin(), oldRemoteTracks.end(), TrackIdCompare);
+  std::sort(newRemoteTracks.begin(), newRemoteTracks.end(), TrackIdCompare);
+
+  mRemoteTracksAdded.clear();
+  mRemoteTracksRemoved.clear();
+
+  std::set_difference(
+      oldRemoteTracks.begin(),
+      oldRemoteTracks.end(),
+      newRemoteTracks.begin(),
+      newRemoteTracks.end(),
+      std::inserter(mRemoteTracksRemoved, mRemoteTracksRemoved.begin()),
+      TrackIdCompare);
+
+  std::set_difference(
+      newRemoteTracks.begin(),
+      newRemoteTracks.end(),
+      oldRemoteTracks.begin(),
+      oldRemoteTracks.end(),
+      std::inserter(mRemoteTracksAdded, mRemoteTracksAdded.begin()),
+      TrackIdCompare);
+
   return NS_OK;
 }
 
+
+bool
+JsepSessionImpl::WasMsectionDisabledLastNegotiation(size_t level) const
+{
+  const Sdp* answer(GetAnswer());
+
+  if (answer &&
+      (level < answer->GetMediaSectionCount()) &&
+      mSdpHelper.MsectionIsDisabled(answer->GetMediaSection(level))) {
+    return true;
+  }
+
+  return false;
+}
+
+JsepTransceiver*
+JsepSessionImpl::FindUnassociatedTransceiver(
+    SdpMediaSection::MediaType type, bool magic)
+{
+  // Look through transceivers that are not mapped to an m-section
+  for (RefPtr<JsepTransceiver>& transceiver : mTransceivers) {
+    if (!transceiver->IsStopped() &&
+        !transceiver->HasLevel() &&
+        (!magic || transceiver->HasAddTrackMagic()) &&
+        transceiver->mSending.GetMediaType() == type) {
+      return transceiver.get();
+    }
+  }
+
+  return nullptr;
+}
+
+void
+JsepSessionImpl::RollbackLocalOffer()
+{
+  for (size_t i = 0; i < mTransceivers.size(); ++i) {
+    RefPtr<JsepTransceiver>& transceiver(mTransceivers[i]);
+    if (i < mOldTransceivers.size()) {
+      transceiver->Rollback(*mOldTransceivers[i]);
+      continue;
+    }
+
+    RefPtr<JsepTransceiver> temp(
+        new JsepTransceiver(transceiver->mSending.GetMediaType()));
+    transceiver->Rollback(*temp);
+  }
+
+  mOldTransceivers.clear();
+}
+
+void
+JsepSessionImpl::RollbackRemoteOffer()
+{
+  for (size_t i = 0; i < mTransceivers.size(); ++i) {
+    RefPtr<JsepTransceiver>& transceiver(mTransceivers[i]);
+    if (i < mOldTransceivers.size()) {
+      transceiver->Rollback(*mOldTransceivers[i]);
+      continue;
+    }
+
+    // New transceiver!
+    if (!transceiver->HasAddTrackMagic() &&
+        transceiver->WasCreatedBySetRemote()) {
+      transceiver->Stop();
+      transceiver->Disassociate();
+      transceiver->ClearLevel();
+      transceiver->SetRemoved();
+      mTransceivers.erase(mTransceivers.begin() + i);
+      --i;
+      continue;
+    }
+
+    // Transceiver has been "touched" by addTrack; let it live, but unhook it
+    // from everything.
+    RefPtr<JsepTransceiver> temp(
+        new JsepTransceiver(transceiver->mSending.GetMediaType()));
+    transceiver->Rollback(*temp);
+  }
+
+  mOldTransceivers.clear();
+  std::swap(mRemoteTracksAdded, mRemoteTracksRemoved);
+}
+
 nsresult
 JsepSessionImpl::ValidateLocalDescription(const Sdp& description)
 {
   // TODO(bug 1095226): Better checking.
   if (!mGeneratedLocalDescription) {
     JSEP_SET_ERROR("Calling SetLocal without first calling CreateOffer/Answer"
                    " is not supported.");
     return NS_ERROR_UNEXPECTED;
@@ -2121,39 +1777,16 @@ JsepSessionImpl::ValidateAnswer(const Sd
       }
     }
   }
 
   return NS_OK;
 }
 
 nsresult
-JsepSessionImpl::CreateReceivingTrack(size_t mline,
-                                      const Sdp& sdp,
-                                      const SdpMediaSection& msection,
-                                      RefPtr<JsepTrack>* track)
-{
-  std::string streamId;
-  std::string trackId;
-
-  nsresult rv = GetRemoteIds(sdp, msection, &streamId, &trackId);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  *track = new JsepTrack(msection.GetMediaType(),
-                         streamId,
-                         trackId,
-                         sdp::kRecv);
-
-  (*track)->SetCNAME(mSdpHelper.GetCNAME(msection));
-  (*track)->PopulateCodecs(mSupportedCodecs.values);
-
-  return NS_OK;
-}
-
-nsresult
 JsepSessionImpl::CreateGenericSDP(UniquePtr<Sdp>* sdpp)
 {
   // draft-ietf-rtcweb-jsep-08 Section 5.2.1:
   //  o  The second SDP line MUST be an "o=" line, as specified in
   //     [RFC4566], Section 5.2.  The value of the <username> field SHOULD
   //     be "-".  The value of the <sess-id> field SHOULD be a
   //     cryptographically random number.  To ensure uniqueness, this
   //     number SHOULD be at least 64 bits long.  The value of the <sess-
@@ -2220,32 +1853,16 @@ JsepSessionImpl::SetupIds()
   if (!mUuidGen->Generate(&mCNAME)) {
     JSEP_SET_ERROR("Failed to generate CNAME");
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
-nsresult
-JsepSessionImpl::CreateSsrc(uint32_t* ssrc)
-{
-  do {
-    SECStatus rv = PK11_GenerateRandom(
-        reinterpret_cast<unsigned char*>(ssrc), sizeof(uint32_t));
-    if (rv != SECSuccess) {
-      JSEP_SET_ERROR("Failed to generate SSRC, error=" << rv);
-      return NS_ERROR_FAILURE;
-    }
-  } while (mSsrcs.count(*ssrc));
-  mSsrcs.insert(*ssrc);
-
-  return NS_OK;
-}
-
 void
 JsepSessionImpl::SetupDefaultCodecs()
 {
   // Supported audio codecs.
   // Per jmspeex on IRC:
   // For 32KHz sampling, 28 is ok, 32 is good, 40 should be really good
   // quality.  Note that 1-2Kbps will be wasted on a stereo Opus channel
   // with mono input compared to configuring it for mono.
@@ -2469,20 +2086,29 @@ JsepSessionImpl::UpdateDefaultCandidate(
     return NS_ERROR_UNEXPECTED;
   }
 
   if (level >= sdp->GetMediaSectionCount()) {
     return NS_OK;
   }
 
   std::string defaultRtcpCandidateAddrCopy(defaultRtcpCandidateAddr);
-  if (mState == kJsepStateStable && mTransports[level]->mComponents == 1) {
-    // We know we're doing rtcp-mux by now. Don't create an rtcp attr.
-    defaultRtcpCandidateAddrCopy = "";
-    defaultRtcpCandidatePort = 0;
+  if (mState == kJsepStateStable) {
+    JsepTransceiver* transceiver(GetTransceiverForLevel(level));
+    if (!transceiver) {
+      MOZ_ASSERT(false);
+      JSEP_SET_ERROR("No transceiver for level " << level);
+      return NS_ERROR_FAILURE;
+    }
+
+    if (transceiver->mTransport->mComponents == 1) {
+      // We know we're doing rtcp-mux by now. Don't create an rtcp attr.
+      defaultRtcpCandidateAddrCopy = "";
+      defaultRtcpCandidatePort = 0;
+    }
   }
 
   // If offer/answer isn't done, it is too early to tell whether these defaults
   // need to be applied to other m-sections.
   SdpHelper::BundledMids bundledMids;
   if (mState == kJsepStateStable) {
     nsresult rv = GetNegotiatedBundledMids(&bundledMids);
     if (NS_FAILED(rv)) {
@@ -2547,48 +2173,16 @@ JsepSessionImpl::GetNegotiatedBundledMid
 
   if (!answerSdp) {
     return NS_OK;
   }
 
   return mSdpHelper.GetBundledMids(*answerSdp, bundledMids);
 }
 
-nsresult
-JsepSessionImpl::EnableOfferMsection(SdpMediaSection* msection)
-{
-  // We assert here because adding rtcp-mux to a non-disabled m-section that
-  // did not already have rtcp-mux can cause problems.
-  MOZ_ASSERT(mSdpHelper.MsectionIsDisabled(*msection));
-
-  msection->SetPort(9);
-
-  // 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));
-  }
-
-  nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  rv = SetRecvonlySsrc(msection);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  AddExtmap(msection);
-
-  std::ostringstream osMid;
-  osMid << "sdparta_" << msection->GetLevel();
-  AddMid(osMid.str(), msection);
-
-  return NS_OK;
-}
-
 mozilla::Sdp*
 JsepSessionImpl::GetParsedLocalDescription(JsepDescriptionPendingOrCurrent type) const
 {
   if (type == kJsepDescriptionPending) {
     return mPendingLocalDescription.get();
   } else if (mPendingLocalDescription &&
              type == kJsepDescriptionPendingOrCurrent) {
     return mPendingLocalDescription.get();
@@ -2625,20 +2219,78 @@ JsepSessionImpl::Close()
 
 const std::string
 JsepSessionImpl::GetLastError() const
 {
   return mLastError;
 }
 
 bool
-JsepSessionImpl::AllLocalTracksAreAssigned() const
+JsepSessionImpl::CheckNegotiationNeeded() const
 {
-  for (const auto& localTrack : mLocalTracks) {
-    if (!localTrack.mAssignedMLine.isSome()) {
-      return false;
+  MOZ_ASSERT(mState == kJsepStateStable);
+
+  for (const auto& transceiver : mTransceivers) {
+    if (transceiver->IsStopped()) {
+      if (transceiver->IsAssociated()) {
+        MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because of "
+                  "stopped transceiver that still has a mid.");
+        return true;
+      }
+      continue;
+    }
+
+    if (!transceiver->IsAssociated()) {
+      MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because of "
+                "unassociated (but not stopped) transceiver.");
+      return true;
+    }
+
+    if (!mCurrentLocalDescription || !mCurrentRemoteDescription) {
+      MOZ_CRASH("Transceivers should not be associated if we're in stable "
+                "before the first negotiation.");
+      continue;
+    }
+
+    if (!transceiver->HasLevel()) {
+      MOZ_CRASH("Associated transceivers should always have a level.");
+      continue;
+    }
+
+    if (transceiver->mSending.GetMediaType() == SdpMediaSection::kApplication) {
+      continue;
+    }
+
+    size_t level = transceiver->GetLevel();
+    const SdpMediaSection& local =
+      mCurrentLocalDescription->GetMediaSection(level);
+    const SdpMediaSection& remote =
+      mCurrentRemoteDescription->GetMediaSection(level);
+
+    if (!local.GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute) &&
+        (transceiver->mJsDirection & sdp::kSend)) {
+      MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because of "
+                "lack of a=msid, and transceiver is sending.");
+      return true;
+    }
+
+    if (IsOfferer()) {
+      if ((local.GetDirection() != transceiver->mJsDirection) &&
+          reverse(remote.GetDirection()) != transceiver->mJsDirection) {
+        MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because "
+                  "the direction on our offer, and the remote answer, does not "
+                  "match the direction on a transceiver.");
+        return true;
+      }
+    } else if (local.GetDirection() !=
+          (transceiver->mJsDirection & reverse(remote.GetDirection()))) {
+      MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiation needed because "
+                "the direction on our answer doesn't match the direction on a "
+                "transceiver, even though the remote offer would have allowed "
+                "it.");
+      return true;
     }
   }
 
-  return true;
+  return false;
 }
 
 } // namespace mozilla
--- a/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
+++ b/media/webrtc/signaling/src/jsep/JsepSessionImpl.h
@@ -5,19 +5,19 @@
 #ifndef _JSEPSESSIONIMPL_H_
 #define _JSEPSESSIONIMPL_H_
 
 #include <set>
 #include <string>
 #include <vector>
 
 #include "signaling/src/jsep/JsepCodecDescription.h"
-#include "signaling/src/jsep/JsepTrack.h"
 #include "signaling/src/jsep/JsepSession.h"
 #include "signaling/src/jsep/JsepTrack.h"
+#include "signaling/src/jsep/SsrcGenerator.h"
 #include "signaling/src/sdp/SipccSdpParser.h"
 #include "signaling/src/sdp/SdpHelper.h"
 #include "signaling/src/common/PtrVector.h"
 
 namespace mozilla {
 
 class JsepUuidGenerator
 {
@@ -42,21 +42,16 @@ public:
         mUuidGen(Move(uuidgen)),
         mSdpHelper(&mLastError)
   {
   }
 
   // Implement JsepSession methods.
   virtual nsresult Init() override;
 
-  virtual nsresult AddTrack(const RefPtr<JsepTrack>& track) override;
-
-  virtual nsresult RemoveTrack(const std::string& streamId,
-                               const std::string& trackId) override;
-
   virtual nsresult SetIceCredentials(const std::string& ufrag,
                                      const std::string& pwd) override;
   virtual const std::string& GetUfrag() const override { return mIceUfrag; }
   virtual const std::string& GetPwd() const override { return mIcePwd; }
   nsresult SetBundlePolicy(JsepBundlePolicy policy) override;
 
   virtual bool
   RemoteIsIceLite() const override
@@ -93,40 +88,19 @@ public:
       SdpDirectionAttribute::Direction::kSendrecv) override;
 
   virtual std::vector<JsepCodecDescription*>&
   Codecs() override
   {
     return mSupportedCodecs.values;
   }
 
-  virtual nsresult ReplaceTrack(const std::string& oldStreamId,
-                                const std::string& oldTrackId,
-                                const std::string& newStreamId,
-                                const std::string& newTrackId) override;
-
-  virtual nsresult SetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      const std::vector<JsepTrack::JsConstraints>& constraints) override;
+  virtual std::vector<JsepTrack> GetRemoteTracksAdded() const override;
 
-  virtual nsresult GetParameters(
-      const std::string& streamId,
-      const std::string& trackId,
-      std::vector<JsepTrack::JsConstraints>* outConstraints) override;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetLocalTracks() const override;
-
-  virtual std::vector<RefPtr<JsepTrack>> GetRemoteTracks() const override;
-
-  virtual std::vector<RefPtr<JsepTrack>>
-    GetRemoteTracksAdded() const override;
-
-  virtual std::vector<RefPtr<JsepTrack>>
-    GetRemoteTracksRemoved() const override;
+  virtual std::vector<JsepTrack> GetRemoteTracksRemoved() const override;
 
   virtual nsresult CreateOffer(const JsepOfferOptions& options,
                                std::string* offer) override;
 
   virtual nsresult CreateAnswer(const JsepAnswerOptions& options,
                                 std::string* answer) override;
 
   virtual std::string GetLocalDescription(JsepDescriptionPendingOrCurrent type)
@@ -170,157 +144,128 @@ public:
   }
 
   virtual bool
   IsOfferer() const override
   {
     return mIsOfferer;
   }
 
-  // Access transports.
-  virtual std::vector<RefPtr<JsepTransport>>
-  GetTransports() const override
-  {
-    return mTransports;
+  virtual const std::vector<RefPtr<JsepTransceiver>>&
+    GetTransceivers() const override {
+    return mTransceivers;
   }
 
-  virtual std::vector<JsepTrackPair>
-  GetNegotiatedTrackPairs() const override
-  {
-    return mNegotiatedTrackPairs;
+  virtual std::vector<RefPtr<JsepTransceiver>>&
+    GetTransceivers() override {
+    return mTransceivers;
   }
 
-  virtual bool AllLocalTracksAreAssigned() const override;
+  virtual nsresult AddTransceiver(RefPtr<JsepTransceiver> transceiver) override;
+
+  virtual bool CheckNegotiationNeeded() const override;
 
 private:
   struct JsepDtlsFingerprint {
     std::string mAlgorithm;
     std::vector<uint8_t> mValue;
   };
 
-  struct JsepSendingTrack {
-    RefPtr<JsepTrack> mTrack;
-    Maybe<size_t> mAssignedMLine;
-  };
-
-  struct JsepReceivingTrack {
-    RefPtr<JsepTrack> mTrack;
-    Maybe<size_t> mAssignedMLine;
-  };
-
   // Non-const so it can set mLastError
   nsresult CreateGenericSDP(UniquePtr<Sdp>* sdp);
-  void AddExtmap(SdpMediaSection* msection) const;
+  void AddExtmap(SdpMediaSection* msection);
   void AddMid(const std::string& mid, SdpMediaSection* msection) const;
-  const std::vector<SdpExtmapAttributeList::Extmap>* GetRtpExtensions(
-      SdpMediaSection::MediaType type) const;
+  std::vector<SdpExtmapAttributeList::Extmap> GetRtpExtensions(
+      const SdpMediaSection& msection);
 
   void AddCommonExtmaps(const SdpMediaSection& remoteMsection,
                         SdpMediaSection* msection);
   nsresult SetupIds();
-  nsresult CreateSsrc(uint32_t* ssrc);
   void SetupDefaultCodecs();
   void SetupDefaultRtpExtensions();
   void SetState(JsepSignalingState state);
   // Non-const so it can set mLastError
   nsresult ParseSdp(const std::string& sdp, UniquePtr<Sdp>* parsedp);
   nsresult SetLocalDescriptionOffer(UniquePtr<Sdp> offer);
   nsresult SetLocalDescriptionAnswer(JsepSdpType type, UniquePtr<Sdp> answer);
   nsresult SetRemoteDescriptionOffer(UniquePtr<Sdp> offer);
   nsresult SetRemoteDescriptionAnswer(JsepSdpType type, UniquePtr<Sdp> answer);
   nsresult ValidateLocalDescription(const Sdp& description);
   nsresult ValidateRemoteDescription(const Sdp& description);
   nsresult ValidateOffer(const Sdp& offer);
   nsresult ValidateAnswer(const Sdp& offer, const Sdp& answer);
-  nsresult SetRemoteTracksFromDescription(const Sdp* remoteDescription);
-  // Non-const because we use our Uuid generator
-  nsresult CreateReceivingTrack(size_t mline,
-                                const Sdp& sdp,
-                                const SdpMediaSection& msection,
-                                RefPtr<JsepTrack>* track);
+  nsresult UpdateTransceiversFromRemoteDescription(const Sdp& remote);
+  bool WasMsectionDisabledLastNegotiation(size_t level) const;
+  JsepTransceiver* GetTransceiverForLevel(size_t level);
+  JsepTransceiver* GetTransceiverForLocal(size_t level);
+  JsepTransceiver* GetTransceiverForRemote(const SdpMediaSection& msection);
+  // The w3c and IETF specs have a lot of "magical" behavior that happens when
+  // addTrack is used. This was a deliberate design choice. Sadface.
+  JsepTransceiver* FindUnassociatedTransceiver(
+      SdpMediaSection::MediaType type, bool magic);
+  // Called for rollback of local description
+  void RollbackLocalOffer();
+  // Called for rollback of remote description
+  void RollbackRemoteOffer();
   nsresult HandleNegotiatedSession(const UniquePtr<Sdp>& local,
                                    const UniquePtr<Sdp>& remote);
   nsresult AddTransportAttributes(SdpMediaSection* msection,
                                   SdpSetupAttribute::Role dtlsRole);
   nsresult CopyPreviousTransportParams(const Sdp& oldAnswer,
                                        const Sdp& offerersPreviousSdp,
                                        const Sdp& newOffer,
                                        Sdp* newLocal);
-  nsresult SetupOfferMSections(const JsepOfferOptions& options, Sdp* sdp);
-  // Non-const so it can assign m-line index to tracks
-  nsresult SetupOfferMSectionsByType(SdpMediaSection::MediaType type,
-                                     const Maybe<size_t>& offerToReceive,
-                                     Sdp* sdp);
-  nsresult BindLocalTracks(SdpMediaSection::MediaType mediatype,
-                           Sdp* sdp);
-  nsresult BindRemoteTracks(SdpMediaSection::MediaType mediatype,
-                            Sdp* sdp,
-                            size_t* offerToReceive);
-  nsresult SetRecvAsNeededOrDisable(SdpMediaSection::MediaType mediatype,
-                                    Sdp* sdp,
-                                    size_t* offerToRecv);
-  void SetupOfferToReceiveMsection(SdpMediaSection* offer);
-  nsresult AddRecvonlyMsections(SdpMediaSection::MediaType mediatype,
-                                size_t count,
-                                Sdp* sdp);
-  nsresult AddReofferMsections(const Sdp& oldLocalSdp,
-                               const Sdp& oldAnswer,
-                               Sdp* newSdp);
+  void CopyPreviousMsid(const Sdp& oldLocal, Sdp* newLocal);
+  void EnsureMsid(Sdp* remote);
   void SetupBundle(Sdp* sdp) const;
   nsresult GetRemoteIds(const Sdp& sdp,
                         const SdpMediaSection& msection,
-                        std::string* streamId,
+                        std::vector<std::string>* streamIds,
                         std::string* trackId);
-  nsresult CreateOfferMSection(SdpMediaSection::MediaType type,
-                               SdpMediaSection::Protocol proto,
-                               SdpDirectionAttribute::Direction direction,
-                               Sdp* sdp);
-  nsresult GetFreeMsectionForSend(SdpMediaSection::MediaType type,
-                                  Sdp* sdp,
-                                  SdpMediaSection** msection);
-  nsresult CreateAnswerMSection(const JsepAnswerOptions& options,
-                                size_t mlineIndex,
+  nsresult CreateOfferMsection(const JsepOfferOptions& options,
+                               JsepTransceiver& transceiver,
+                               Sdp* local);
+  nsresult CreateAnswerMsection(const JsepAnswerOptions& options,
+                                JsepTransceiver& transceiver,
                                 const SdpMediaSection& remoteMsection,
                                 Sdp* sdp);
-  nsresult SetRecvonlySsrc(SdpMediaSection* msection);
-  nsresult BindMatchingLocalTrackToAnswer(SdpMediaSection* msection);
-  nsresult BindMatchingRemoteTrackToAnswer(SdpMediaSection* msection);
+  nsresult CreateReceivingTrack(const Sdp& sdp,
+                                const SdpMediaSection& msection,
+                                RefPtr<JsepTrack>* track);
   nsresult DetermineAnswererSetupRole(const SdpMediaSection& remoteMsection,
                                       SdpSetupAttribute::Role* rolep);
-  nsresult MakeNegotiatedTrackPair(const SdpMediaSection& remote,
-                                   const SdpMediaSection& local,
-                                   const RefPtr<JsepTransport>& transport,
-                                   bool usingBundle,
-                                   size_t transportLevel,
-                                   JsepTrackPair* trackPairOut);
+  nsresult MakeNegotiatedTransceiver(const SdpMediaSection& remote,
+                                     const SdpMediaSection& local,
+                                     bool usingBundle,
+                                     size_t transportLevel,
+                                     JsepTransceiver* transceiverOut);
   void InitTransport(const SdpMediaSection& msection, JsepTransport* transport);
 
   nsresult FinalizeTransport(const SdpAttributeList& remote,
                              const SdpAttributeList& answer,
                              const RefPtr<JsepTransport>& transport);
 
   nsresult GetNegotiatedBundledMids(SdpHelper::BundledMids* bundledMids);
 
   nsresult EnableOfferMsection(SdpMediaSection* msection);
 
   mozilla::Sdp* GetParsedLocalDescription(JsepDescriptionPendingOrCurrent type)
                                           const;
   mozilla::Sdp* GetParsedRemoteDescription(JsepDescriptionPendingOrCurrent type)
                                            const;
   const Sdp* GetAnswer() const;
 
-  std::vector<JsepSendingTrack> mLocalTracks;
-  std::vector<JsepReceivingTrack> mRemoteTracks;
   // By the most recent SetRemoteDescription
-  std::vector<JsepReceivingTrack> mRemoteTracksAdded;
-  std::vector<JsepReceivingTrack> mRemoteTracksRemoved;
-  std::vector<RefPtr<JsepTransport> > mTransports;
-  // So we can rollback
-  std::vector<RefPtr<JsepTransport> > mOldTransports;
-  std::vector<JsepTrackPair> mNegotiatedTrackPairs;
+  std::vector<JsepTrack> mRemoteTracksAdded;
+  std::vector<JsepTrack> mRemoteTracksRemoved;
+  // !!!NOT INDEXED BY LEVEL!!! These are in the order they were created in. The
+  // level mapping is done with JsepTransceiver::mLevel.
+  std::vector<RefPtr<JsepTransceiver>> mTransceivers;
+  // So we can rollback. Not as simple as just going back to the old, though...
+  std::vector<RefPtr<JsepTransceiver>> mOldTransceivers;
 
   bool mIsOfferer;
   bool mWasOffererLastTime;
   bool mIceControlling;
   std::string mIceUfrag;
   std::string mIcePwd;
   bool mRemoteIsIceLite;
   bool mRemoteIceIsRestarting;
@@ -328,30 +273,27 @@ private:
   JsepBundlePolicy mBundlePolicy;
   std::vector<JsepDtlsFingerprint> mDtlsFingerprints;
   uint64_t mSessionId;
   uint64_t mSessionVersion;
   std::vector<SdpExtmapAttributeList::Extmap> mAudioRtpExtensions;
   std::vector<SdpExtmapAttributeList::Extmap> mVideoRtpExtensions;
   UniquePtr<JsepUuidGenerator> mUuidGen;
   std::string mDefaultRemoteStreamId;
-  std::map<size_t, std::string> mDefaultRemoteTrackIdsByLevel;
   std::string mCNAME;
   // Used to prevent duplicate local SSRCs. Not used to prevent local/remote or
   // remote-only duplication, which will be important for EKT but not now.
   std::set<uint32_t> mSsrcs;
-  // When an m-section doesn't have a local track, it still needs an ssrc, which
-  // is stored here.
-  std::vector<uint32_t> mRecvonlySsrcs;
   UniquePtr<Sdp> mGeneratedLocalDescription; // Created but not set.
   UniquePtr<Sdp> mCurrentLocalDescription;
   UniquePtr<Sdp> mCurrentRemoteDescription;
   UniquePtr<Sdp> mPendingLocalDescription;
   UniquePtr<Sdp> mPendingRemoteDescription;
   PtrVector<JsepCodecDescription> mSupportedCodecs;
   std::string mLastError;
   SipccSdpParser mParser;
   SdpHelper mSdpHelper;
+  SsrcGenerator mSsrcGenerator;
 };
 
 } // namespace mozilla
 
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepTrack.cpp
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.cpp
@@ -2,21 +2,22 @@
  * 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/. */
 
 #include "signaling/src/jsep/JsepTrack.h"
 #include "signaling/src/jsep/JsepCodecDescription.h"
 #include "signaling/src/jsep/JsepTrackEncoding.h"
 
 #include <algorithm>
+#include <iostream>
 
 namespace mozilla
 {
 void
-JsepTrack::GetNegotiatedPayloadTypes(std::vector<uint16_t>* payloadTypes)
+JsepTrack::GetNegotiatedPayloadTypes(std::vector<uint16_t>* payloadTypes) const
 {
   if (!mNegotiatedDetails) {
     return;
   }
 
   for (const auto* encoding : mNegotiatedDetails->mEncodings.values) {
     GetPayloadTypes(encoding->GetCodecs(), payloadTypes);
   }
@@ -84,80 +85,113 @@ JsepTrack::EnsureNoDuplicatePayloadTypes
         codec->mDefaultPt = os.str();
         break;
       }
     }
   }
 }
 
 void
+JsepTrack::EnsureSsrcs(SsrcGenerator& ssrcGenerator)
+{
+  if (mSsrcs.empty()) {
+    uint32_t ssrc;
+    if (!ssrcGenerator.GenerateSsrc(&ssrc)) {
+      return;
+    }
+    mSsrcs.push_back(ssrc);
+  }
+}
+
+void
 JsepTrack::PopulateCodecs(const std::vector<JsepCodecDescription*>& prototype)
 {
   for (const JsepCodecDescription* prototypeCodec : prototype) {
     if (prototypeCodec->mType == mType) {
       mPrototypeCodecs.values.push_back(prototypeCodec->Clone());
       mPrototypeCodecs.values.back()->mDirection = mDirection;
     }
   }
 
   EnsureNoDuplicatePayloadTypes(&mPrototypeCodecs.values);
 }
 
 void
-JsepTrack::AddToOffer(SdpMediaSection* offer) const
+JsepTrack::AddToOffer(SsrcGenerator& ssrcGenerator, SdpMediaSection* offer)
 {
   AddToMsection(mPrototypeCodecs.values, offer);
+
   if (mDirection == sdp::kSend) {
-    AddToMsection(mJsEncodeConstraints, sdp::kSend, offer);
+    std::vector<JsConstraints> constraints;
+    if (offer->IsSending()) {
+      constraints = mJsEncodeConstraints;
+    }
+    AddToMsection(mJsEncodeConstraints, sdp::kSend, ssrcGenerator, offer);
   }
 }
 
 void
 JsepTrack::AddToAnswer(const SdpMediaSection& offer,
-                       SdpMediaSection* answer) const
+                       SsrcGenerator& ssrcGenerator,
+                       SdpMediaSection* answer)
 {
   // We do not modify mPrototypeCodecs here, since we're only creating an
   // answer. Once offer/answer concludes, we will update mPrototypeCodecs.
   PtrVector<JsepCodecDescription> codecs;
   codecs.values = GetCodecClones();
   NegotiateCodecs(offer, &codecs.values);
   if (codecs.values.empty()) {
     return;
   }
 
   AddToMsection(codecs.values, answer);
 
   if (mDirection == sdp::kSend) {
-    std::vector<JsConstraints> constraints(mJsEncodeConstraints);
-    std::vector<SdpRidAttributeList::Rid> rids;
-    GetRids(offer, sdp::kRecv, &rids);
-    NegotiateRids(rids, &constraints);
-    AddToMsection(constraints, sdp::kSend, answer);
+    std::vector<JsConstraints> constraints;
+    if (answer->IsSending()) {
+      constraints = mJsEncodeConstraints;
+      std::vector<SdpRidAttributeList::Rid> rids;
+      GetRids(offer, sdp::kRecv, &rids);
+      NegotiateRids(rids, &constraints);
+    }
+    AddToMsection(constraints, sdp::kSend, ssrcGenerator, answer);
   }
 }
 
 void
+JsepTrack::SetJsConstraints(
+    const std::vector<JsConstraints>& constraintsList)
+{
+  mJsEncodeConstraints = constraintsList;
+}
+
+void
 JsepTrack::AddToMsection(const std::vector<JsepCodecDescription*>& codecs,
-                         SdpMediaSection* msection) const
+                         SdpMediaSection* msection)
 {
   MOZ_ASSERT(msection->GetMediaType() == mType);
   MOZ_ASSERT(!codecs.empty());
 
   for (const JsepCodecDescription* codec : codecs) {
     codec->AddToMediaSection(*msection);
   }
 
-  if (mDirection == sdp::kSend) {
-    if (msection->GetMediaType() != SdpMediaSection::kApplication) {
-      msection->SetSsrcs(mSsrcs, mCNAME);
-      msection->AddMsid(mStreamId, mTrackId);
+  if (mDirection == sdp::kSend && mType != SdpMediaSection::kApplication) {
+    if (msection->IsSending()) {
+      if (mStreamIds.empty()) {
+        msection->AddMsid("-", mTrackId);
+      } else {
+        for (const std::string& streamId : mStreamIds) {
+          msection->AddMsid(streamId, mTrackId);
+          // TODO() Interop hack; older Firefox barfs if there is more than one
+          // msid. Remove when safe.
+          break;
+        }
+      }
     }
-    msection->SetSending(true);
-  } else {
-    msection->SetReceiving(true);
   }
 }
 
 // Updates the |id| values in |constraintsList| with the rid values in |rids|,
 // where necessary.
 void
 JsepTrack::NegotiateRids(const std::vector<SdpRidAttributeList::Rid>& rids,
                          std::vector<JsConstraints>* constraintsList) const
@@ -168,20 +202,42 @@ JsepTrack::NegotiateRids(const std::vect
       JsConstraints* constraints = FindConstraints("", *constraintsList);
       if (constraints) {
         constraints->rid = rid.id;
       }
     }
   }
 }
 
-/* static */
+void
+JsepTrack::UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings)
+{
+  MOZ_ASSERT(mDirection == sdp::kSend);
+  MOZ_ASSERT(mType != SdpMediaSection::kApplication);
+  size_t numSsrcs = std::max<size_t>(encodings, 1U);
+
+  // Right now, the spec does not permit changing the number of encodings after
+  // the initial creation of the sender, so we don't need to worry about things
+  // like a new encoding inserted in between two pre-existing encodings.
+  while (mSsrcs.size() < numSsrcs) {
+    uint32_t ssrc;
+    if (!ssrcGenerator.GenerateSsrc(&ssrc)) {
+      return;
+    }
+    mSsrcs.push_back(ssrc);
+  }
+
+  mSsrcs.resize(numSsrcs);
+  MOZ_ASSERT(!mSsrcs.empty());
+}
+
 void
 JsepTrack::AddToMsection(const std::vector<JsConstraints>& constraintsList,
                          sdp::Direction direction,
+                         SsrcGenerator& ssrcGenerator,
                          SdpMediaSection* msection)
 {
   UniquePtr<SdpSimulcastAttribute> simulcast(new SdpSimulcastAttribute);
   UniquePtr<SdpRidAttributeList> rids(new SdpRidAttributeList);
   for (const JsConstraints& constraints : constraintsList) {
     if (!constraints.rid.empty()) {
       SdpRidAttributeList::Rid rid;
       rid.id = constraints.rid;
@@ -193,19 +249,26 @@ JsepTrack::AddToMsection(const std::vect
       if (direction == sdp::kSend) {
         simulcast->sendVersions.push_back(version);
       } else {
         simulcast->recvVersions.push_back(version);
       }
     }
   }
 
-  if (!rids->mRids.empty()) {
+  if (rids->mRids.size() > 1) {
     msection->GetAttributeList().SetAttribute(simulcast.release());
-    msection->GetAttributeList().SetAttribute(rids.release());
+    if (!rids->mRids.empty()) {
+      msection->GetAttributeList().SetAttribute(rids.release());
+    }
+  }
+
+  if (mType != SdpMediaSection::kApplication && mDirection == sdp::kSend) {
+    UpdateSsrcs(ssrcGenerator, constraintsList.size());
+    msection->SetSsrcs(mSsrcs, mCNAME);
   }
 }
 
 void
 JsepTrack::GetRids(const SdpMediaSection& msection,
                    sdp::Direction direction,
                    std::vector<SdpRidAttributeList::Rid>* rids) const
 {
@@ -276,16 +339,17 @@ JsepTrack::CreateEncodings(
   }
 
   size_t max_streams = 1;
 
   if (!mJsEncodeConstraints.empty()) {
     max_streams = std::min(rids.size(), mJsEncodeConstraints.size());
   }
   // Drop SSRCs if less RIDs were offered than we have encoding constraints
+  // Just in case.
   if (mSsrcs.size() > max_streams) {
     mSsrcs.resize(max_streams);
   }
 
   // For each stream make sure we have an encoding, and configure
   // that encoding appropriately.
   for (size_t i = 0; i < max_streams; ++i) {
     if (i == negotiatedDetails->mEncodings.values.size()) {
@@ -329,16 +393,17 @@ CompareCodec(const JsepCodecDescription*
 }
 
 void
 JsepTrack::NegotiateCodecs(
     const SdpMediaSection& remote,
     std::vector<JsepCodecDescription*>* codecs,
     std::map<std::string, std::string>* formatChanges) const
 {
+  MOZ_ASSERT(codecs->size());
   PtrVector<JsepCodecDescription> unnegotiatedCodecs;
   std::swap(unnegotiatedCodecs.values, *codecs);
 
   // Outer loop establishes the remote side's preference
   for (const std::string& fmt : remote.GetFormats()) {
     for (size_t i = 0; i < unnegotiatedCodecs.values.size(); ++i) {
       JsepCodecDescription* codec = unnegotiatedCodecs.values[i];
       if (!codec || !codec->mEnabled || !codec->Matches(fmt, remote)) {
@@ -483,41 +548,32 @@ JsepTrack::Negotiate(const SdpMediaSecti
       }
 
       if (direction & mDirection) {
         negotiatedDetails->mExtmap[extmapAttr.extensionname] = extmapAttr;
       }
     }
   }
 
-  if (mDirection == sdp::kRecv) {
-    mSsrcs.clear();
-    if (remote.GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)) {
-      for (auto& ssrcAttr : remote.GetAttributeList().GetSsrc().mSsrcs) {
-        AddSsrc(ssrcAttr.ssrc);
-      }
-    }
-  }
-
   mNegotiatedDetails = Move(negotiatedDetails);
 }
 
 // When doing bundle, if all else fails we can try to figure out which m-line a
 // given RTP packet belongs to by looking at the payload type field. This only
 // works, however, if that payload type appeared in only one m-section.
 // We figure that out here.
 /* static */
 void
-JsepTrack::SetUniquePayloadTypes(const std::vector<RefPtr<JsepTrack>>& tracks)
+JsepTrack::SetUniquePayloadTypes(std::vector<JsepTrack*>& tracks)
 {
   // Maps to track details if no other track contains the payload type,
   // otherwise maps to nullptr.
   std::map<uint16_t, JsepTrackNegotiatedDetails*> payloadTypeToDetailsMap;
 
-  for (const RefPtr<JsepTrack>& track : tracks) {
+  for (JsepTrack* track : tracks) {
     if (track->GetMediaType() == SdpMediaSection::kApplication) {
       continue;
     }
 
     auto* details = track->GetNegotiatedDetails();
     if (!details) {
       // Can happen if negotiation fails on a track
       continue;
--- a/media/webrtc/signaling/src/jsep/JsepTrack.h
+++ b/media/webrtc/signaling/src/jsep/JsepTrack.h
@@ -6,38 +6,49 @@
 #define _JSEPTRACK_H_
 
 #include <functional>
 #include <algorithm>
 #include <string>
 #include <map>
 #include <set>
 
-#include <mozilla/RefPtr.h>
+#include <mozilla/OwningNonNull.h>
 #include <mozilla/UniquePtr.h>
 #include <mozilla/Maybe.h>
 #include "nsISupportsImpl.h"
 #include "nsError.h"
 
 #include "signaling/src/jsep/JsepTransport.h"
 #include "signaling/src/jsep/JsepTrackEncoding.h"
+#include "signaling/src/jsep/SsrcGenerator.h"
 #include "signaling/src/sdp/Sdp.h"
 #include "signaling/src/sdp/SdpAttribute.h"
 #include "signaling/src/sdp/SdpMediaSection.h"
 #include "signaling/src/common/PtrVector.h"
 
 namespace mozilla {
 
 class JsepTrackNegotiatedDetails
 {
 public:
   JsepTrackNegotiatedDetails() :
     mTias(0)
   {}
 
+  JsepTrackNegotiatedDetails(const JsepTrackNegotiatedDetails& orig) :
+    mExtmap(orig.mExtmap),
+    mUniquePayloadTypes(orig.mUniquePayloadTypes),
+    mTias(orig.mTias)
+  {
+    for (const JsepTrackEncoding* encoding : orig.mEncodings.values) {
+      mEncodings.values.push_back(new JsepTrackEncoding(*encoding));
+    }
+  }
+
   size_t
   GetEncodingCount() const
   {
     return mEncodings.values.size();
   }
 
   const JsepTrackEncoding&
   GetEncoding(size_t index) const
@@ -83,56 +94,108 @@ private:
   PtrVector<JsepTrackEncoding> mEncodings;
   uint32_t mTias; // bits per second
 };
 
 class JsepTrack
 {
 public:
   JsepTrack(mozilla::SdpMediaSection::MediaType type,
-            const std::string& streamid,
-            const std::string& trackid,
-            sdp::Direction direction = sdp::kSend)
+            sdp::Direction direction)
       : mType(type),
-        mStreamId(streamid),
-        mTrackId(trackid),
         mDirection(direction),
         mActive(false)
-  {}
+  {
+  }
+
+  void UpdateTrack(const std::vector<std::string>& streamIds,
+                   const std::string& trackId)
+  {
+    mStreamIds = streamIds;
+    mTrackId = trackId;
+  }
+
+  void ClearTrack()
+  {
+    mStreamIds.clear();
+    mTrackId.clear();
+  }
+
+  void UpdateRecvTrack(const Sdp& sdp, const SdpMediaSection& msection)
+  {
+    MOZ_ASSERT(mDirection == sdp::kRecv);
+    std::string error;
+    SdpHelper helper(&error);
+
+    if (msection.IsSending()) {
+      if (msection.GetMediaType() != SdpMediaSection::MediaType::kApplication) {
+        (void)helper.GetIdsFromMsid(sdp, msection, &mStreamIds, &mTrackId);
+      }
+    }
+
+    // We do this whether or not the track is active
+    SetCNAME(helper.GetCNAME(msection));
+    mSsrcs.clear();
+    if (msection.GetAttributeList().HasAttribute(
+          SdpAttribute::kSsrcAttribute)) {
+      for (auto& ssrcAttr : msection.GetAttributeList().GetSsrc().mSsrcs) {
+        mSsrcs.push_back(ssrcAttr.ssrc);
+      }
+    }
+  }
+
+  JsepTrack(const JsepTrack& orig)
+  {
+    *this = orig;
+  }
+
+  JsepTrack(JsepTrack&& orig) = default;
+  JsepTrack& operator=(JsepTrack&& rhs) = default;
+
+  JsepTrack& operator=(const JsepTrack& rhs)
+  {
+    if (this != &rhs) {
+      mType = rhs.mType;
+      mStreamIds = rhs.mStreamIds;
+      mTrackId = rhs.mTrackId;
+      mCNAME = rhs.mCNAME;
+      mDirection = rhs.mDirection;
+      mJsEncodeConstraints = rhs.mJsEncodeConstraints;
+      mSsrcs = rhs.mSsrcs;
+      mActive = rhs.mActive;
+
+      for (const JsepCodecDescription* codec : rhs.mPrototypeCodecs.values) {
+        mPrototypeCodecs.values.push_back(codec->Clone());
+      }
+      if (rhs.mNegotiatedDetails) {
+        mNegotiatedDetails.reset(
+          new JsepTrackNegotiatedDetails(*rhs.mNegotiatedDetails));
+      }
+    }
+    return *this;
+  }
 
   virtual mozilla::SdpMediaSection::MediaType
   GetMediaType() const
   {
     return mType;
   }
 
-  virtual const std::string&
-  GetStreamId() const
+  virtual const std::vector<std::string>&
+  GetStreamIds() const
   {
-    return mStreamId;
-  }
-
-  virtual void
-  SetStreamId(const std::string& id)
-  {
-    mStreamId = id;
+    return mStreamIds;
   }
 
   virtual const std::string&
   GetTrackId() const
   {
     return mTrackId;
   }
 
-  virtual void
-  SetTrackId(const std::string& id)
-  {
-    mTrackId = id;
-  }
-
   virtual const std::string&
   GetCNAME() const
   {
     return mCNAME;
   }
 
   virtual void
   SetCNAME(const std::string& cname)
@@ -147,23 +210,17 @@ public:
   }
 
   virtual const std::vector<uint32_t>&
   GetSsrcs() const
   {
     return mSsrcs;
   }
 
-  virtual void
-  AddSsrc(uint32_t ssrc)
-  {
-    if (mType != SdpMediaSection::kApplication) {
-      mSsrcs.push_back(ssrc);
-    }
-  }
+  virtual void EnsureSsrcs(SsrcGenerator& ssrcGenerator);
 
   bool
   GetActive() const
   {
     return mActive;
   }
 
   void
@@ -184,24 +241,28 @@ public:
 
   template <class BinaryPredicate>
   void SortCodecs(BinaryPredicate sorter)
   {
     std::stable_sort(mPrototypeCodecs.values.begin(),
                      mPrototypeCodecs.values.end(), sorter);
   }
 
-  virtual void AddToOffer(SdpMediaSection* offer) const;
+  // These two are non-const because this is where ssrcs are chosen.
+  virtual void AddToOffer(SsrcGenerator& ssrcGenerator,
+                          SdpMediaSection* offer);
   virtual void AddToAnswer(const SdpMediaSection& offer,
-                           SdpMediaSection* answer) const;
+                           SsrcGenerator& ssrcGenerator,
+                           SdpMediaSection* answer);
+
   virtual void Negotiate(const SdpMediaSection& answer,
                          const SdpMediaSection& remote);
-  static void SetUniquePayloadTypes(
-      const std::vector<RefPtr<JsepTrack>>& tracks);
-  virtual void GetNegotiatedPayloadTypes(std::vector<uint16_t>* payloadTypes);
+  static void SetUniquePayloadTypes(std::vector<JsepTrack*>& tracks);
+  virtual void GetNegotiatedPayloadTypes(
+      std::vector<uint16_t>* payloadTypes) const;
 
   // This will be set when negotiation is carried out.
   virtual const JsepTrackNegotiatedDetails*
   GetNegotiatedDetails() const
   {
     if (mNegotiatedDetails) {
       return mNegotiatedDetails.get();
     }
@@ -218,53 +279,47 @@ public:
   }
 
   virtual void
   ClearNegotiatedDetails()
   {
     mNegotiatedDetails.reset();
   }
 
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JsepTrack);
-
   struct JsConstraints
   {
     std::string rid;
     EncodingConstraints constraints;
   };
 
-  void SetJsConstraints(const std::vector<JsConstraints>& constraintsList)
-  {
-    mJsEncodeConstraints = constraintsList;
-  }
+  void SetJsConstraints(const std::vector<JsConstraints>& constraintsList);
 
   void GetJsConstraints(std::vector<JsConstraints>* outConstraintsList) const
   {
     MOZ_ASSERT(outConstraintsList);
     *outConstraintsList = mJsEncodeConstraints;
   }
 
-  static void AddToMsection(const std::vector<JsConstraints>& constraintsList,
-                            sdp::Direction direction,
-                            SdpMediaSection* msection);
+  void AddToMsection(const std::vector<JsConstraints>& constraintsList,
+                     sdp::Direction direction,
+                     SsrcGenerator& ssrcGenerator,
+                     SdpMediaSection* msection);
 
-protected:
-  virtual ~JsepTrack() {}
 
 private:
   std::vector<JsepCodecDescription*> GetCodecClones() const;
   static void EnsureNoDuplicatePayloadTypes(
       std::vector<JsepCodecDescription*>* codecs);
   static void GetPayloadTypes(
       const std::vector<JsepCodecDescription*>& codecs,
       std::vector<uint16_t>* pts);
   static void EnsurePayloadTypeIsUnique(std::set<uint16_t>* uniquePayloadTypes,
                                         JsepCodecDescription* codec);
   void AddToMsection(const std::vector<JsepCodecDescription*>& codecs,
-                     SdpMediaSection* msection) const;
+                     SdpMediaSection* msection);
   void GetRids(const SdpMediaSection& msection,
                sdp::Direction direction,
                std::vector<SdpRidAttributeList::Rid>* rids) const;
   void CreateEncodings(
       const SdpMediaSection& remote,
       const std::vector<JsepCodecDescription*>& negotiatedCodecs,
       JsepTrackNegotiatedDetails* details);
 
@@ -276,53 +331,217 @@ private:
       std::vector<JsepCodecDescription*>* codecs,
       std::map<std::string, std::string>* formatChanges = nullptr) const;
 
   JsConstraints* FindConstraints(
       const std::string& rid,
       std::vector<JsConstraints>& constraintsList) const;
   void NegotiateRids(const std::vector<SdpRidAttributeList::Rid>& rids,
                      std::vector<JsConstraints>* constraints) const;
+  void UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings);
 
-  const mozilla::SdpMediaSection::MediaType mType;
-  std::string mStreamId;
+  mozilla::SdpMediaSection::MediaType mType;
+  // These are the ids that everyone outside of JsepSession care about
+  std::vector<std::string> mStreamIds;
   std::string mTrackId;
   std::string mCNAME;
-  const sdp::Direction mDirection;
+  sdp::Direction mDirection;
   PtrVector<JsepCodecDescription> mPrototypeCodecs;
   // Holds encoding params/constraints from JS. Simulcast happens when there are
   // multiple of these. If there are none, we assume unconstrained unicast with
   // no rid.
   std::vector<JsConstraints> mJsEncodeConstraints;
   UniquePtr<JsepTrackNegotiatedDetails> mNegotiatedDetails;
   std::vector<uint32_t> mSsrcs;
   bool mActive;
 };
 
-// Need a better name for this.
-struct JsepTrackPair {
-  size_t mLevel;
-  // Is this track pair sharing a transport with another?
-  size_t mBundleLevel = SIZE_MAX; // SIZE_MAX if no bundle level
-  uint32_t mRecvonlySsrc;
-  RefPtr<JsepTrack> mSending;
-  RefPtr<JsepTrack> mReceiving;
-  RefPtr<JsepTransport> mRtpTransport;
-  RefPtr<JsepTransport> mRtcpTransport;
+class JsepTransceiver {
+  private:
+    ~JsepTransceiver() {};
+
+  public:
+    explicit JsepTransceiver(SdpMediaSection::MediaType type,
+                             SdpDirectionAttribute::Direction jsDirection =
+                                 SdpDirectionAttribute::kSendrecv) :
+      mJsDirection(jsDirection),
+      mSending(type, sdp::kSend),
+      mReceiving(type, sdp::kRecv),
+      mTransport(*(new JsepTransport)),
+      mLevel(SIZE_MAX),
+      mBundleLevel(SIZE_MAX),
+      mAddTrackMagic(false),
+      mWasCreatedBySetRemote(false),
+      mStopped(false),
+      mRemoved(false)
+    {}
+
+    // Can't use default copy c'tor because of the refcount members. Ugh.
+    JsepTransceiver(const JsepTransceiver& orig) :
+      mJsDirection(orig.mJsDirection),
+      mSending(orig.mSending),
+      mReceiving(orig.mReceiving),
+      mTransport(*(new JsepTransport(orig.mTransport))),
+      mMid(orig.mMid),
+      mLevel(orig.mLevel),
+      mBundleLevel(orig.mBundleLevel),
+      mAddTrackMagic(orig.mAddTrackMagic),
+      mWasCreatedBySetRemote(orig.mWasCreatedBySetRemote),
+      mStopped(orig.mStopped),
+      mRemoved(orig.mRemoved)
+    {}
+
+    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(JsepTransceiver);
+
+    void Rollback(JsepTransceiver& oldTransceiver)
+    {
+      *mTransport = *oldTransceiver.mTransport;
+      mLevel = oldTransceiver.mLevel;
+      mBundleLevel = oldTransceiver.mBundleLevel;
+      mReceiving = oldTransceiver.mReceiving;
+
+      // stop() caused by a disabled m-section in a remote offer cannot be
+      // rolled back.
+      if (!IsStopped()) {
+        mMid = oldTransceiver.mMid;
+      }
+    }
+
+    bool IsAssociated() const
+    {
+      return !mMid.empty();
+    }
+
+    const std::string& GetMid() const
+    {
+      MOZ_ASSERT(IsAssociated());
+      return mMid;
+    }
+
+    void Associate(const std::string& mid)
+    {
+      MOZ_ASSERT(HasLevel());
+      mMid = mid;
+    }
+
+    void Disassociate()
+    {
+      mMid.clear();
+    }
+
+    bool HasLevel() const
+    {
+      return mLevel != SIZE_MAX;
+    }
+
+    void SetLevel(size_t level)
+    {
+      MOZ_ASSERT(!HasLevel());
+      MOZ_ASSERT(!IsStopped());
+
+      mLevel = level;
+    }
+
+    void ClearLevel()
+    {
+      MOZ_ASSERT(mStopped);
+      MOZ_ASSERT(!IsAssociated());
+      mLevel = SIZE_MAX;
+    }
 
-  bool HasBundleLevel() const {
-    return mBundleLevel != SIZE_MAX;
-  }
+    size_t GetLevel() const
+    {
+      MOZ_ASSERT(HasLevel());
+      return mLevel;
+    }
+
+    void Stop()
+    {
+      mStopped = true;
+    }
+
+    bool IsStopped() const
+    {
+      return mStopped;
+    }
+
+    void SetRemoved()
+    {
+      mRemoved = true;
+    }
+
+    bool IsRemoved() const
+    {
+      return mRemoved;
+    }
+
+    bool HasBundleLevel() const {
+      return mBundleLevel != SIZE_MAX;
+    }
+
+    size_t BundleLevel() const {
+      MOZ_ASSERT(HasBundleLevel());
+      return mBundleLevel;
+    }
+
+    void SetBundleLevel(size_t aBundleLevel) {
+      MOZ_ASSERT(aBundleLevel != SIZE_MAX);
+      mBundleLevel = aBundleLevel;
+    }
+
+    void ClearBundleLevel()
+    {
+      mBundleLevel = SIZE_MAX;
+    }
 
-  size_t BundleLevel() const {
-    MOZ_ASSERT(HasBundleLevel());
-    return mBundleLevel;
-  }
+    size_t GetTransportLevel() const
+    {
+      MOZ_ASSERT(HasLevel());
+      if (HasBundleLevel()) {
+        return BundleLevel();
+      }
+      return GetLevel();
+    }
+
+    void SetAddTrackMagic()
+    {
+      mAddTrackMagic = true;
+    }
+
+    bool HasAddTrackMagic() const
+    {
+      return mAddTrackMagic;
+    }
+
+    void SetCreatedBySetRemote()
+    {
+      mWasCreatedBySetRemote = true;
+    }
 
-  void SetBundleLevel(size_t aBundleLevel) {
-    MOZ_ASSERT(aBundleLevel != SIZE_MAX);
-    mBundleLevel = aBundleLevel;
-  }
+    bool WasCreatedBySetRemote() const
+    {
+      return mWasCreatedBySetRemote;
+    }
+
+    // This is the direction JS wants. It might not actually happen.
+    SdpDirectionAttribute::Direction mJsDirection;
+
+    JsepTrack mSending;
+    JsepTrack mReceiving;
+    OwningNonNull<JsepTransport> mTransport;
+
+  private:
+    // Stuff that is not negotiated
+    std::string mMid;
+    size_t mLevel;
+    // Is this track pair sharing a transport with another?
+    size_t mBundleLevel; // SIZE_MAX if no bundle level
+    // The w3c and IETF specs have a lot of "magical" behavior that happens
+    // when addTrack is used. This was a deliberate design choice. Sadface.
+    bool mAddTrackMagic;
+    bool mWasCreatedBySetRemote;
+    bool mStopped;
+    bool mRemoved;
 };
 
 } // namespace mozilla
 
 #endif
--- a/media/webrtc/signaling/src/jsep/JsepTrackEncoding.h
+++ b/media/webrtc/signaling/src/jsep/JsepTrackEncoding.h
@@ -14,16 +14,26 @@
 namespace mozilla {
 // Represents a single encoding of a media track. When simulcast is used, there
 // may be multiple. Each encoding may have some constraints (imposed by JS), and
 // may be able to use any one of multiple codecs (JsepCodecDescription) at any
 // given time.
 class JsepTrackEncoding
 {
 public:
+  JsepTrackEncoding() = default;
+  JsepTrackEncoding(const JsepTrackEncoding& orig) :
+    mConstraints(orig.mConstraints),
+    mRid(orig.mRid)
+  {
+    for (const JsepCodecDescription* codec : orig.mCodecs.values) {
+      mCodecs.values.push_back(codec->Clone());
+    }
+  }
+
   const std::vector<JsepCodecDescription*>& GetCodecs() const
   {
     return mCodecs.values;
   }
 
   void AddCodec(const JsepCodecDescription& codec)
   {
     mCodecs.values.push_back(codec.Clone());
--- a/media/webrtc/signaling/src/jsep/JsepTransport.h
+++ b/media/webrtc/signaling/src/jsep/JsepTransport.h
@@ -82,16 +82,32 @@ private:
 class JsepTransport
 {
 public:
   JsepTransport()
       : mComponents(0)
   {
   }
 
+  JsepTransport(const JsepTransport& orig)
+  {
+    *this = orig;
+  }
+
+  JsepTransport& operator=(const JsepTransport& orig)
+  {
+    if (this != &orig) {
+      mIce.reset(orig.mIce ? new JsepIceTransport(*orig.mIce) : nullptr);
+      mDtls.reset(orig.mDtls ? new JsepDtlsTransport(*orig.mDtls) : nullptr);
+      mTransportId = orig.mTransportId;
+      mComponents = orig.mComponents;
+    }
+    return *this;
+  }
+
   void Close()
   {
     mComponents = 0;
     mTransportId.clear();
     mIce.reset();
     mDtls.reset();
   }
 
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/SsrcGenerator.cpp
@@ -0,0 +1,24 @@
+/* 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/. */
+
+#include "signaling/src/jsep/SsrcGenerator.h"
+#include "pk11pub.h"
+
+namespace mozilla {
+
+bool
+SsrcGenerator::GenerateSsrc(uint32_t* ssrc)
+{
+  do {
+    SECStatus rv = PK11_GenerateRandom(
+        reinterpret_cast<unsigned char*>(ssrc), sizeof(uint32_t));
+    if (rv != SECSuccess) {
+      return false;
+    }
+  } while (mSsrcs.count(*ssrc));
+  mSsrcs.insert(*ssrc);
+
+  return true;
+}
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/jsep/SsrcGenerator.h
@@ -0,0 +1,20 @@
+/* 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/. */
+
+#ifndef _SSRCGENERATOR_H_
+#define _SSRCGENERATOR_H_
+
+#include <set>
+
+namespace mozilla {
+class SsrcGenerator {
+  public:
+    bool GenerateSsrc(uint32_t* ssrc);
+  private:
+    std::set<uint32_t> mSsrcs;
+};
+} // namespace mozilla
+
+#endif // _SSRCGENERATOR_H_
+
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
@@ -931,18 +931,23 @@ WebrtcAudioConduit::SendRtp(const uint8_
     }
   }
   ReentrantMonitorAutoEnter enter(mTransportMonitor);
   // XXX(pkerr) - the PacketOptions are being ignored. This parameter was added along
   // with the Call API update in the webrtc.org codebase.
   // The only field in it is the packet_id, which is used when the header
   // extension for TransportSequenceNumber is being used, which we don't.
   (void)options;
-  if(mTransmitterTransport &&
-     (mTransmitterTransport->SendRtpPacket(data, len) == NS_OK))
+
+  if (!mTransmitterTransport) {
+    // Not set yet, don't complain
+    return false;
+  }
+
+  if(mTransmitterTransport->SendRtpPacket(data, len) == NS_OK)
   {
     CSFLogDebug(logTag, "%s Sent RTP Packet ", __FUNCTION__);
     return true;
   }
   CSFLogError(logTag, "%s RTP Packet Send Failed ", __FUNCTION__);
   return false;
 }
 
@@ -954,16 +959,21 @@ WebrtcAudioConduit::SendRtcp(const uint8
               __FUNCTION__,
               (unsigned long) len,
               static_cast<unsigned>(data[1]));
 
   // We come here if we have only one pipeline/conduit setup,
   // such as for unidirectional streams.
   // We also end up here if we are receiving
   ReentrantMonitorAutoEnter enter(mTransportMonitor);
+  if (!mTransmitterTransport && !mReceiverTransport) {
+    // Not set yet, don't complain
+    return false;
+  }
+
   if(mReceiverTransport &&
      mReceiverTransport->SendRtcpPacket(data, len) == NS_OK)
   {
     // Might be a sender report, might be a receiver report, we don't know.
     CSFLogDebug(logTag, "%s Sent RTCP Packet ", __FUNCTION__);
     return true;
   }
   if (mTransmitterTransport &&
--- a/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
+++ b/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
@@ -325,17 +325,17 @@ public:
                           mUsingTmmbr(false),
                           mUsingFEC(false) {}
 
   virtual ~VideoSessionConduit() {}
 
   virtual Type type() const { return VIDEO; }
 
   /**
-  * Adds negotiated RTP extensions
+  * Sets negotiated RTP extensions
   * XXX Move to MediaSessionConduit
   */
   virtual void SetLocalRTPExtensions(bool aIsSend,
                                      const std::vector<webrtc::RtpExtension>& extensions) = 0;
 
   /**
   * Returns the negotiated RTP extensions
   */
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
@@ -326,16 +326,18 @@ WebrtcVideoConduit::SetLocalRTPExtension
 std::vector<webrtc::RtpExtension>
 WebrtcVideoConduit::GetLocalRTPExtensions(bool aIsSend) const
 {
   return aIsSend ? mSendStreamConfig.rtp.extensions : mRecvStreamConfig.rtp.extensions;
 }
 
 bool WebrtcVideoConduit::SetLocalSSRCs(const std::vector<unsigned int> & aSSRCs)
 {
+  MOZ_RELEASE_ASSERT(!aSSRCs.empty());
+
   // Special case: the local SSRCs are the same - do nothing.
   if (mSendStreamConfig.rtp.ssrcs == aSSRCs) {
     return true;
   }
 
   // Update the value of the ssrcs in the config structure.
   mSendStreamConfig.rtp.ssrcs = aSSRCs;
 
@@ -728,17 +730,18 @@ WebrtcVideoConduit::ConfigureSendMediaCo
   // SetSendCodec()
 
   if (mSendingWidth != 0) {
     // We're already in a call and are reconfiguring (perhaps due to
     // ReplaceTrack).
     bool resolutionChanged;
     {
       MutexAutoLock lock(mCodecMutex);
-      resolutionChanged = !mCurSendCodecConfig->ResolutionEquals(*codecConfig);
+      resolutionChanged = mCurSendCodecConfig &&
+        !mCurSendCodecConfig->ResolutionEquals(*codecConfig);
     }
 
     if (resolutionChanged) {
       // We're already in a call and due to renegotiation an encoder parameter
       // that requires reconfiguration has changed. Resetting these members
       // triggers reconfig on the next frame.
       mLastWidth = 0;
       mLastHeight = 0;
@@ -849,21 +852,17 @@ WebrtcVideoConduit::ConfigureSendMediaCo
 
 bool
 WebrtcVideoConduit::SetRemoteSSRC(unsigned int ssrc)
 {
   CSFLogDebug(logTag, "%s: SSRC %u (0x%x)", __FUNCTION__, ssrc, ssrc);
   mRecvStreamConfig.rtp.remote_ssrc = ssrc;
 
   unsigned int current_ssrc;
-  if (!GetRemoteSSRC(&current_ssrc)) {
-    return false;
-  }
-
-  if (current_ssrc == ssrc) {
+  if (GetRemoteSSRC(&current_ssrc) && (current_ssrc == ssrc)) {
     return true;
   }
 
   bool wasReceiving = mEngineReceiving;
   if (StopReceiving() != kMediaConduitNoError) {
     return false;
   }
 
@@ -1942,16 +1941,17 @@ WebrtcVideoConduit::OnSinkWantsChanged(
 MediaConduitErrorCode
 WebrtcVideoConduit::SendVideoFrame(webrtc::VideoFrame& frame)
 {
   // XXX Google uses a "timestamp_aligner" to translate timestamps from the
   // camera via TranslateTimestamp(); we should look at doing the same.  This
   // avoids sampling error when capturing frames, but google had to deal with some
   // broken cameras, include Logitech c920's IIRC.
 
+  MOZ_RELEASE_ASSERT(!mSendStreamConfig.rtp.ssrcs.empty());
   CSFLogVerbose(logTag, "%s (send SSRC %u (0x%x))", __FUNCTION__,
               mSendStreamConfig.rtp.ssrcs.front(), mSendStreamConfig.rtp.ssrcs.front());
   // See if we need to recalculate what we're sending.
   // Don't compute mSendingWidth/Height, since those may not be the same as the input.
   {
     MutexAutoLock lock(mCodecMutex);
     if (mInReconfig) {
       // Waiting for it to finish
@@ -2108,16 +2108,17 @@ WebrtcVideoConduit::ReceivedRTPPacket(co
       return kMediaConduitRTPProcessingFailed;
     }
     NS_DispatchToMainThread(media::NewRunnableFrom([self, thread, ssrc]() mutable {
           // Normally this is done in CreateOrUpdateMediaPipeline() for
           // initial creation and renegotiation, but here we're rebuilding the
           // Receive channel at a lower level.  This is needed whenever we're
           // creating a GMPVideoCodec (in particular, H264) so it can communicate
           // errors to the PC.
+          MOZ_ASSERT(!self->mPCHandle.empty());
           WebrtcGmpPCHandleSetter setter(self->mPCHandle);
           self->SetRemoteSSRC(ssrc); // this will likely re-create the VideoReceiveStream
           // We want to unblock the queued packets on the original thread
           thread->Dispatch(media::NewRunnableFrom([self, ssrc]() mutable {
                 if (ssrc == self->mRecvSSRC) {
                   // SSRC is set; insert queued packets
                   for (auto& packet : self->mQueuedPackets) {
                     CSFLogDebug(logTag, "Inserting queued packets: seq# %u, Len %d ",
@@ -2181,16 +2182,17 @@ WebrtcVideoConduit::StopTransmitting()
     mEngineTransmitting = false;
   }
   return kMediaConduitNoError;
 }
 
 MediaConduitErrorCode
 WebrtcVideoConduit::StartTransmitting()
 {
+  MOZ_RELEASE_ASSERT(!mSendStreamConfig.rtp.ssrcs.empty());
   if (mEngineTransmitting) {
     return kMediaConduitNoError;
   }
 
   CSFLogDebug(logTag, "%s Attemping to start... ", __FUNCTION__);
   {
     // Start Transmitting on the video engine
     MutexAutoLock lock(mCodecMutex);
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.h
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.h
@@ -74,17 +74,17 @@ public:
   static const uint32_t kDefaultStartBitrate_bps;
   /* Default maximum bitrate for video streams. */
   static const uint32_t kDefaultMaxBitrate_bps;
 
   //VoiceEngine defined constant for Payload Name Size.
   static const unsigned int CODEC_PLNAME_SIZE;
 
   /**
-  * Add rtp extensions to the the VideoSendStream
+  * Set rtp extensions on the VideoSendStream
   * TODO(@@NG) promote this the MediaConduitInterface when the VoE rework
   * hits Webrtc.org.
   */
   void SetLocalRTPExtensions(bool aIsSend,
                              const std::vector<webrtc::RtpExtension>& extensions) override;
   std::vector<webrtc::RtpExtension> GetLocalRTPExtensions(bool aIsSend) const override;
 
   /**
--- a/media/webrtc/signaling/src/mediapipeline/MediaPipeline.cpp
+++ b/media/webrtc/signaling/src/mediapipeline/MediaPipeline.cpp
@@ -20,16 +20,17 @@
 #include "ImageTypes.h"
 #include "ImageContainer.h"
 #include "DOMMediaStream.h"
 #include "MediaStreamTrack.h"
 #include "MediaStreamListener.h"
 #include "MediaStreamVideoSink.h"
 #include "VideoUtils.h"
 #include "VideoStreamTrack.h"
+#include "MediaEngine.h"
 
 #include "nsError.h"
 #include "AudioSegment.h"
 #include "MediaSegment.h"
 #include "MediaPipelineFilter.h"
 #include "RtpLogger.h"
 #include "databuffer.h"
 #include "transportflow.h"
@@ -555,83 +556,63 @@ protected:
 };
 
 static char kDTLSExporterLabel[] = "EXTRACTOR-dtls_srtp";
 
 MediaPipeline::MediaPipeline(const std::string& pc,
                              Direction direction,
                              nsCOMPtr<nsIEventTarget> main_thread,
                              nsCOMPtr<nsIEventTarget> sts_thread,
-                             const std::string& track_id,
-                             int level,
-                             RefPtr<MediaSessionConduit> conduit,
-                             RefPtr<TransportFlow> rtp_transport,
-                             RefPtr<TransportFlow> rtcp_transport,
-                             nsAutoPtr<MediaPipelineFilter> filter)
+                             RefPtr<MediaSessionConduit> conduit)
   : direction_(direction),
-    track_id_(track_id),
-    level_(level),
     conduit_(conduit),
-    rtp_(rtp_transport, rtcp_transport ? RTP : MUX),
-    rtcp_(rtcp_transport ? rtcp_transport : rtp_transport,
-          rtcp_transport ? RTCP : MUX),
+    rtp_(nullptr, RTP),
+    rtcp_(nullptr, RTCP),
     main_thread_(main_thread),
     sts_thread_(sts_thread),
     rtp_packets_sent_(0),
     rtcp_packets_sent_(0),
     rtp_packets_received_(0),
     rtcp_packets_received_(0),
     rtp_bytes_sent_(0),
     rtp_bytes_received_(0),
     pc_(pc),
     description_(),
-    filter_(filter),
     rtp_parser_(webrtc::RtpHeaderParser::Create()){
-  // To indicate rtcp-mux rtcp_transport should be nullptr.
-  // Therefore it's an error to send in the same flow for
-  // both rtp and rtcp.
-  MOZ_ASSERT(rtp_transport != rtcp_transport);
-
   // PipelineTransport() will access this->sts_thread_; moved here for safety
   transport_ = new PipelineTransport(this);
-}
-
-MediaPipeline::~MediaPipeline() {
-  ASSERT_ON_THREAD(main_thread_);
-  MOZ_MTLOG(ML_INFO, "Destroying MediaPipeline: " << description_);
-}
-
-nsresult MediaPipeline::Init() {
-  ASSERT_ON_THREAD(main_thread_);
 
   if (direction_ == RECEIVE) {
     conduit_->SetReceiverTransport(transport_);
   } else {
     conduit_->SetTransmitterTransport(transport_);
   }
+}
+
+MediaPipeline::~MediaPipeline() {
+  MOZ_MTLOG(ML_INFO, "Destroying MediaPipeline: " << description_);
+  // MediaSessionConduit insists that it be released on main.
+  RUN_ON_THREAD(main_thread_, WrapRelease(conduit_.forget()),
+      NS_DISPATCH_NORMAL);
+}
+
+void
+MediaPipeline::Shutdown_m()
+{
+  MOZ_MTLOG(ML_INFO, description_ << " in " << __FUNCTION__);
+
+  ShutdownMedia_m();
 
   RUN_ON_THREAD(sts_thread_,
                 WrapRunnable(
                     RefPtr<MediaPipeline>(this),
-                    &MediaPipeline::Init_s),
+                    &MediaPipeline::DetachTransport_s),
                 NS_DISPATCH_NORMAL);
-
-  return NS_OK;
 }
 
-nsresult MediaPipeline::Init_s() {
-  ASSERT_ON_THREAD(sts_thread_);
-
-  return AttachTransport_s();
-}
-
-
-// Disconnect us from the transport so that we can cleanly destruct the
-// pipeline on the main thread.  ShutdownMedia_m() must have already been
-// called
 void
 MediaPipeline::DetachTransport_s()
 {
   ASSERT_ON_THREAD(sts_thread_);
 
   disconnect_all();
   transport_->Detach();
   rtp_.Detach();
@@ -658,54 +639,51 @@ MediaPipeline::AttachTransport_s()
   }
 
   transport_->Attach(this);
 
   return NS_OK;
 }
 
 void
-MediaPipeline::UpdateTransport_m(int level,
-                                 RefPtr<TransportFlow> rtp_transport,
+MediaPipeline::UpdateTransport_m(RefPtr<TransportFlow> rtp_transport,
                                  RefPtr<TransportFlow> rtcp_transport,
                                  nsAutoPtr<MediaPipelineFilter> filter)
 {
   RUN_ON_THREAD(sts_thread_,
                 WrapRunnable(
-                    this,
+                    RefPtr<MediaPipeline>(this),
                     &MediaPipeline::UpdateTransport_s,
-                    level,
                     rtp_transport,
                     rtcp_transport,
                     filter),
                 NS_DISPATCH_NORMAL);
 }
 
 void
-MediaPipeline::UpdateTransport_s(int level,
-                                 RefPtr<TransportFlow> rtp_transport,
+MediaPipeline::UpdateTransport_s(RefPtr<TransportFlow> rtp_transport,
                                  RefPtr<TransportFlow> rtcp_transport,
                                  nsAutoPtr<MediaPipelineFilter> filter)
 {
   bool rtcp_mux = false;
   if (!rtcp_transport) {
     rtcp_transport = rtp_transport;
     rtcp_mux = true;
   }
 
   if ((rtp_transport != rtp_.transport_) ||
       (rtcp_transport != rtcp_.transport_)) {
     DetachTransport_s();
-    rtp_ = TransportInfo(rtp_transport, rtcp_mux ? MUX : RTP);
-    rtcp_ = TransportInfo(rtcp_transport, rtcp_mux ? MUX : RTCP);
-    AttachTransport_s();
+    if (rtp_transport && rtcp_transport) {
+      rtp_ = TransportInfo(rtp_transport, rtcp_mux ? MUX : RTP);
+      rtcp_ = TransportInfo(rtcp_transport, rtcp_mux ? MUX : RTCP);
+      AttachTransport_s();
+    }
   }
 
-  level_ = level;
-
   if (filter_ && filter) {
     // Use the new filter, but don't forget any remote SSRCs that we've learned
     // by receiving traffic.
     filter_->Update(*filter);
   } else {
     filter_ = filter;
   }
 }
@@ -1290,24 +1268,24 @@ public:
 
   void OnVideoFrameConverted(unsigned char* aVideoFrame,
                              unsigned int aVideoFrameLength,
                              unsigned short aWidth,
                              unsigned short aHeight,
                              VideoType aVideoType,
                              uint64_t aCaptureTime)
   {
-    MOZ_ASSERT(conduit_->type() == MediaSessionConduit::VIDEO);
+    MOZ_RELEASE_ASSERT(conduit_->type() == MediaSessionConduit::VIDEO);
     static_cast<VideoSessionConduit*>(conduit_.get())->SendVideoFrame(
       aVideoFrame, aVideoFrameLength, aWidth, aHeight, aVideoType, aCaptureTime);
   }
 
   void OnVideoFrameConverted(webrtc::VideoFrame& aVideoFrame)
   {
-    MOZ_ASSERT(conduit_->type() == MediaSessionConduit::VIDEO);
+    MOZ_RELEASE_ASSERT(conduit_->type() == MediaSessionConduit::VIDEO);
     static_cast<VideoSessionConduit*>(conduit_.get())->SendVideoFrame(aVideoFrame);
   }
 
   // Implement MediaStreamTrackListener
   void NotifyQueuedChanges(MediaStreamGraph* aGraph,
                            StreamTime aTrackOffset,
                            const MediaSegment& aQueuedMedia) override;
 
@@ -1412,28 +1390,26 @@ protected:
   RefPtr<PipelineListener> listener_;
   Mutex mutex_;
 };
 
 MediaPipelineTransmit::MediaPipelineTransmit(
     const std::string& pc,
     nsCOMPtr<nsIEventTarget> main_thread,
     nsCOMPtr<nsIEventTarget> sts_thread,
+    bool is_video,
     dom::MediaStreamTrack* domtrack,
-    const std::string& track_id,
-    int level,
-    RefPtr<MediaSessionConduit> conduit,
-    RefPtr<TransportFlow> rtp_transport,
-    RefPtr<TransportFlow> rtcp_transport,
-    nsAutoPtr<MediaPipelineFilter> filter) :
-  MediaPipeline(pc, TRANSMIT, main_thread, sts_thread, track_id, level,
-                conduit, rtp_transport, rtcp_transport, filter),
+    RefPtr<MediaSessionConduit> conduit) :
+  MediaPipeline(pc, TRANSMIT, main_thread, sts_thread, conduit),
   listener_(new PipelineListener(conduit)),
-  domtrack_(domtrack)
+  is_video_(is_video),
+  domtrack_(domtrack),
+  track_attached_(false)
 {
+  SetDescription();
   if (!IsVideo()) {
     audio_processing_ = MakeAndAddRef<AudioProxyThread>(static_cast<AudioSessionConduit*>(conduit.get()));
     listener_->SetAudioProxy(audio_processing_);
   }
   else { // Video
     // For video we send frames to an async VideoFrameConverter that calls
     // back to a VideoFrameFeeder that feeds I420 frames to VideoConduit.
 
@@ -1446,32 +1422,64 @@ MediaPipelineTransmit::MediaPipelineTran
   }
 }
 
 MediaPipelineTransmit::~MediaPipelineTransmit()
 {
   if (feeder_) {
     feeder_->Detach();
   }
+
+  MOZ_ASSERT(!domtrack_);
 }
 
-nsresult MediaPipelineTransmit::Init() {
-  AttachToTrack(track_id_);
-
-  return MediaPipeline::Init();
-}
-
-void MediaPipelineTransmit::AttachToTrack(const std::string& track_id) {
-  ASSERT_ON_THREAD(main_thread_);
-
+void MediaPipelineTransmit::SetDescription() {
   description_ = pc_ + "| ";
   description_ += conduit_->type() == MediaSessionConduit::AUDIO ?
       "Transmit audio[" : "Transmit video[";
+
+  if (!domtrack_) {
+    description_ += "no track]";
+    return;
+  }
+
+  nsString nsTrackId;
+  domtrack_->GetId(nsTrackId);
+  std::string track_id(NS_ConvertUTF16toUTF8(nsTrackId).get());
   description_ += track_id;
   description_ += "]";
+}
+
+void MediaPipelineTransmit::DetachFromTrack() {
+  ASSERT_ON_THREAD(main_thread_);
+
+  if (!domtrack_ || !track_attached_) {
+    return;
+  }
+
+  track_attached_ = false;
+
+  if (domtrack_->AsAudioStreamTrack()) {
+    domtrack_->RemoveDirectListener(listener_);
+    domtrack_->RemoveListener(listener_);
+  } else if (VideoStreamTrack* video = domtrack_->AsVideoStreamTrack()) {
+    video->RemoveVideoOutput(listener_);
+  } else {
+    MOZ_ASSERT(false, "Unknown track type");
+  }
+}
+
+void MediaPipelineTransmit::AttachToTrack() {
+  ASSERT_ON_THREAD(main_thread_);
+
+  if (!domtrack_ || track_attached_) {
+    return;
+  }
+
+  track_attached_ = true;
 
   // TODO(ekr@rtfm.com): Check for errors
   MOZ_MTLOG(ML_DEBUG, "Attaching pipeline to track "
             << static_cast<void *>(domtrack_) << " conduit type=" <<
             (conduit_->type() == MediaSessionConduit::AUDIO ?"audio":"video"));
 
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
   // With full duplex we don't risk having audio come in late to the MSG
@@ -1496,17 +1504,17 @@ void MediaPipelineTransmit::AttachToTrac
   } else {
     MOZ_ASSERT(false, "Unknown track type");
   }
 }
 
 bool
 MediaPipelineTransmit::IsVideo() const
 {
-  return !!domtrack_->AsVideoStreamTrack();
+  return is_video_;
 }
 
 void MediaPipelineTransmit::UpdateSinkIdentity_m(MediaStreamTrack* track,
                                                  nsIPrincipal* principal,
                                                  const PeerIdentity* sinkIdentity) {
   ASSERT_ON_THREAD(main_thread_);
 
   if (track != nullptr && track != domtrack_) {
@@ -1530,59 +1538,58 @@ void MediaPipelineTransmit::UpdateSinkId
 
   listener_->SetEnabled(enableTrack);
 }
 
 void
 MediaPipelineTransmit::DetachMedia()
 {
   ASSERT_ON_THREAD(main_thread_);
-  if (domtrack_) {
-    if (domtrack_->AsAudioStreamTrack()) {
-      domtrack_->RemoveDirectListener(listener_);
-      domtrack_->RemoveListener(listener_);
-    } else if (VideoStreamTrack* video = domtrack_->AsVideoStreamTrack()) {
-      video->RemoveVideoOutput(listener_);
-    } else {
-      MOZ_ASSERT(false, "Unknown track type");
-    }
-    domtrack_ = nullptr;
-  }
+
+  DetachFromTrack();
+  domtrack_ = nullptr;
   // Let the listener be destroyed with the pipeline (or later).
 }
 
 nsresult MediaPipelineTransmit::TransportReady_s(TransportInfo &info) {
   ASSERT_ON_THREAD(sts_thread_);
   // Call base ready function.
   MediaPipeline::TransportReady_s(info);
 
   // Should not be set for a transmitter
   if (&info == &rtp_) {
     listener_->SetActive(true);
   }
 
   return NS_OK;
 }
 
-nsresult MediaPipelineTransmit::ReplaceTrack(MediaStreamTrack& domtrack) {
+nsresult MediaPipelineTransmit::ReplaceTrack(RefPtr<MediaStreamTrack>& domtrack) {
   // MainThread, checked in calls we make
-  nsString nsTrackId;
-  domtrack.GetId(nsTrackId);
-  std::string track_id(NS_ConvertUTF16toUTF8(nsTrackId).get());
-  MOZ_MTLOG(ML_DEBUG, "Reattaching pipeline " << description_ << " to track "
-            << static_cast<void *>(&domtrack)
-            << " track " << track_id << " conduit type=" <<
-            (conduit_->type() == MediaSessionConduit::AUDIO ?"audio":"video"));
+  if (domtrack) {
+    nsString nsTrackId;
+    domtrack->GetId(nsTrackId);
+    std::string track_id(NS_ConvertUTF16toUTF8(nsTrackId).get());
+    MOZ_MTLOG(ML_DEBUG, "Reattaching pipeline " << description_ << " to track "
+              << static_cast<void *>(domtrack.get())
+              << " track " << track_id << " conduit type=" <<
+              (conduit_->type() == MediaSessionConduit::AUDIO ?"audio":"video"));
+  }
 
-  DetachMedia();
-  domtrack_ = &domtrack; // Detach clears it
-  // Unsets the track id after RemoveListener() takes effect.
-  listener_->UnsetTrackId(domtrack_->GraphImpl());
-  track_id_ = track_id;
-  AttachToTrack(track_id);
+  RefPtr<dom::MediaStreamTrack> oldTrack = domtrack_;
+  DetachFromTrack();
+  domtrack_ = domtrack;
+  SetDescription();
+
+  if (oldTrack) {
+    // Unsets the track id after RemoveListener() takes effect.
+    listener_->UnsetTrackId(oldTrack->GraphImpl());
+  }
+
+  AttachToTrack();
   return NS_OK;
 }
 
 void MediaPipeline::DisconnectTransport_s(TransportInfo &info) {
   MOZ_ASSERT(info.transport_);
   ASSERT_ON_THREAD(sts_thread_);
 
   info.transport_->SignalStateChange.disconnect(this);
@@ -1889,28 +1896,54 @@ static void AddListener(MediaStream* sou
 class GenericReceiveListener : public MediaStreamListener
 {
  public:
   GenericReceiveListener(SourceMediaStream *source, TrackID track_id)
     : source_(source),
       track_id_(track_id),
       played_ticks_(0),
       last_log_(0),
-      principal_handle_(PRINCIPAL_HANDLE_NONE) {}
+      principal_handle_(PRINCIPAL_HANDLE_NONE)
+  {
+    MOZ_ASSERT(source);
+  }
 
   virtual ~GenericReceiveListener() {}
 
   void AddSelf()
   {
     AddListener(source_, this);
   }
 
   void EndTrack()
   {
-    source_->EndTrack(track_id_);
+    MOZ_MTLOG(ML_DEBUG, "GenericReceiveListener ending track");
+
+    // We do this on MSG to avoid it racing against StartTrack.
+    class Message : public ControlMessage
+    {
+    public:
+      Message(SourceMediaStream* stream,
+              TrackID track_id)
+        : ControlMessage(stream),
+          source_(stream),
+          track_id_(track_id)
+      {}
+
+      void Run() override {
+        source_->EndTrack(track_id_);
+      }
+
+      RefPtr<SourceMediaStream> source_;
+      const TrackID track_id_;
+    };
+
+    source_->GraphImpl()->AppendMessage(MakeUnique<Message>(source_, track_id_));
+    // This breaks the cycle with source_
+    source_->RemoveListener(this);
   }
 
   // Must be called on the main thread
   void SetPrincipalHandle_m(const PrincipalHandle& principal_handle)
   {
     class Message : public ControlMessage
     {
     public:
@@ -1935,55 +1968,44 @@ class GenericReceiveListener : public Me
 
   // Must be called on the MediaStreamGraph thread
   void SetPrincipalHandle_msg(const PrincipalHandle& principal_handle)
   {
     principal_handle_ = principal_handle;
   }
 
  protected:
-  SourceMediaStream *source_;
+  RefPtr<SourceMediaStream> source_;
   const TrackID track_id_;
   TrackTicks played_ticks_;
   TrackTicks last_log_; // played_ticks_ when we last logged
   PrincipalHandle principal_handle_;
 };
 
 MediaPipelineReceive::MediaPipelineReceive(
     const std::string& pc,
     nsCOMPtr<nsIEventTarget> main_thread,
     nsCOMPtr<nsIEventTarget> sts_thread,
-    SourceMediaStream *stream,
-    const std::string& track_id,
-    int level,
-    RefPtr<MediaSessionConduit> conduit,
-    RefPtr<TransportFlow> rtp_transport,
-    RefPtr<TransportFlow> rtcp_transport,
-    nsAutoPtr<MediaPipelineFilter> filter) :
-  MediaPipeline(pc, RECEIVE, main_thread, sts_thread,
-                track_id, level, conduit, rtp_transport,
-                rtcp_transport, filter),
-  stream_(stream),
+    RefPtr<MediaSessionConduit> conduit) :
+  MediaPipeline(pc, RECEIVE, main_thread, sts_thread, conduit),
   segments_added_(0)
 {
-  MOZ_ASSERT(stream_);
 }
 
 MediaPipelineReceive::~MediaPipelineReceive()
 {
-  MOZ_ASSERT(!stream_);  // Check that we have shut down already.
 }
 
 class MediaPipelineReceiveAudio::PipelineListener
   : public GenericReceiveListener
 {
 public:
-  PipelineListener(SourceMediaStream * source, TrackID track_id,
+  PipelineListener(SourceMediaStream * source,
                    const RefPtr<MediaSessionConduit>& conduit)
-    : GenericReceiveListener(source, track_id),
+    : GenericReceiveListener(source, kAudioTrack),
       conduit_(conduit)
   {
   }
 
   ~PipelineListener()
   {
     if (!NS_IsMainThread()) {
       // release conduit on mainthread.  Must use forget()!
@@ -2090,67 +2112,49 @@ public:
 private:
   RefPtr<MediaSessionConduit> conduit_;
 };
 
 MediaPipelineReceiveAudio::MediaPipelineReceiveAudio(
     const std::string& pc,
     nsCOMPtr<nsIEventTarget> main_thread,
     nsCOMPtr<nsIEventTarget> sts_thread,
-    SourceMediaStream* stream,
-    const std::string& media_stream_track_id,
-    TrackID numeric_track_id,
-    int level,
-    RefPtr<AudioSessionConduit> conduit,
-    RefPtr<TransportFlow> rtp_transport,
-    RefPtr<TransportFlow> rtcp_transport,
-    nsAutoPtr<MediaPipelineFilter> filter) :
-  MediaPipelineReceive(pc, main_thread, sts_thread,
-                       stream, media_stream_track_id, level, conduit,
-                       rtp_transport, rtcp_transport, filter),
-  listener_(new PipelineListener(stream, numeric_track_id, conduit))
-{}
+    RefPtr<AudioSessionConduit> conduit) :
+  MediaPipelineReceive(pc, main_thread, sts_thread, conduit)
+{
+  description_ = pc_ + "| Receive audio";
+}
 
 void MediaPipelineReceiveAudio::DetachMedia()
 {
   ASSERT_ON_THREAD(main_thread_);
-  if (stream_ && listener_) {
+  if (listener_) {
     listener_->EndTrack();
-
-    if (stream_->GraphImpl()) {
-      stream_->RemoveListener(listener_);
-    }
-    stream_ = nullptr;
+    listener_ = nullptr;
   }
 }
 
-nsresult MediaPipelineReceiveAudio::Init()
-{
-  ASSERT_ON_THREAD(main_thread_);
-  MOZ_MTLOG(ML_DEBUG, __FUNCTION__);
-
-  description_ = pc_ + "| Receive audio[";
-  description_ += track_id_;
-  description_ += "]";
-
-  listener_->AddSelf();
-
-  return MediaPipelineReceive::Init();
-}
-
 void MediaPipelineReceiveAudio::SetPrincipalHandle_m(const PrincipalHandle& principal_handle)
 {
   listener_->SetPrincipalHandle_m(principal_handle);
 }
 
+void
+MediaPipelineReceiveAudio::AttachMedia(SourceMediaStream* aStream)
+{
+  ASSERT_ON_THREAD(main_thread_);
+  listener_ = new PipelineListener(aStream, conduit_);
+  listener_->AddSelf();
+}
+
 class MediaPipelineReceiveVideo::PipelineListener
   : public GenericReceiveListener {
 public:
-  PipelineListener(SourceMediaStream* source, TrackID track_id)
-    : GenericReceiveListener(source, track_id)
+  explicit PipelineListener(SourceMediaStream* source)
+    : GenericReceiveListener(source, kVideoTrack)
     , image_container_()
     , image_()
     , mutex_("Video PipelineListener")
   {
     image_container_ =
       LayerManager::CreateImageContainer(ImageContainer::ASYNCHRONOUS);
   }
 
@@ -2269,64 +2273,37 @@ private:
   MediaPipelineReceiveVideo *pipeline_;  // Raw pointer to avoid cycles
 };
 
 
 MediaPipelineReceiveVideo::MediaPipelineReceiveVideo(
     const std::string& pc,
     nsCOMPtr<nsIEventTarget> main_thread,
     nsCOMPtr<nsIEventTarget> sts_thread,
-    SourceMediaStream *stream,
-    const std::string& media_stream_track_id,
-    TrackID numeric_track_id,
-    int level,
-    RefPtr<VideoSessionConduit> conduit,
-    RefPtr<TransportFlow> rtp_transport,
-    RefPtr<TransportFlow> rtcp_transport,
-    nsAutoPtr<MediaPipelineFilter> filter) :
-  MediaPipelineReceive(pc, main_thread, sts_thread,
-                       stream, media_stream_track_id, level, conduit,
-                       rtp_transport, rtcp_transport, filter),
-  renderer_(new PipelineRenderer(this)),
-  listener_(new PipelineListener(stream, numeric_track_id))
-{}
+    RefPtr<VideoSessionConduit> conduit) :
+  MediaPipelineReceive(pc, main_thread, sts_thread, conduit)
+{
+  description_ = pc_ + "| Receive video";
+}
 
 void MediaPipelineReceiveVideo::DetachMedia()
 {
   ASSERT_ON_THREAD(main_thread_);
 
   // stop generating video and thus stop invoking the PipelineRenderer
   // and PipelineListener - the renderer has a raw ptr to the Pipeline to
   // avoid cycles, and the render callbacks are invoked from a different
   // thread so simple null-checks would cause TSAN bugs without locks.
   static_cast<VideoSessionConduit*>(conduit_.get())->DetachRenderer();
-  if (stream_ && listener_) {
+  if (listener_) {
     listener_->EndTrack();
-    stream_->RemoveListener(listener_);
-    stream_ = nullptr;
+    listener_ = nullptr;
   }
 }
 
-nsresult MediaPipelineReceiveVideo::Init() {
-  ASSERT_ON_THREAD(main_thread_);
-  MOZ_MTLOG(ML_DEBUG, __FUNCTION__);
-
-  description_ = pc_ + "| Receive video[";
-  description_ += track_id_;
-  description_ += "]";
-
-  listener_->AddSelf();
-
-  // Always happens before we can DetachMedia()
-  static_cast<VideoSessionConduit *>(conduit_.get())->
-      AttachRenderer(renderer_);
-
-  return MediaPipelineReceive::Init();
-}
-
 void MediaPipelineReceiveVideo::SetPrincipalHandle_m(const PrincipalHandle& principal_handle)
 {
   listener_->SetPrincipalHandle_m(principal_handle);
 }
 
 DOMHighResTimeStamp MediaPipeline::GetNow() {
   return webrtc::Clock::GetRealTimeClock()->TimeInMilliseconds();
 }
@@ -2353,9 +2330,19 @@ MediaPipeline::RtpCSRCStats::GetWebidlIn
   statId.AppendInt(mCsrc);
   aWebidlObj.mId.Construct(statId);
   aWebidlObj.mType.Construct(RTCStatsType::Csrc);
   aWebidlObj.mTimestamp.Construct(mTimestamp);
   aWebidlObj.mContributorSsrc.Construct(mCsrc);
   aWebidlObj.mInboundRtpStreamId.Construct(aInboundRtpStreamId);
 }
 
+void
+MediaPipelineReceiveVideo::AttachMedia(SourceMediaStream* aStream)
+{
+  ASSERT_ON_THREAD(main_thread_);
+  renderer_ = new PipelineRenderer(this);
+  listener_ = new PipelineListener(aStream);
+  listener_->AddSelf();
+  static_cast<VideoSessionConduit *>(conduit_.get())->AttachRenderer(renderer_);
+}
+
 }  // end namespace
--- a/media/webrtc/signaling/src/mediapipeline/MediaPipeline.h
+++ b/media/webrtc/signaling/src/mediapipeline/MediaPipeline.h
@@ -7,23 +7,23 @@
 
 #ifndef mediapipeline_h__
 #define mediapipeline_h__
 
 #include <map>
 
 #include "sigslot.h"
 
-#include "MediaConduitInterface.h"
+#include "signaling/src/media-conduit/MediaConduitInterface.h"
 #include "mozilla/ReentrantMonitor.h"
 #include "mozilla/Atomics.h"
 #include "SrtpFlow.h"
 #include "databuffer.h"
-#include "runnable_utils.h"
-#include "transportflow.h"
+#include "mtransport/runnable_utils.h"
+#include "mtransport/transportflow.h"
 #include "AudioPacketizer.h"
 #include "StreamTracks.h"
 
 #include "webrtc/modules/rtp_rtcp/include/rtp_header_parser.h"
 
 // Should come from MediaEngine.h, but that's a pain to include here
 // because of the MOZILLA_EXTERNAL_LINKAGE stuff.
 #define WEBRTC_DEFAULT_SAMPLE_RATE 32000
@@ -77,63 +77,55 @@ class SourceMediaStream;
 class MediaPipeline : public sigslot::has_slots<> {
  public:
   enum Direction { TRANSMIT, RECEIVE };
   enum State { MP_CONNECTING, MP_OPEN, MP_CLOSED };
   MediaPipeline(const std::string& pc,
                 Direction direction,
                 nsCOMPtr<nsIEventTarget> main_thread,
                 nsCOMPtr<nsIEventTarget> sts_thread,
-                const std::string& track_id,
-                int level,
-                RefPtr<MediaSessionConduit> conduit,
-                RefPtr<TransportFlow> rtp_transport,
-                RefPtr<TransportFlow> rtcp_transport,
-                nsAutoPtr<MediaPipelineFilter> filter);
+                RefPtr<MediaSessionConduit> conduit);
 
   // Must be called on the STS thread.  Must be called after ShutdownMedia_m().
   void DetachTransport_s();
 
   // Must be called on the main thread.
+  void Shutdown_m();
+
+  // Must be called on the main thread.
   void ShutdownMedia_m()
   {
     ASSERT_ON_THREAD(main_thread_);
 
     if (direction_ == RECEIVE) {
       conduit_->StopReceiving();
     } else {
       conduit_->StopTransmitting();
     }
     DetachMedia();
   }
 
-  virtual nsresult Init();
-
-  void UpdateTransport_m(int level,
-                         RefPtr<TransportFlow> rtp_transport,
+  void UpdateTransport_m(RefPtr<TransportFlow> rtp_transport,
                          RefPtr<TransportFlow> rtcp_transport,
                          nsAutoPtr<MediaPipelineFilter> filter);
 
-  void UpdateTransport_s(int level,
-                         RefPtr<TransportFlow> rtp_transport,
+  void UpdateTransport_s(RefPtr<TransportFlow> rtp_transport,
                          RefPtr<TransportFlow> rtcp_transport,
                          nsAutoPtr<MediaPipelineFilter> filter);
 
   // Used only for testing; adds RTP header extension for RTP Stream Id with
   // the given id.
   void AddRIDExtension_m(size_t extension_id);
   void AddRIDExtension_s(size_t extension_id);
   // Used only for testing; installs a MediaPipelineFilter that filters
   // everything but the given RID
   void AddRIDFilter_m(const std::string& rid);
   void AddRIDFilter_s(const std::string& rid);
 
   virtual Direction direction() const { return direction_; }
-  virtual const std::string& trackid() const { return track_id_; }
-  virtual int level() const { return level_; }
   virtual bool IsVideo() const = 0;
 
   bool IsDoingRtcpMux() const {
     return (rtp_.type_ == MUX);
   }
 
   class RtpCSRCStats {
   public:
@@ -207,37 +199,33 @@ class MediaPipeline : public sigslot::ha
 
     virtual nsresult SendRtpPacket(const uint8_t* data, size_t len);
     virtual nsresult SendRtcpPacket(const uint8_t* data, size_t len);
 
    private:
     nsresult SendRtpRtcpPacket_s(nsAutoPtr<DataBuffer> data,
                                  bool is_rtp);
 
-    MediaPipeline *pipeline_;  // Raw pointer to avoid cycles
+    // Creates a cycle, which we break with Detach
+    RefPtr<MediaPipeline> pipeline_;
     nsCOMPtr<nsIEventTarget> sts_thread_;
   };
 
-  RefPtr<PipelineTransport> GetPiplelineTransport() {
-    return transport_;
-  }
-
  protected:
   virtual ~MediaPipeline();
   virtual void DetachMedia() {}
   nsresult AttachTransport_s();
   friend class PipelineTransport;
 
   class TransportInfo {
     public:
       TransportInfo(RefPtr<TransportFlow> flow, RtpType type) :
         transport_(flow),
         state_(MP_CONNECTING),
         type_(type) {
-        MOZ_ASSERT(flow);
       }
 
       void Detach()
       {
         transport_ = nullptr;
         send_srtp_ = nullptr;
         recv_srtp_ = nullptr;
       }
@@ -273,64 +261,54 @@ class MediaPipeline : public sigslot::ha
   void RtpPacketReceived(TransportLayer *layer, const unsigned char *data,
                          size_t len);
   void RtcpPacketReceived(TransportLayer *layer, const unsigned char *data,
                           size_t len);
   void PacketReceived(TransportLayer *layer, const unsigned char *data,
                       size_t len);
 
   Direction direction_;
-  std::string track_id_;        // The track on the stream.
-                                // Written on the main thread.
-                                // Used on STS and MediaStreamGraph threads.
-                                // Not used outside initialization in MediaPipelineTransmit
-  // The m-line index (starting at 0, to match convention) Atomic because
-  // this value is updated from STS, but read on main, and we don't want to
-  // bother with dispatches just to get an int occasionally.
-  Atomic<int> level_;
   RefPtr<MediaSessionConduit> conduit_;  // Our conduit. Written on the main
                                          // thread. Read on STS thread.
 
   // The transport objects. Read/written on STS thread.
   TransportInfo rtp_;
   TransportInfo rtcp_;
 
   // Pointers to the threads we need. Initialized at creation
   // and used all over the place.
   nsCOMPtr<nsIEventTarget> main_thread_;
   nsCOMPtr<nsIEventTarget> sts_thread_;
 
-  // Created on Init. Referenced by the conduit and eventually
-  // destroyed on the STS thread.
+  // Created in c'tor. Referenced by the conduit.
   RefPtr<PipelineTransport> transport_;
 
   // Only safe to access from STS thread.
   // Build into TransportInfo?
   int32_t rtp_packets_sent_;
   int32_t rtcp_packets_sent_;
   int32_t rtp_packets_received_;
   int32_t rtcp_packets_received_;
   int64_t rtp_bytes_sent_;
   int64_t rtp_bytes_received_;
 
   // Only safe to access from STS thread.
   std::map<uint32_t, RtpCSRCStats> csrc_stats_;
 
-  // Written on Init. Read on STS thread.
+  // Written in c'tor. Read on STS thread.
   std::string pc_;
   std::string description_;
 
-  // Written on Init, all following accesses are on the STS thread.
+  // Written in c'tor, all following accesses are on the STS thread.
   nsAutoPtr<MediaPipelineFilter> filter_;
   nsAutoPtr<webrtc::RtpHeaderParser> rtp_parser_;
 
  private:
   // Gets the current time as a DOMHighResTimeStamp
   static DOMHighResTimeStamp GetNow();
-  nsresult Init_s();
 
   bool IsRtp(const unsigned char *data, size_t len);
 };
 
 class ConduitDeleteEvent: public Runnable
 {
 public:
   explicit ConduitDeleteEvent(already_AddRefed<MediaSessionConduit> aConduit) :
@@ -346,28 +324,22 @@ private:
 // A specialization of pipeline for reading from an input device
 // and transmitting to the network.
 class MediaPipelineTransmit : public MediaPipeline {
 public:
   // Set rtcp_transport to nullptr to use rtcp-mux
   MediaPipelineTransmit(const std::string& pc,
                         nsCOMPtr<nsIEventTarget> main_thread,
                         nsCOMPtr<nsIEventTarget> sts_thread,
+                        bool is_video,
                         dom::MediaStreamTrack* domtrack,
-                        const std::string& track_id,
-                        int level,
-                        RefPtr<MediaSessionConduit> conduit,
-                        RefPtr<TransportFlow> rtp_transport,
-                        RefPtr<TransportFlow> rtcp_transport,
-                        nsAutoPtr<MediaPipelineFilter> filter);
+                        RefPtr<MediaSessionConduit> conduit);
 
-  // Initialize (stuff here may fail)
-  nsresult Init() override;
-
-  virtual void AttachToTrack(const std::string& track_id);
+  virtual void AttachToTrack();
+  virtual void DetachFromTrack();
 
   // written and used from MainThread
   bool IsVideo() const override;
 
   // When the principal of the domtrack changes, it calls through to here
   // so that we can determine whether to enable track transmission.
   // `track` has to be null or equal `domtrack_` for us to apply the update.
   virtual void UpdateSinkIdentity_m(dom::MediaStreamTrack* track,
@@ -379,130 +351,107 @@ public:
 
   // Override MediaPipeline::TransportReady.
   nsresult TransportReady_s(TransportInfo &info) override;
 
   // Replace a track with a different one
   // In non-compliance with the likely final spec, allow the new
   // track to be part of a different stream (since we don't support
   // multiple tracks of a type in a stream yet).  bug 1056650
-  virtual nsresult ReplaceTrack(dom::MediaStreamTrack& domtrack);
+  virtual nsresult ReplaceTrack(RefPtr<dom::MediaStreamTrack>& domtrack);
 
   // Separate classes to allow ref counting
   class PipelineListener;
   class VideoFrameFeeder;
 
  protected:
   ~MediaPipelineTransmit();
 
+  void SetDescription();
+
  private:
   RefPtr<PipelineListener> listener_;
   RefPtr<AudioProxyThread> audio_processing_;
   RefPtr<VideoFrameFeeder> feeder_;
   RefPtr<VideoFrameConverter> converter_;
-  dom::MediaStreamTrack* domtrack_;
+  bool is_video_;
+  RefPtr<dom::MediaStreamTrack> domtrack_;
+  bool track_attached_;
 };
 
 
 // A specialization of pipeline for reading from the network and
 // rendering video.
 class MediaPipelineReceive : public MediaPipeline {
  public:
   // Set rtcp_transport to nullptr to use rtcp-mux
   MediaPipelineReceive(const std::string& pc,
                        nsCOMPtr<nsIEventTarget> main_thread,
                        nsCOMPtr<nsIEventTarget> sts_thread,
-                       SourceMediaStream *stream,
-                       const std::string& track_id,
-                       int level,
-                       RefPtr<MediaSessionConduit> conduit,
-                       RefPtr<TransportFlow> rtp_transport,
-                       RefPtr<TransportFlow> rtcp_transport,
-                       nsAutoPtr<MediaPipelineFilter> filter);
+                       RefPtr<MediaSessionConduit> conduit);
 
   int segments_added() const { return segments_added_; }
 
   // Sets the PrincipalHandle we set on the media chunks produced by this
   // pipeline. Must be called on the main thread.
   virtual void SetPrincipalHandle_m(const PrincipalHandle& principal_handle) = 0;
+
+  virtual void AttachMedia(SourceMediaStream* aStream) = 0;
  protected:
   ~MediaPipelineReceive();
 
-  RefPtr<SourceMediaStream> stream_;
   int segments_added_;
 
  private:
 };
 
 
 // A specialization of pipeline for reading from the network and
 // rendering audio.
 class MediaPipelineReceiveAudio : public MediaPipelineReceive {
  public:
   MediaPipelineReceiveAudio(const std::string& pc,
                             nsCOMPtr<nsIEventTarget> main_thread,
                             nsCOMPtr<nsIEventTarget> sts_thread,
-                            SourceMediaStream* stream,
-                            // This comes from an msid attribute. Everywhere
-                            // but MediaStreamGraph uses this.
-                            const std::string& media_stream_track_id,
-                            // This is an integer identifier that is only
-                            // unique within a single DOMMediaStream, which is
-                            // used by MediaStreamGraph
-                            TrackID numeric_track_id,
-                            int level,
-                            RefPtr<AudioSessionConduit> conduit,
-                            RefPtr<TransportFlow> rtp_transport,
-                            RefPtr<TransportFlow> rtcp_transport,
-                            nsAutoPtr<MediaPipelineFilter> filter);
+                            RefPtr<AudioSessionConduit> conduit);
 
   void DetachMedia() override;
 
-  nsresult Init() override;
   bool IsVideo() const override { return false; }
 
   void SetPrincipalHandle_m(const PrincipalHandle& principal_handle) override;
 
+  void AttachMedia(SourceMediaStream* aStream) override;
+
  private:
   // Separate class to allow ref counting
   class PipelineListener;
 
   RefPtr<PipelineListener> listener_;
 };
 
 
 // A specialization of pipeline for reading from the network and
 // rendering video.
 class MediaPipelineReceiveVideo : public MediaPipelineReceive {
  public:
   MediaPipelineReceiveVideo(const std::string& pc,
                             nsCOMPtr<nsIEventTarget> main_thread,
                             nsCOMPtr<nsIEventTarget> sts_thread,
-                            SourceMediaStream *stream,
-                            // This comes from an msid attribute. Everywhere
-                            // but MediaStreamGraph uses this.
-                            const std::string& media_stream_track_id,
-                            // This is an integer identifier that is only
-                            // unique within a single DOMMediaStream, which is
-                            // used by MediaStreamGraph
-                            TrackID numeric_track_id,
-                            int level,
-                            RefPtr<VideoSessionConduit> conduit,
-                            RefPtr<TransportFlow> rtp_transport,
-                            RefPtr<TransportFlow> rtcp_transport,
-                            nsAutoPtr<MediaPipelineFilter> filter);
+                            RefPtr<VideoSessionConduit> conduit);
 
   // Called on the main thread.
   void DetachMedia() override;
 
-  nsresult Init() override;
   bool IsVideo() const override { return true; }
 
   void SetPrincipalHandle_m(const PrincipalHandle& principal_handle) override;
 
+  void AttachMedia(SourceMediaStream* aStream) override;
+
  private:
   class PipelineRenderer;
   friend class PipelineRenderer;
 
   // Separate class to allow ref counting
   class PipelineListener;
 
   RefPtr<PipelineRenderer> renderer_;
deleted file mode 100644
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.cpp
+++ /dev/null
@@ -1,936 +0,0 @@
-/* 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/. */
-
-#include "logging.h"
-#include "nsIGfxInfo.h"
-#include "nsServiceManagerUtils.h"
-
-#include "PeerConnectionImpl.h"
-#include "PeerConnectionMedia.h"
-#include "MediaPipelineFactory.h"
-#include "MediaPipelineFilter.h"
-#include "transportflow.h"
-#include "transportlayer.h"
-#include "transportlayerdtls.h"
-#include "transportlayerice.h"
-
-#include "signaling/src/jsep/JsepTrack.h"
-#include "signaling/src/jsep/JsepTransport.h"
-#include "signaling/src/common/PtrVector.h"
-
-#include "MediaStreamTrack.h"
-#include "nsIPrincipal.h"
-#include "nsIDocument.h"
-#include "mozilla/Preferences.h"
-#include "MediaEngine.h"
-
-#include "mozilla/Preferences.h"
-
-#include "WebrtcGmpVideoCodec.h"
-
-#include <stdlib.h>
-
-namespace mozilla {
-
-MOZ_MTLOG_MODULE("MediaPipelineFactory")
-
-static nsresult
-JsepCodecDescToCodecConfig(const JsepCodecDescription& aCodec,
-                           AudioCodecConfig** aConfig)
-{
-  MOZ_ASSERT(aCodec.mType == SdpMediaSection::kAudio);
-  if (aCodec.mType != SdpMediaSection::kAudio)
-    return NS_ERROR_INVALID_ARG;
-
-  const JsepAudioCodecDescription& desc =
-      static_cast<const JsepAudioCodecDescription&>(aCodec);
-
-  uint16_t pt;
-
-  if (!desc.GetPtAsInt(&pt)) {
-    MOZ_MTLOG(ML_ERROR, "Invalid payload type: " << desc.mDefaultPt);
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  *aConfig = new AudioCodecConfig(pt,
-                                  desc.mName,
-                                  desc.mClock,
-                                  desc.mPacketSize,
-                                  desc.mForceMono ? 1 : desc.mChannels,
-                                  desc.mBitrate,
-                                  desc.mFECEnabled);
-  (*aConfig)->mMaxPlaybackRate = desc.mMaxPlaybackRate;
-  (*aConfig)->mDtmfEnabled = desc.mDtmfEnabled;
-
-  return NS_OK;
-}
-
-static std::vector<JsepCodecDescription*>
-GetCodecs(const JsepTrackNegotiatedDetails& aDetails)
-{
-  // We do not try to handle cases where a codec is not used on the primary
-  // encoding.
-  if (aDetails.GetEncodingCount()) {
-    return aDetails.GetEncoding(0).GetCodecs();
-  }
-  return std::vector<JsepCodecDescription*>();
-}
-
-static nsresult
-NegotiatedDetailsToAudioCodecConfigs(const JsepTrackNegotiatedDetails& aDetails,
-                                     PtrVector<AudioCodecConfig>* aConfigs)
-{
-  std::vector<JsepCodecDescription*> codecs(GetCodecs(aDetails));
-  for (const JsepCodecDescription* codec : codecs) {
-    AudioCodecConfig* config;
-    if (NS_FAILED(JsepCodecDescToCodecConfig(*codec, &config))) {
-      return NS_ERROR_INVALID_ARG;
-    }
-    aConfigs->values.push_back(config);
-  }
-  return NS_OK;
-}
-
-static nsresult
-JsepCodecDescToCodecConfig(const JsepCodecDescription& aCodec,
-                           VideoCodecConfig** aConfig)
-{
-  MOZ_ASSERT(aCodec.mType == SdpMediaSection::kVideo);
-  if (aCodec.mType != SdpMediaSection::kVideo) {
-    MOZ_ASSERT(false, "JsepCodecDescription has wrong type");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  const JsepVideoCodecDescription& desc =
-      static_cast<const JsepVideoCodecDescription&>(aCodec);
-
-  uint16_t pt;
-
-  if (!desc.GetPtAsInt(&pt)) {
-    MOZ_MTLOG(ML_ERROR, "Invalid payload type: " << desc.mDefaultPt);
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  UniquePtr<VideoCodecConfigH264> h264Config;
-
-  if (desc.mName == "H264") {
-    h264Config = MakeUnique<VideoCodecConfigH264>();
-    size_t spropSize = sizeof(h264Config->sprop_parameter_sets);
-    strncpy(h264Config->sprop_parameter_sets,
-            desc.mSpropParameterSets.c_str(),
-            spropSize);
-    h264Config->sprop_parameter_sets[spropSize - 1] = '\0';
-    h264Config->packetization_mode = desc.mPacketizationMode;
-    h264Config->profile_level_id = desc.mProfileLevelId;
-    h264Config->tias_bw = 0; // TODO. Issue 165.
-  }
-
-  VideoCodecConfig* configRaw;
-  configRaw = new VideoCodecConfig(
-      pt, desc.mName, desc.mConstraints, h264Config.get());
-
-  configRaw->mAckFbTypes = desc.mAckFbTypes;
-  configRaw->mNackFbTypes = desc.mNackFbTypes;
-  configRaw->mCcmFbTypes = desc.mCcmFbTypes;
-  configRaw->mRembFbSet = desc.RtcpFbRembIsSet();
-  configRaw->mFECFbSet = desc.mFECEnabled;
-  if (desc.mFECEnabled) {
-    configRaw->mREDPayloadType = desc.mREDPayloadType;
-    configRaw->mULPFECPayloadType = desc.mULPFECPayloadType;
-  }
-
-  *aConfig = configRaw;
-  return NS_OK;
-}
-
-static nsresult
-NegotiatedDetailsToVideoCodecConfigs(const JsepTrackNegotiatedDetails& aDetails,
-                                     PtrVector<VideoCodecConfig>* aConfigs)
-{
-  std::vector<JsepCodecDescription*> codecs(GetCodecs(aDetails));
-  for (const JsepCodecDescription* codec : codecs) {
-    VideoCodecConfig* config;
-    if (NS_FAILED(JsepCodecDescToCodecConfig(*codec, &config))) {
-      return NS_ERROR_INVALID_ARG;
-    }
-
-    config->mTias = aDetails.GetTias();
-
-    for (size_t i = 0; i < aDetails.GetEncodingCount(); ++i) {
-      const JsepTrackEncoding& jsepEncoding(aDetails.GetEncoding(i));
-      if (jsepEncoding.HasFormat(codec->mDefaultPt)) {
-        VideoCodecConfig::SimulcastEncoding encoding;
-        encoding.rid = jsepEncoding.mRid;
-        encoding.constraints = jsepEncoding.mConstraints;
-        config->mSimulcastEncodings.push_back(encoding);
-      }
-    }
-
-    aConfigs->values.push_back(config);
-  }
-
-  return NS_OK;
-}
-
-// Accessing the PCMedia should be safe here because we shouldn't
-// have enqueued this function unless it was still active and
-// the ICE data is destroyed on the STS.
-static void
-FinalizeTransportFlow_s(RefPtr<PeerConnectionMedia> aPCMedia,
-                        RefPtr<TransportFlow> aFlow, size_t aLevel,
-                        bool aIsRtcp,
-                        nsAutoPtr<PtrVector<TransportLayer> > aLayerList)
-{
-  TransportLayerIce* ice =
-      static_cast<TransportLayerIce*>(aLayerList->values.front());
-  ice->SetParameters(aPCMedia->ice_ctx(),
-                     aPCMedia->ice_media_stream(aLevel),
-                     aIsRtcp ? 2 : 1);
-  nsAutoPtr<std::queue<TransportLayer*> > layerQueue(
-      new std::queue<TransportLayer*>);
-  for (auto& value : aLayerList->values) {
-    layerQueue->push(value);
-  }
-  aLayerList->values.clear();
-  (void)aFlow->PushLayers(layerQueue); // TODO(bug 854518): Process errors.
-}
-
-static void
-AddNewIceStreamForRestart_s(RefPtr<PeerConnectionMedia> aPCMedia,
-                            RefPtr<TransportFlow> aFlow,
-                            size_t aLevel,
-                            bool aIsRtcp)
-{
-  TransportLayerIce* ice =
-      static_cast<TransportLayerIce*>(aFlow->GetLayer("ice"));
-  ice->SetParameters(aPCMedia->ice_ctx(),
-                     aPCMedia->ice_media_stream(aLevel),
-                     aIsRtcp ? 2 : 1);
-}
-
-nsresult
-MediaPipelineFactory::CreateOrGetTransportFlow(
-    size_t aLevel,
-    bool aIsRtcp,
-    const JsepTransport& aTransport,
-    RefPtr<TransportFlow>* aFlowOutparam)
-{
-  nsresult rv;
-  RefPtr<TransportFlow> flow;
-
-  flow = mPCMedia->GetTransportFlow(aLevel, aIsRtcp);
-  if (flow) {
-    if (mPCMedia->IsIceRestarting()) {
-      MOZ_MTLOG(ML_INFO, "Flow[" << flow->id() << "]: "
-                                 << "detected ICE restart - level: "
-                                 << aLevel << " rtcp: " << aIsRtcp);
-
-      rv = mPCMedia->GetSTSThread()->Dispatch(
-          WrapRunnableNM(AddNewIceStreamForRestart_s,
-                         mPCMedia, flow, aLevel, aIsRtcp),
-          NS_DISPATCH_NORMAL);
-      if (NS_FAILED(rv)) {
-        MOZ_MTLOG(ML_ERROR, "Failed to dispatch AddNewIceStreamForRestart_s");
-        return rv;
-      }
-    }
-
-    *aFlowOutparam = flow;
-    return NS_OK;
-  }
-
-  std::ostringstream osId;
-  osId << mPC->GetHandle() << ":" << aLevel << ","
-       << (aIsRtcp ? "rtcp" : "rtp");
-  flow = new TransportFlow(osId.str());
-
-  // The media streams are made on STS so we need to defer setup.
-  auto ice = MakeUnique<TransportLayerIce>(mPC->GetHandle());
-  auto dtls = MakeUnique<TransportLayerDtls>();
-  dtls->SetRole(aTransport.mDtls->GetRole() ==
-                        JsepDtlsTransport::kJsepDtlsClient
-                    ? TransportLayerDtls::CLIENT
-                    : TransportLayerDtls::SERVER);
-
-  RefPtr<DtlsIdentity> pcid = mPC->Identity();
-  if (!pcid) {
-    MOZ_MTLOG(ML_ERROR, "Failed to get DTLS identity.");
-    return NS_ERROR_FAILURE;
-  }
-  dtls->SetIdentity(pcid);
-
-  const SdpFingerprintAttributeList& fingerprints =
-      aTransport.mDtls->GetFingerprints();
-  for (const auto& fingerprint : fingerprints.mFingerprints) {
-    std::ostringstream ss;
-    ss << fingerprint.hashFunc;
-    rv = dtls->SetVerificationDigest(ss.str(), &fingerprint.fingerprint[0],
-                                     fingerprint.fingerprint.size());
-    if (NS_FAILED(rv)) {
-      MOZ_MTLOG(ML_ERROR, "Could not set fingerprint");
-      return rv;
-    }
-  }
-
-  std::vector<uint16_t> srtpCiphers;
-  srtpCiphers.push_back(SRTP_AES128_CM_HMAC_SHA1_80);
-  srtpCiphers.push_back(SRTP_AES128_CM_HMAC_SHA1_32);
-
-  rv = dtls->SetSrtpCiphers(srtpCiphers);
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Couldn't set SRTP ciphers");
-    return rv;
-  }
-
-  // Always permits negotiation of the confidential mode.
-  // Only allow non-confidential (which is an allowed default),
-  // if we aren't confidential.
-  std::set<std::string> alpn;
-  std::string alpnDefault = "";
-  alpn.insert("c-webrtc");
-  if (!mPC->PrivacyRequested()) {
-    alpnDefault = "webrtc";
-    alpn.insert(alpnDefault);
-  }
-  rv = dtls->SetAlpn(alpn, alpnDefault);
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Couldn't set ALPN");
-    return rv;
-  }
-
-  nsAutoPtr<PtrVector<TransportLayer> > layers(new PtrVector<TransportLayer>);
-  layers->values.push_back(ice.release());
-  layers->values.push_back(dtls.release());
-
-  rv = mPCMedia->GetSTSThread()->Dispatch(
-      WrapRunnableNM(FinalizeTransportFlow_s, mPCMedia, flow, aLevel, aIsRtcp,
-                     layers),
-      NS_DISPATCH_NORMAL);
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Failed to dispatch FinalizeTransportFlow_s");
-    return rv;
-  }
-
-  mPCMedia->AddTransportFlow(aLevel, aIsRtcp, flow);
-
-  *aFlowOutparam = flow;
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::GetTransportParameters(
-    const JsepTrackPair& aTrackPair,
-    const JsepTrack& aTrack,
-    size_t* aLevelOut,
-    RefPtr<TransportFlow>* aRtpOut,
-    RefPtr<TransportFlow>* aRtcpOut,
-    nsAutoPtr<MediaPipelineFilter>* aFilterOut)
-{
-  *aLevelOut = aTrackPair.mLevel;
-
-  size_t transportLevel = aTrackPair.HasBundleLevel() ?
-                          aTrackPair.BundleLevel() :
-                          aTrackPair.mLevel;
-
-  nsresult rv = CreateOrGetTransportFlow(
-      transportLevel, false, *aTrackPair.mRtpTransport, aRtpOut);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  MOZ_ASSERT(aRtpOut);
-
-  if (aTrackPair.mRtcpTransport) {
-    rv = CreateOrGetTransportFlow(
-        transportLevel, true, *aTrackPair.mRtcpTransport, aRtcpOut);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-    MOZ_ASSERT(aRtcpOut);
-  }
-
-  if (aTrackPair.HasBundleLevel()) {
-    bool receiving = aTrack.GetDirection() == sdp::kRecv;
-
-    *aFilterOut = new MediaPipelineFilter;
-
-    if (receiving) {
-      // Add remote SSRCs so we can distinguish which RTP packets actually
-      // belong to this pipeline (also RTCP sender reports).
-      for (unsigned int ssrc : aTrack.GetSsrcs()) {
-        (*aFilterOut)->AddRemoteSSRC(ssrc);
-      }
-
-      // TODO(bug 1105005): Tell the filter about the mid for this track
-
-      // Add unique payload types as a last-ditch fallback
-      auto uniquePts = aTrack.GetNegotiatedDetails()->GetUniquePayloadTypes();
-      for (unsigned char& uniquePt : uniquePts) {
-        (*aFilterOut)->AddUniquePT(uniquePt);
-      }
-    }
-  }
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::CreateOrUpdateMediaPipeline(
-    const JsepTrackPair& aTrackPair,
-    const JsepTrack& aTrack)
-{
-  // The GMP code is all the way on the other side of webrtc.org, and it is not
-  // feasible to plumb this information all the way through. So, we set it (for
-  // the duration of this call) in a global variable. This allows the GMP code
-  // to report errors to the PC.
-  WebrtcGmpPCHandleSetter setter(mPC->GetHandle());
-
-  MOZ_ASSERT(aTrackPair.mRtpTransport);
-
-  bool receiving = aTrack.GetDirection() == sdp::kRecv;
-
-  size_t level;
-  RefPtr<TransportFlow> rtpFlow;
-  RefPtr<TransportFlow> rtcpFlow;
-  nsAutoPtr<MediaPipelineFilter> filter;
-
-  nsresult rv = GetTransportParameters(aTrackPair,
-                                       aTrack,
-                                       &level,
-                                       &rtpFlow,
-                                       &rtcpFlow,
-                                       &filter);
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Failed to get transport parameters for pipeline, rv="
-              << static_cast<unsigned>(rv));
-    return rv;
-  }
-
-  if (aTrack.GetMediaType() == SdpMediaSection::kApplication) {
-    // GetTransportParameters has already done everything we need for
-    // datachannel.
-    return NS_OK;
-  }
-
-  // Find the stream we need
-  SourceStreamInfo* stream;
-  if (receiving) {
-    stream = mPCMedia->GetRemoteStreamById(aTrack.GetStreamId());
-  } else {
-    stream = mPCMedia->GetLocalStreamById(aTrack.GetStreamId());
-  }
-
-  if (!stream) {
-    MOZ_MTLOG(ML_ERROR, "Negotiated " << (receiving ? "recv" : "send")
-              << " stream id " << aTrack.GetStreamId() << " was never added");
-    MOZ_ASSERT(false);
-    return NS_ERROR_FAILURE;
-  }
-
-  if (!stream->HasTrack(aTrack.GetTrackId())) {
-    MOZ_MTLOG(ML_ERROR, "Negotiated " << (receiving ? "recv" : "send")
-              << " track id " << aTrack.GetTrackId() << " was never added");
-    MOZ_ASSERT(false);
-    return NS_ERROR_FAILURE;
-  }
-
-  RefPtr<MediaSessionConduit> conduit;
-  if (aTrack.GetMediaType() == SdpMediaSection::kAudio) {
-    rv = GetOrCreateAudioConduit(aTrackPair, aTrack, &conduit);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-  } else if (aTrack.GetMediaType() == SdpMediaSection::kVideo) {
-    rv = GetOrCreateVideoConduit(aTrackPair, aTrack, &conduit);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-    conduit->SetPCHandle(mPC->GetHandle());
-  } else {
-    // We've created the TransportFlow, nothing else to do here.
-    return NS_OK;
-  }
-
-  if (aTrack.GetActive()) {
-    if (receiving) {
-      auto error = conduit->StartReceiving();
-      if (error) {
-        MOZ_MTLOG(ML_ERROR, "StartReceiving failed: " << error);
-        return NS_ERROR_FAILURE;
-      }
-    } else {
-      auto error = conduit->StartTransmitting();
-      if (error) {
-        MOZ_MTLOG(ML_ERROR, "StartTransmitting failed: " << error);
-        return NS_ERROR_FAILURE;
-      }
-    }
-  } else {
-    if (receiving) {
-      auto error = conduit->StopReceiving();
-      if (error) {
-        MOZ_MTLOG(ML_ERROR, "StopReceiving failed: " << error);
-        return NS_ERROR_FAILURE;
-      }
-    } else {
-      auto error = conduit->StopTransmitting();
-      if (error) {
-        MOZ_MTLOG(ML_ERROR, "StopTransmitting failed: " << error);
-        return NS_ERROR_FAILURE;
-      }
-    }
-  }
-
-  RefPtr<MediaPipeline> pipeline =
-    stream->GetPipelineByTrackId_m(aTrack.GetTrackId());
-
-  if (pipeline && pipeline->level() != static_cast<int>(level)) {
-    MOZ_MTLOG(ML_WARNING, "Track " << aTrack.GetTrackId() <<
-                          " has moved from level " << pipeline->level() <<
-                          " to level " << level <<
-                          ". This requires re-creating the MediaPipeline.");
-    RefPtr<dom::MediaStreamTrack> domTrack =
-      stream->GetTrackById(aTrack.GetTrackId());
-    MOZ_ASSERT(domTrack, "MediaPipeline existed for a track, but no MediaStreamTrack");
-
-    // Since we do not support changing the conduit on a pre-existing
-    // MediaPipeline
-    pipeline = nullptr;
-    stream->RemoveTrack(aTrack.GetTrackId());
-    stream->AddTrack(aTrack.GetTrackId(), domTrack);
-  }
-
-  if (pipeline) {
-    pipeline->UpdateTransport_m(level, rtpFlow, rtcpFlow, filter);
-    return NS_OK;
-  }
-
-  MOZ_MTLOG(ML_DEBUG,
-            "Creating media pipeline"
-                << " m-line index=" << aTrackPair.mLevel
-                << " type=" << aTrack.GetMediaType()
-                << " direction=" << aTrack.GetDirection());
-
-  if (receiving) {
-    rv = CreateMediaPipelineReceiving(aTrackPair, aTrack,
-                                      level, rtpFlow, rtcpFlow, filter,
-                                      conduit);
-    if (NS_FAILED(rv))
-      return rv;
-  } else {
-    rv = CreateMediaPipelineSending(aTrackPair, aTrack,
-                                    level, rtpFlow, rtcpFlow, filter,
-                                    conduit);
-    if (NS_FAILED(rv))
-      return rv;
-  }
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::CreateMediaPipelineReceiving(
-    const JsepTrackPair& aTrackPair,
-    const JsepTrack& aTrack,
-    size_t aLevel,
-    RefPtr<TransportFlow> aRtpFlow,
-    RefPtr<TransportFlow> aRtcpFlow,
-    nsAutoPtr<MediaPipelineFilter> aFilter,
-    const RefPtr<MediaSessionConduit>& aConduit)
-{
-  // We will error out earlier if this isn't here.
-  RefPtr<RemoteSourceStreamInfo> stream =
-      mPCMedia->GetRemoteStreamById(aTrack.GetStreamId());
-
-  RefPtr<MediaPipelineReceive> pipeline;
-
-  TrackID numericTrackId = stream->GetNumericTrackId(aTrack.GetTrackId());
-  MOZ_ASSERT(IsTrackIDExplicit(numericTrackId));
-
-  MOZ_MTLOG(ML_DEBUG, __FUNCTION__ << ": Creating pipeline for "
-            << numericTrackId << " -> " << aTrack.GetTrackId());
-
-  if (aTrack.GetMediaType() == SdpMediaSection::kAudio) {
-    pipeline = new MediaPipelineReceiveAudio(
-        mPC->GetHandle(),
-        mPC->GetMainThread().get(),
-        mPC->GetSTSThread(),
-        stream->GetMediaStream()->GetInputStream()->AsSourceStream(),
-        aTrack.GetTrackId(),
-        numericTrackId,
-        aLevel,
-        static_cast<AudioSessionConduit*>(aConduit.get()), // Ugly downcast.
-        aRtpFlow,
-        aRtcpFlow,
-        aFilter);
-  } else if (aTrack.GetMediaType() == SdpMediaSection::kVideo) {
-    pipeline = new MediaPipelineReceiveVideo(
-        mPC->GetHandle(),
-        mPC->GetMainThread().get(),
-        mPC->GetSTSThread(),
-        stream->GetMediaStream()->GetInputStream()->AsSourceStream(),
-        aTrack.GetTrackId(),
-        numericTrackId,
-        aLevel,
-        static_cast<VideoSessionConduit*>(aConduit.get()), // Ugly downcast.
-        aRtpFlow,
-        aRtcpFlow,
-        aFilter);
-  } else {
-    MOZ_ASSERT(false);
-    MOZ_MTLOG(ML_ERROR, "Invalid media type in CreateMediaPipelineReceiving");
-    return NS_ERROR_FAILURE;
-  }
-
-  nsresult rv = pipeline->Init();
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Couldn't initialize receiving pipeline");
-    return rv;
-  }
-
-  rv = stream->StorePipeline(aTrack.GetTrackId(),
-                             RefPtr<MediaPipeline>(pipeline));
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Couldn't store receiving pipeline " <<
-                        static_cast<unsigned>(rv));
-    return rv;
-  }
-
-  stream->SyncPipeline(pipeline);
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::CreateMediaPipelineSending(
-    const JsepTrackPair& aTrackPair,
-    const JsepTrack& aTrack,
-    size_t aLevel,
-    RefPtr<TransportFlow> aRtpFlow,
-    RefPtr<TransportFlow> aRtcpFlow,
-    nsAutoPtr<MediaPipelineFilter> aFilter,
-    const RefPtr<MediaSessionConduit>& aConduit)
-{
-  nsresult rv;
-
-  // This is checked earlier
-  RefPtr<LocalSourceStreamInfo> stream =
-      mPCMedia->GetLocalStreamById(aTrack.GetStreamId());
-
-  dom::MediaStreamTrack* track =
-    stream->GetTrackById(aTrack.GetTrackId());
-  MOZ_ASSERT(track);
-
-  // Now we have all the pieces, create the pipeline
-  RefPtr<MediaPipelineTransmit> pipeline = new MediaPipelineTransmit(
-      mPC->GetHandle(),
-      mPC->GetMainThread().get(),
-      mPC->GetSTSThread(),
-      track,
-      aTrack.GetTrackId(),
-      aLevel,
-      aConduit,
-      aRtpFlow,
-      aRtcpFlow,
-      aFilter);
-
-  // implement checking for peerIdentity (where failure == black/silence)
-  nsIDocument* doc = mPC->GetWindow()->GetExtantDoc();
-  if (doc) {
-    pipeline->UpdateSinkIdentity_m(track,
-                                   doc->NodePrincipal(),
-                                   mPC->GetPeerIdentity());
-  } else {
-    MOZ_MTLOG(ML_ERROR, "Cannot initialize pipeline without attached doc");
-    return NS_ERROR_FAILURE; // Don't remove this till we know it's safe.
-  }
-
-  rv = pipeline->Init();
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Couldn't initialize sending pipeline");
-    return rv;
-  }
-
-  rv = stream->StorePipeline(aTrack.GetTrackId(),
-                             RefPtr<MediaPipeline>(pipeline));
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Couldn't store receiving pipeline " <<
-                        static_cast<unsigned>(rv));
-    return rv;
-  }
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::GetOrCreateAudioConduit(
-    const JsepTrackPair& aTrackPair,
-    const JsepTrack& aTrack,
-    RefPtr<MediaSessionConduit>* aConduitp)
-{
-
-  if (!aTrack.GetNegotiatedDetails()) {
-    MOZ_ASSERT(false, "Track is missing negotiated details");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  bool receiving = aTrack.GetDirection() == sdp::kRecv;
-
-  RefPtr<AudioSessionConduit> conduit =
-    mPCMedia->GetAudioConduit(aTrackPair.mLevel);
-
-  if (!conduit) {
-    conduit = AudioSessionConduit::Create();
-    if (!conduit) {
-      MOZ_MTLOG(ML_ERROR, "Could not create audio conduit");
-      return NS_ERROR_FAILURE;
-    }
-
-    mPCMedia->AddAudioConduit(aTrackPair.mLevel, conduit);
-  }
-
-  PtrVector<AudioCodecConfig> configs;
-  nsresult rv = NegotiatedDetailsToAudioCodecConfigs(
-      *aTrack.GetNegotiatedDetails(), &configs);
-
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Failed to convert JsepCodecDescriptions to "
-                        "AudioCodecConfigs.");
-    return rv;
-  }
-
-  if (configs.values.empty()) {
-    MOZ_MTLOG(ML_ERROR, "Can't set up a conduit with 0 codecs");
-    return NS_ERROR_FAILURE;
-  }
-
-  if (receiving) {
-    auto error = conduit->ConfigureRecvMediaCodecs(configs.values);
-
-    if (error) {
-      MOZ_MTLOG(ML_ERROR, "ConfigureRecvMediaCodecs failed: " << error);
-      return NS_ERROR_FAILURE;
-    }
-
-    if (!aTrackPair.mSending) {
-      // No send track, but we still need to configure an SSRC for receiver
-      // reports.
-      if (!conduit->SetLocalSSRCs(std::vector<unsigned int>(1,aTrackPair.mRecvonlySsrc))) {
-        MOZ_MTLOG(ML_ERROR, "SetLocalSSRC failed");
-        return NS_ERROR_FAILURE;
-      }
-    }
-  } else {
-    auto ssrcs = aTrack.GetSsrcs();
-    if (!ssrcs.empty()) {
-      if (!conduit->SetLocalSSRCs(ssrcs)) {
-        MOZ_MTLOG(ML_ERROR, "SetLocalSSRCs failed");
-        return NS_ERROR_FAILURE;
-      }
-    }
-
-    conduit->SetLocalCNAME(aTrack.GetCNAME().c_str());
-
-    if (configs.values.size() > 1
-        && configs.values.back()->mName == "telephone-event") {
-      // we have a telephone event codec, so we need to make sure
-      // the dynamic pt is set properly
-      conduit->SetDtmfPayloadType(configs.values.back()->mType,
-                                  configs.values.back()->mFreq);
-    }
-
-    auto error = conduit->ConfigureSendMediaCodec(configs.values[0]);
-    if (error) {
-      MOZ_MTLOG(ML_ERROR, "ConfigureSendMediaCodec failed: " << error);
-      return NS_ERROR_FAILURE;
-    }
-
-    const SdpExtmapAttributeList::Extmap* audioLevelExt =
-        aTrack.GetNegotiatedDetails()->GetExt(
-            "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
-
-    if (audioLevelExt) {
-      MOZ_MTLOG(ML_DEBUG, "Calling EnableAudioLevelExtension");
-      error = conduit->EnableAudioLevelExtension(true, audioLevelExt->entry);
-
-      if (error) {
-        MOZ_MTLOG(ML_ERROR, "EnableAudioLevelExtension failed: " << error);
-        return NS_ERROR_FAILURE;
-      }
-    }
-  }
-
-  *aConduitp = conduit;
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::GetOrCreateVideoConduit(
-    const JsepTrackPair& aTrackPair,
-    const JsepTrack& aTrack,
-    RefPtr<MediaSessionConduit>* aConduitp)
-{
-  if (!aTrack.GetNegotiatedDetails()) {
-    MOZ_ASSERT(false, "Track is missing negotiated details");
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  bool receiving = aTrack.GetDirection() == sdp::kRecv;
-
-  RefPtr<VideoSessionConduit> conduit =
-    mPCMedia->GetVideoConduit(aTrackPair.mLevel);
-
-  if (!conduit) {
-    conduit = VideoSessionConduit::Create(mPCMedia->mCall);
-    if (!conduit) {
-      MOZ_MTLOG(ML_ERROR, "Could not create video conduit");
-      return NS_ERROR_FAILURE;
-    }
-
-    mPCMedia->AddVideoConduit(aTrackPair.mLevel, conduit);
-  }
-
-  PtrVector<VideoCodecConfig> configs;
-  nsresult rv = NegotiatedDetailsToVideoCodecConfigs(
-      *aTrack.GetNegotiatedDetails(), &configs);
-
-  if (NS_FAILED(rv)) {
-    MOZ_MTLOG(ML_ERROR, "Failed to convert JsepCodecDescriptions to "
-                        "VideoCodecConfigs.");
-    return rv;
-  }
-
-  if (configs.values.empty()) {
-    MOZ_MTLOG(ML_ERROR, "Can't set up a conduit with 0 codecs");
-    return NS_ERROR_FAILURE;
-  }
-
-  const std::vector<uint32_t>* ssrcs;
-
-  const JsepTrackNegotiatedDetails* details = aTrack.GetNegotiatedDetails();
-  std::vector<webrtc::RtpExtension> extmaps;
-  if (details) {
-    // @@NG read extmap from track
-    details->ForEachRTPHeaderExtension(
-      [&extmaps](const SdpExtmapAttributeList::Extmap& extmap)
-    {
-      extmaps.emplace_back(extmap.extensionname,extmap.entry);
-    });
-  }
-
-  if (receiving) {
-    // NOTE(pkerr) - the Call API requires the both local_ssrc and remote_ssrc be
-    // set to a non-zero value or the CreateVideo...Stream call will fail.
-    if (aTrackPair.mSending) {
-      ssrcs = &aTrackPair.mSending->GetSsrcs();
-      if (!ssrcs->empty()) {
-        conduit->SetLocalSSRCs(*ssrcs);
-      }
-    } else {
-      // No send track, but we still need to configure an SSRC for receiver
-      // reports.
-      if (!conduit->SetLocalSSRCs(std::vector<unsigned int>(1,aTrackPair.mRecvonlySsrc))) {
-        MOZ_MTLOG(ML_ERROR, "SetLocalSSRCs failed");
-        return NS_ERROR_FAILURE;
-      }
-    }
-
-    ssrcs = &aTrack.GetSsrcs();
-    // NOTE(pkerr) - this is new behavior. Needed because the CreateVideoReceiveStream
-    // method of the Call API will assert (in debug) and fail if a value is not provided
-    // for the remote_ssrc that will be used by the far-end sender.
-    if (!ssrcs->empty()) {
-      conduit->SetRemoteSSRC(ssrcs->front());
-    }
-
-    if (!extmaps.empty()) {
-      conduit->SetLocalRTPExtensions(false, extmaps);
-    }
-    auto error = conduit->ConfigureRecvMediaCodecs(configs.values);
-    if (error) {
-      MOZ_MTLOG(ML_ERROR, "ConfigureRecvMediaCodecs failed: " << error);
-      return NS_ERROR_FAILURE;
-    }
-  } else { //Create a send side
-    // For now we only expect to have one ssrc per local track.
-    ssrcs = &aTrack.GetSsrcs();
-    if (ssrcs->empty()) {
-      MOZ_MTLOG(ML_ERROR, "No SSRC set for send track");
-      return NS_ERROR_FAILURE;
-    }
-
-    if (!conduit->SetLocalSSRCs(*ssrcs)) {
-      MOZ_MTLOG(ML_ERROR, "SetLocalSSRC failed");
-      return NS_ERROR_FAILURE;
-    }
-
-    conduit->SetLocalCNAME(aTrack.GetCNAME().c_str());
-
-    rv = ConfigureVideoCodecMode(aTrack, *conduit);
-    if (NS_FAILED(rv)) {
-      return rv;
-    }
-
-    if (!extmaps.empty()) {
-      conduit->SetLocalRTPExtensions(true, extmaps);
-    }
-    auto error = conduit->ConfigureSendMediaCodec(configs.values[0]);
-    if (error) {
-      MOZ_MTLOG(ML_ERROR, "ConfigureSendMediaCodec failed: " << error);
-      return NS_ERROR_FAILURE;
-    }
-  }
-
-  *aConduitp = conduit;
-
-  return NS_OK;
-}
-
-nsresult
-MediaPipelineFactory::ConfigureVideoCodecMode(const JsepTrack& aTrack,
-                                              VideoSessionConduit& aConduit)
-{
-  RefPtr<LocalSourceStreamInfo> stream =
-    mPCMedia->GetLocalStreamByTrackId(aTrack.GetTrackId());
-
-  //get video track
-  RefPtr<mozilla::dom::MediaStreamTrack> track =
-    stream->GetTrackById(aTrack.GetTrackId());
-
-  RefPtr<mozilla::dom::VideoStreamTrack> videotrack =
-    track->AsVideoStreamTrack();
-
-  if (!videotrack) {
-    MOZ_MTLOG(ML_ERROR, "video track not available");
-    return NS_ERROR_FAILURE;
-  }
-
-  dom::MediaSourceEnum source = videotrack->GetSource().GetMediaSource();
-  webrtc::VideoCodecMode mode = webrtc::kRealtimeVideo;
-  switch (source) {
-    case dom::MediaSourceEnum::Browser:
-    case dom::MediaSourceEnum::Screen:
-    case dom::MediaSourceEnum::Application:
-    case dom::MediaSourceEnum::Window:
-      mode = webrtc::kScreensharing;
-      break;
-
-    case dom::MediaSourceEnum::Camera:
-    default:
-      mode = webrtc::kRealtimeVideo;
-      break;
-  }
-
-  auto error = aConduit.ConfigureCodecMode(mode);
-  if (error) {
-    MOZ_MTLOG(ML_ERROR, "ConfigureCodecMode failed: " << error);
-    return NS_ERROR_FAILURE;
-  }
-
-  return NS_OK;
-}
-
-
-} // namespace mozilla
deleted file mode 100644
--- a/media/webrtc/signaling/src/peerconnection/MediaPipelineFactory.h
+++ /dev/null
@@ -1,78 +0,0 @@
-/* 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/. */
-#ifndef _MEDIAPIPELINEFACTORY_H_
-#define _MEDIAPIPELINEFACTORY_H_
-
-#include "MediaConduitInterface.h"
-#include "PeerConnectionMedia.h"
-#include "transportflow.h"
-
-#include "signaling/src/jsep/JsepTrack.h"
-#include "mozilla/RefPtr.h"
-#include "mozilla/UniquePtr.h"
-
-namespace mozilla {
-
-class MediaPipelineFactory
-{
-public:
-  explicit MediaPipelineFactory(PeerConnectionMedia* aPCMedia)
-      : mPCMedia(aPCMedia), mPC(aPCMedia->GetPC())
-  {
-  }
-
-  nsresult CreateOrUpdateMediaPipeline(const JsepTrackPair& aTrackPair,
-                                       const JsepTrack& aTrack);
-
-private:
-  nsresult CreateMediaPipelineReceiving(
-      const JsepTrackPair& aTrackPair,
-      const JsepTrack& aTrack,
-      size_t level,
-      RefPtr<TransportFlow> aRtpFlow,
-      RefPtr<TransportFlow> aRtcpFlow,
-      nsAutoPtr<MediaPipelineFilter> filter,
-      const RefPtr<MediaSessionConduit>& aConduit);
-
-  nsresult CreateMediaPipelineSending(
-      const JsepTrackPair& aTrackPair,
-      const JsepTrack& aTrack,
-      size_t level,
-      RefPtr<TransportFlow> aRtpFlow,
-      RefPtr<TransportFlow> aRtcpFlow,
-      nsAutoPtr<MediaPipelineFilter> filter,
-      const RefPtr<MediaSessionConduit>& aConduit);
-
-  nsresult GetOrCreateAudioConduit(const JsepTrackPair& aTrackPair,
-                                   const JsepTrack& aTrack,
-                                   RefPtr<MediaSessionConduit>* aConduitp);
-
-  nsresult GetOrCreateVideoConduit(const JsepTrackPair& aTrackPair,
-                                   const JsepTrack& aTrack,
-                                   RefPtr<MediaSessionConduit>* aConduitp);
-
-  nsresult CreateOrGetTransportFlow(size_t aLevel, bool aIsRtcp,
-                                    const JsepTransport& transport,
-                                    RefPtr<TransportFlow>* out);
-
-  nsresult GetTransportParameters(const JsepTrackPair& aTrackPair,
-                                  const JsepTrack& aTrack,
-                                  size_t* aLevelOut,
-                                  RefPtr<TransportFlow>* aRtpOut,
-                                  RefPtr<TransportFlow>* aRtcpOut,
-                                  nsAutoPtr<MediaPipelineFilter>* aFilterOut);
-
-  nsresult ConfigureVideoCodecMode(const JsepTrack& aTrack,
-                                   VideoSessionConduit& aConduit);
-
-private:
-  // Not owned, and assumed to exist as long as the factory.
-  // The factory is a transient object, so this is fairly easy.
-  PeerConnectionMedia* mPCMedia;
-  PeerConnectionImpl* mPC;
-};
-
-} // namespace mozilla
-
-#endif
deleted file mode 100644
--- a/media/webrtc/signaling/src/peerconnection/MediaStreamList.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-/* 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/. */
-
-#include "CSFLog.h"
-#include "base/basictypes.h"
-#include "MediaStreamList.h"
-#include "mozilla/dom/MediaStreamListBinding.h"
-#include "nsIScriptGlobalObject.h"
-#include "PeerConnectionImpl.h"
-#include "PeerConnectionMedia.h"
-
-namespace mozilla {
-namespace dom {
-
-MediaStreamList::MediaStreamList(PeerConnectionImpl* peerConnection,
-                                 StreamType type)
-  : mPeerConnection(peerConnection),
-    mType(type)
-{
-}
-
-MediaStreamList::~MediaStreamList()
-{
-}
-
-NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(MediaStreamList)
-
-NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaStreamList)
-NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaStreamList)
-NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamList)
-  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
-  NS_INTERFACE_MAP_ENTRY(nsISupports)
-NS_INTERFACE_MAP_END
-
-JSObject*
-MediaStreamList::WrapObject(JSContext* cx, JS::Handle<JSObject*> aGivenProto)
-{
-  return MediaStreamListBinding::Wrap(cx, this, aGivenProto);
-}
-
-nsISupports*
-MediaStreamList::GetParentObject()
-{
-  return mPeerConnection->GetWindow();
-}
-
-template<class T>
-static DOMMediaStream*
-GetStreamFromInfo(T* info, bool& found)
-{
-  if (!info) {
-    found = false;
-    return nullptr;
-  }
-
-  found = true;
-  return info->GetMediaStream();
-}
-
-DOMMediaStream*
-MediaStreamList::IndexedGetter(uint32_t index, bool& found)
-{
-  if (!mPeerConnection->media()) { // PeerConnection closed
-    found = false;
-    return nullptr;
-  }
-  if (mType == Local) {
-    return GetStreamFromInfo(mPeerConnection->media()->
-      GetLocalStreamByIndex(index), found);
-  }
-
-  return GetStreamFromInfo(mPeerConnection->media()->
-    GetRemoteStreamByIndex(index), found);
-}
-
-uint32_t
-MediaStreamList::Length()
-{
-  if (!mPeerConnection->media()) { // PeerConnection closed
-    return 0;
-  }
-  return mType == Local ? mPeerConnection->media()->LocalStreamsLength() :
-      mPeerConnection->media()->RemoteStreamsLength();
-}
-
-} // namespace dom
-} // namespace mozilla
deleted file mode 100644
--- a/media/webrtc/signaling/src/peerconnection/MediaStreamList.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/* 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/. */
-
-#ifndef MediaStreamList_h__
-#define MediaStreamList_h__
-
-#include "mozilla/ErrorResult.h"
-#include "nsISupportsImpl.h"
-#include "nsAutoPtr.h"
-#include "nsWrapperCache.h"
-
-#ifdef USE_FAKE_MEDIA_STREAMS
-#include "FakeMediaStreams.h"
-#else
-#include "DOMMediaStream.h"
-#endif
-
-namespace mozilla {
-class PeerConnectionImpl;
-namespace dom {
-
-class MediaStreamList : public nsISupports,
-                        public nsWrapperCache
-{
-public:
-  enum StreamType {
-    Local,
-    Remote
-  };
-
-  MediaStreamList(PeerConnectionImpl* peerConnection, StreamType type);
-
-  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
-  NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MediaStreamList)
-
-  virtual JSObject* WrapObject(JSContext *cx, JS::Handle<JSObject*> aGivenProto)
-    override;
-  nsISupports* GetParentObject();
-
-  DOMMediaStream* IndexedGetter(uint32_t index, bool& found);
-  uint32_t Length();
-
-private:
-  virtual ~MediaStreamList();
-
-  RefPtr<PeerConnectionImpl> mPeerConnection;
-  StreamType mType;
-};
-
-} // namespace dom
-} // namespace mozilla
-
-#endif // MediaStreamList_h__
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -28,31 +28,35 @@
 #include "nsISocketTransportService.h"
 #include "nsIConsoleService.h"
 #include "nsThreadUtils.h"
 #include "nsIPrefService.h"
 #include "nsIPrefBranch.h"
 #include "nsProxyRelease.h"
 #include "nsQueryObject.h"
 #include "prtime.h"
+#include "MediaEngine.h"
 
 #include "AudioConduit.h"
 #include "VideoConduit.h"
 #include "runnable_utils.h"
 #include "PeerConnectionCtx.h"
 #include "PeerConnectionImpl.h"
 #include "PeerConnectionMedia.h"
+#include "RemoteTrackSource.h"
 #include "nsDOMDataChannelDeclarations.h"
 #include "dtlsidentity.h"
 #include "signaling/src/sdp/SdpAttribute.h"
 
 #include "signaling/src/jsep/JsepTrack.h"
 #include "signaling/src/jsep/JsepSession.h"
 #include "signaling/src/jsep/JsepSessionImpl.h"
 
+#include "signaling/src/mediapipeline/MediaPipeline.h"
+
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/Sprintf.h"
 
 #ifdef XP_WIN
 // We need to undef the MS macro for nsIDocument::CreateEvent
 #ifdef CreateEvent
 #undef CreateEvent
 #endif
@@ -77,24 +81,24 @@
 #include "nsIURLParser.h"
 #include "nsIDOMDataChannel.h"
 #include "NullPrincipal.h"
 #include "mozilla/PeerIdentity.h"
 #include "mozilla/dom/RTCCertificate.h"
 #include "mozilla/dom/RTCConfigurationBinding.h"
 #include "mozilla/dom/RTCDTMFSenderBinding.h"
 #include "mozilla/dom/RTCDTMFToneChangeEvent.h"
+#include "mozilla/dom/RTCRtpReceiverBinding.h"
 #include "mozilla/dom/RTCRtpSenderBinding.h"
 #include "mozilla/dom/RTCStatsReportBinding.h"
 #include "mozilla/dom/RTCPeerConnectionBinding.h"
 #include "mozilla/dom/PeerConnectionImplBinding.h"
 #include "mozilla/dom/DataChannelBinding.h"
 #include "mozilla/dom/PerformanceTiming.h"
 #include "mozilla/dom/PluginCrashedEvent.h"
-#include "MediaStreamList.h"
 #include "MediaStreamTrack.h"
 #include "AudioStreamTrack.h"
 #include "VideoStreamTrack.h"
 #include "nsIScriptGlobalObject.h"
 #include "MediaStreamGraph.h"
 #include "DOMMediaStream.h"
 #include "rlogconnector.h"
 #include "WebrtcGlobalInformation.h"
@@ -241,16 +245,35 @@ RTCStatsQuery::RTCStatsQuery(bool intern
 
 RTCStatsQuery::~RTCStatsQuery() {
   MOZ_ASSERT(NS_IsMainThread());
 }
 
 
 NS_IMPL_ISUPPORTS0(PeerConnectionImpl)
 
+already_AddRefed<PeerConnectionImpl>
+PeerConnectionImpl::Constructor(const dom::GlobalObject& aGlobal, ErrorResult& rv)
+{
+  RefPtr<PeerConnectionImpl> pc = new PeerConnectionImpl(&aGlobal);
+
+  CSFLogDebug(logTag, "Created PeerConnection: %p", pc.get());
+
+  return pc.forget();
+}
+
+PeerConnectionImpl* PeerConnectionImpl::CreatePeerConnection()
+{
+  PeerConnectionImpl *pc = new PeerConnectionImpl();
+
+  CSFLogDebug(logTag, "Created PeerConnection: %p", pc);
+
+  return pc;
+}
+
 bool
 PeerConnectionImpl::WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto,
                                JS::MutableHandle<JSObject*> aReflector)
 {
   return PeerConnectionImplBinding::Wrap(aCx, this, aGivenProto, aReflector);
 }
 
@@ -309,17 +332,16 @@ PeerConnectionImpl::PeerConnectionImpl(c
   , mAllowIceLinkLocal(false)
   , mForceIceTcp(false)
   , mMedia(nullptr)
   , mUuidGen(MakeUnique<PCUuidGenerator>())
   , mHaveConfiguredCodecs(false)
   , mHaveDataStream(false)
   , mAddCandidateErrorCount(0)
   , mTrickle(true) // TODO(ekr@rtfm.com): Use pref
-  , mNegotiationNeeded(false)
   , mPrivateWindow(false)
   , mActiveOnWindow(false)
 {
   MOZ_ASSERT(NS_IsMainThread());
   auto log = RLogConnector::CreateInstance();
   if (aGlobal) {
     mWindow = do_QueryInterface(aGlobal->GetAsSupports());
     if (IsPrivateBrowsing(mWindow)) {
@@ -382,48 +404,28 @@ PeerConnectionImpl::~PeerConnectionImpl(
   // running at once
 
   // Right now, we delete PeerConnectionCtx at XPCOM shutdown only, but we
   // probably want to shut it down more aggressively to save memory.  We
   // could shut down here when there are no uses.  It might be more optimal
   // to release off a timer (and XPCOM Shutdown) to avoid churn
 }
 
-already_AddRefed<DOMMediaStream>
+OwningNonNull<DOMMediaStream>
 PeerConnectionImpl::MakeMediaStream()
 {
   MediaStreamGraph* graph =
     MediaStreamGraph::GetInstance(MediaStreamGraph::AUDIO_THREAD_DRIVER, GetWindow());
 
   RefPtr<DOMMediaStream> stream =
     DOMMediaStream::CreateSourceStreamAsInput(GetWindow(), graph);
 
   CSFLogDebug(logTag, "Created media stream %p, inner: %p", stream.get(), stream->GetInputStream());
 
-  return stream.forget();
-}
-
-nsresult
-PeerConnectionImpl::CreateRemoteSourceStreamInfo(RefPtr<RemoteSourceStreamInfo>*
-                                                 aInfo,
-                                                 const std::string& aStreamID)
-{
-  MOZ_ASSERT(aInfo);
-  PC_AUTO_ENTER_API_CALL_NO_CHECK();
-
-  RefPtr<DOMMediaStream> stream = MakeMediaStream();
-  if (!stream) {
-    return NS_ERROR_FAILURE;
-  }
-
-  RefPtr<RemoteSourceStreamInfo> remote;
-  remote = new RemoteSourceStreamInfo(stream.forget(), mMedia, aStreamID);
-  *aInfo = remote;
-
-  return NS_OK;
+  return *stream;
 }
 
 /**
  * In JS, an RTCConfiguration looks like this:
  *
  * { "iceServers": [ { url:"stun:stun.example.org" },
  *                   { url:"turn:turn.example.org?transport=udp",
  *                     username: "jib", credential:"mypass"} ] }
@@ -1127,31 +1129,28 @@ nsresult
 PeerConnectionImpl::GetDatachannelParameters(
     uint32_t* channels,
     uint16_t* localport,
     uint16_t* remoteport,
     uint32_t* remotemaxmessagesize,
     bool*     mmsset,
     uint16_t* level) const {
 
-  auto trackPairs = mJsepSession->GetNegotiatedTrackPairs();
-  for (auto& trackPair : trackPairs) {
+  for (const auto& transceiver : mJsepSession->GetTransceivers()) {
     bool sendDataChannel =
-      trackPair.mSending &&
-      trackPair.mSending->GetMediaType() == SdpMediaSection::kApplication;
+      transceiver->mSending.GetMediaType() == SdpMediaSection::kApplication;
     bool recvDataChannel =
-      trackPair.mReceiving &&
-      trackPair.mReceiving->GetMediaType() == SdpMediaSection::kApplication;
+      transceiver->mReceiving.GetMediaType() == SdpMediaSection::kApplication;
     (void)recvDataChannel;
     MOZ_ASSERT(sendDataChannel == recvDataChannel);
 
     if (sendDataChannel) {
       // This will release assert if there is no such index, and that's ok
       const JsepTrackEncoding& encoding =
-        trackPair.mSending->GetNegotiatedDetails()->GetEncoding(0);
+        transceiver->mSending.GetNegotiatedDetails()->GetEncoding(0);
 
       if (encoding.GetCodecs().empty()) {
         CSFLogError(logTag, "%s: Negotiated m=application with no codec. "
                             "This is likely to be broken.",
                             __FUNCTION__);
         return NS_ERROR_FAILURE;
       }
 
@@ -1181,81 +1180,134 @@ PeerConnectionImpl::GetDatachannelParame
         *localport =
           static_cast<const JsepApplicationCodecDescription*>(codec)->mLocalPort;
         *remoteport =
           static_cast<const JsepApplicationCodecDescription*>(codec)->mRemotePort;
         *remotemaxmessagesize = static_cast<const JsepApplicationCodecDescription*>
           (codec)->mRemoteMaxMessageSize;
         *mmsset = static_cast<const JsepApplicationCodecDescription*>
           (codec)->mRemoteMMSSet;
-        if (trackPair.HasBundleLevel()) {
-          *level = static_cast<uint16_t>(trackPair.BundleLevel());
+        if (transceiver->HasBundleLevel()) {
+          *level = static_cast<uint16_t>(transceiver->BundleLevel());
         } else {
-          *level = static_cast<uint16_t>(trackPair.mLevel);
+          *level = static_cast<uint16_t>(transceiver->GetLevel());
         }
         return NS_OK;
       }
     }
   }
 
   *channels = 0;
   *localport = 0;
   *remoteport = 0;
   *remotemaxmessagesize = 0;
   *mmsset = false;
   *level = 0;
   return NS_ERROR_FAILURE;
 }
 
-/* static */
-void
-PeerConnectionImpl::DeferredAddTrackToJsepSession(
-    const std::string& pcHandle,
-    SdpMediaSection::MediaType type,
-    const std::string& streamId,
-    const std::string& trackId)
-{
-  PeerConnectionWrapper wrapper(pcHandle);
-
-  if (wrapper.impl()) {
-    if (!PeerConnectionCtx::GetInstance()->isReady()) {
-      MOZ_CRASH("Why is DeferredAddTrackToJsepSession being executed when the "
-                "PeerConnectionCtx isn't ready?");
-    }
-    wrapper.impl()->AddTrackToJsepSession(type, streamId, trackId);
-  }
-}
-
 nsresult
-PeerConnectionImpl::AddTrackToJsepSession(SdpMediaSection::MediaType type,
-                                          const std::string& streamId,
-                                          const std::string& trackId)
+PeerConnectionImpl::AddTransceiverToJsepSession(
+    RefPtr<JsepTransceiver>& transceiver)
 {
   nsresult res = ConfigureJsepSessionCodecs();
   if (NS_FAILED(res)) {
     CSFLogError(logTag, "Failed to configure codecs");
     return res;
   }
 
-  res = mJsepSession->AddTrack(
-      new JsepTrack(type, streamId, trackId, sdp::kSend));
+  res = mJsepSession->AddTransceiver(transceiver);
 
   if (NS_FAILED(res)) {
     std::string errorString = mJsepSession->GetLastError();
     CSFLogError(logTag, "%s (%s) : pc = %s, error = %s",
                 __FUNCTION__,
-                type == SdpMediaSection::kAudio ? "audio" : "video",
+                transceiver->mSending.GetMediaType() == SdpMediaSection::kAudio ?
+                  "audio" : "video",
                 mHandle.c_str(),
                 errorString.c_str());
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
+already_AddRefed<TransceiverImpl>
+PeerConnectionImpl::CreateTransceiverImpl(
+    RefPtr<JsepTransceiver>& aJsepTransceiver,
+    RefPtr<dom::MediaStreamTrack> aSendTrack,
+    ErrorResult& aRv)
+{
+  // TODO: Maybe this should be done in PeerConnectionMedia?
+  if (aSendTrack) {
+    aSendTrack->AddPrincipalChangeObserver(this);
+  }
+
+  OwningNonNull<DOMMediaStream> receiveStream =
+    CreateReceiveStreamWithTrack(aJsepTransceiver->mReceiving.GetMediaType());
+
+  RefPtr<TransceiverImpl> transceiverImpl;
+
+  aRv = mMedia->AddTransceiver(aJsepTransceiver,
+                               receiveStream,
+                               aSendTrack,
+                               &transceiverImpl);
+
+  return transceiverImpl.forget();
+}
+
+already_AddRefed<TransceiverImpl>
+PeerConnectionImpl::CreateTransceiverImpl(
+    const nsAString& aKind,
+    RefPtr<dom::MediaStreamTrack> aSendTrack,
+    ErrorResult& jrv)
+{
+  SdpMediaSection::MediaType type;
+  if (aKind.EqualsASCII("audio")) {
+    type = SdpMediaSection::MediaType::kAudio;
+  } else if (aKind.EqualsASCII("video")) {
+    type = SdpMediaSection::MediaType::kVideo;
+  } else {
+    MOZ_ASSERT(false);
+    jrv = NS_ERROR_INVALID_ARG;
+    return nullptr;
+  }
+
+  RefPtr<JsepTransceiver> jsepTransceiver = new JsepTransceiver(type);
+
+  RefPtr<TransceiverImpl> transceiverImpl =
+    CreateTransceiverImpl(jsepTransceiver, aSendTrack, jrv);
+
+  if (jrv.Failed()) {
+    // Would be nice if we could peek at the rv without stealing it, so we
+    // could log...
+    CSFLogError(logTag, "%s: failed", __FUNCTION__);
+    return nullptr;
+  }
+
+  // Do this last, since it is not possible to roll back.
+  nsresult rv = AddTransceiverToJsepSession(jsepTransceiver);
+  if (NS_FAILED(rv)) {
+    CSFLogError(logTag, "%s: AddTransceiverToJsepSession failed, res=%u",
+                         __FUNCTION__,
+                         static_cast<unsigned>(rv));
+    jrv = rv;
+    return nullptr;
+  }
+
+  return transceiverImpl.forget();
+}
+
+bool
+PeerConnectionImpl::CheckNegotiationNeeded(ErrorResult &rv)
+{
+  MOZ_ASSERT(mSignalingState == PCImplSignalingState::SignalingStable);
+  return mJsepSession->CheckNegotiationNeeded();
+}
+
 nsresult
 PeerConnectionImpl::InitializeDataChannel()
 {
   PC_AUTO_ENTER_API_CALL(false);
   CSFLogDebug(logTag, "%s", __FUNCTION__);
 
   uint32_t channels = 0;
   uint16_t localport = 0;
@@ -1345,42 +1397,19 @@ PeerConnectionImpl::CreateDataChannel(co
     (aType == DataChannelConnection::PARTIAL_RELIABLE_TIMED ? aMaxTime : 0),
     nullptr, nullptr, aExternalNegotiated, aStream
   );
   NS_ENSURE_TRUE(dataChannel,NS_ERROR_FAILURE);
 
   CSFLogDebug(logTag, "%s: making DOMDataChannel", __FUNCTION__);
 
   if (!mHaveDataStream) {
-
-    std::string streamId;
-    std::string trackId;
-
-    // Generate random ids because these aren't linked to any local streams.
-    if (!mUuidGen->Generate(&streamId)) {
-      return NS_ERROR_FAILURE;
-    }
-    if (!mUuidGen->Generate(&trackId)) {
-      return NS_ERROR_FAILURE;
-    }
-
-    RefPtr<JsepTrack> track(new JsepTrack(
-          mozilla::SdpMediaSection::kApplication,
-          streamId,
-          trackId,
-          sdp::kSend));
-
-    rv = mJsepSession->AddTrack(track);
-    if (NS_FAILED(rv)) {
-      CSFLogError(logTag, "%s: Failed to add application track.",
-                          __FUNCTION__);
-      return rv;
-    }
+    mJsepSession->AddTransceiver(
+        new JsepTransceiver(SdpMediaSection::MediaType::kApplication));
     mHaveDataStream = true;
-    OnNegotiationNeeded();
   }
   nsIDOMDataChannel *retval;
   rv = NS_NewDOMDataChannel(dataChannel.forget(), mWindow, &retval);
   if (NS_FAILED(rv)) {
     return rv;
   }
   *aRetval = static_cast<nsDOMDataChannel*>(retval);
   return NS_OK;
@@ -1566,20 +1595,20 @@ PeerConnectionImpl::CreateOffer(const Js
         error = kInternalError;
     }
     std::string errorString = mJsepSession->GetLastError();
 
     CSFLogError(logTag, "%s: pc = %s, error = %s",
                 __FUNCTION__, mHandle.c_str(), errorString.c_str());
     pco->OnCreateOfferError(error, ObString(errorString.c_str()), rv);
   } else {
+    UpdateSignalingState();
     pco->OnCreateOfferSuccess(ObString(offer.c_str()), rv);
   }
 
-  UpdateSignalingState();
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PeerConnectionImpl::CreateAnswer()
 {
   PC_AUTO_ENTER_API_CALL(true);
 
@@ -1625,21 +1654,20 @@ PeerConnectionImpl::CreateAnswer()
         error = kInternalError;
     }
     std::string errorString = mJsepSession->GetLastError();
 
     CSFLogError(logTag, "%s: pc = %s, error = %s",
                 __FUNCTION__, mHandle.c_str(), errorString.c_str());
     pco->OnCreateAnswerError(error, ObString(errorString.c_str()), rv);
   } else {
+    UpdateSignalingState();
     pco->OnCreateAnswerSuccess(ObString(answer.c_str()), rv);
   }
 
-  UpdateSignalingState();
-
   return NS_OK;
 }
 
 nsresult
 PeerConnectionImpl::SetupIceRestart()
 {
   if (mMedia->IsIceRestarting()) {
     CSFLogError(logTag, "%s: ICE already restarting",
@@ -1757,20 +1785,20 @@ PeerConnectionImpl::SetLocalDescription(
         error = kInternalError;
     }
 
     std::string errorString = mJsepSession->GetLastError();
     CSFLogError(logTag, "%s: pc = %s, error = %s",
                 __FUNCTION__, mHandle.c_str(), errorString.c_str());
     pco->OnSetLocalDescriptionError(error, ObString(errorString.c_str()), rv);
   } else {
+    UpdateSignalingState(sdpType == mozilla::kJsepSdpRollback);
     pco->OnSetLocalDescriptionSuccess(rv);
   }
 
-  UpdateSignalingState(sdpType == mozilla::kJsepSdpRollback);
   return NS_OK;
 }
 
 static void DeferredSetRemote(const std::string& aPcHandle,
                               int32_t aAction,
                               const std::string& aSdp) {
   PeerConnectionWrapper wrapper(aPcHandle);
 
@@ -1778,243 +1806,51 @@ static void DeferredSetRemote(const std:
     if (!PeerConnectionCtx::GetInstance()->isReady()) {
       MOZ_CRASH("Why is DeferredSetRemote being executed when the "
                 "PeerConnectionCtx isn't ready?");
     }
     wrapper.impl()->SetRemoteDescription(aAction, aSdp.c_str());
   }
 }
 
-static void StartTrack(MediaStream* aSource,
-                       TrackID aTrackId,
-                       nsAutoPtr<MediaSegment>&& aSegment) {
-  class Message : public ControlMessage {
-   public:
-    Message(MediaStream* aStream,
-            TrackID aTrack,
-            nsAutoPtr<MediaSegment>&& aSegment)
-      : ControlMessage(aStream),
-        track_id_(aTrack),
-        segment_(aSegment) {}
-
-    virtual void Run() override {
-      TrackRate track_rate = segment_->GetType() == MediaSegment::AUDIO ?
-        WEBRTC_DEFAULT_SAMPLE_RATE : mStream->GraphRate();
-      StreamTime current_end = mStream->GetTracksEnd();
-      TrackTicks current_ticks =
-        mStream->TimeToTicksRoundUp(track_rate, current_end);
-
-      // Add a track 'now' to avoid possible underrun, especially if we add
-      // a track "later".
-
-      if (current_end != 0L) {
-        CSFLogDebug(logTag, "added track @ %u -> %f",
-                    static_cast<unsigned>(current_end),
-                    mStream->StreamTimeToSeconds(current_end));
-      }
-
-      // To avoid assertions, we need to insert a dummy segment that covers up
-      // to the "start" time for the track
-      segment_->AppendNullData(current_ticks);
-      if (segment_->GetType() == MediaSegment::AUDIO) {
-        mStream->AsSourceStream()->AddAudioTrack(
-            track_id_,
-            WEBRTC_DEFAULT_SAMPLE_RATE,
-            0,
-            static_cast<AudioSegment*>(segment_.forget()));
-      } else {
-        mStream->AsSourceStream()->AddTrack(track_id_, 0, segment_.forget());
-      }
-    }
-   private:
-    TrackID track_id_;
-    nsAutoPtr<MediaSegment> segment_;
-  };
-
-  aSource->GraphImpl()->AppendMessage(
-      MakeUnique<Message>(aSource, aTrackId, Move(aSegment)));
-  CSFLogInfo(logTag, "Dispatched track-add for track id %u on stream %p",
-             aTrackId, aSource);
-}
-
-
 nsresult
-PeerConnectionImpl::CreateNewRemoteTracks(RefPtr<PeerConnectionObserver>& aPco)
+PeerConnectionImpl::FireOnTrackEvents(RefPtr<PeerConnectionObserver>& aPco)
 {
-  JSErrorResult jrv;
-
-  std::vector<RefPtr<JsepTrack>> newTracks =
-    mJsepSession->GetRemoteTracksAdded();
-
-  // Group new tracks by stream id
-  std::map<std::string, std::vector<RefPtr<JsepTrack>>> tracksByStreamId;
-  for (auto track : newTracks) {
-    if (track->GetMediaType() == mozilla::SdpMediaSection::kApplication) {
+  for (auto& track : mJsepSession->GetRemoteTracksAdded()) {
+    if (track.GetMediaType() == mozilla::SdpMediaSection::kApplication) {
       // Ignore datachannel
       continue;
     }
 
-    tracksByStreamId[track->GetStreamId()].push_back(track);
-  }
-
-  for (auto& id : tracksByStreamId) {
-    std::string streamId = id.first;
-    std::vector<RefPtr<JsepTrack>>& tracks = id.second;
-
-    bool newStream = false;
-    RefPtr<RemoteSourceStreamInfo> info =
-      mMedia->GetRemoteStreamById(streamId);
-    if (!info) {
-      newStream = true;
-      nsresult nrv = CreateRemoteSourceStreamInfo(&info, streamId);
-      if (NS_FAILED(nrv)) {
-        aPco->OnSetRemoteDescriptionError(
-            kInternalError,
-            ObString("CreateRemoteSourceStreamInfo failed"),
-            jrv);
-        return nrv;
-      }
-
-      nrv = mMedia->AddRemoteStream(info);
-      if (NS_FAILED(nrv)) {
-        aPco->OnSetRemoteDescriptionError(
-            kInternalError,
-            ObString("AddRemoteStream failed"),
-            jrv);
-        return nrv;
-      }
-
-      CSFLogDebug(logTag, "Added remote stream %s", info->GetId().c_str());
-
-      info->GetMediaStream()->AssignId(NS_ConvertUTF8toUTF16(streamId.c_str()));
-      info->GetMediaStream()->SetLogicalStreamStartTime(
-          info->GetMediaStream()->GetPlaybackStream()->GetCurrentTime());
-    }
-
-    Sequence<OwningNonNull<DOMMediaStream>> streams;
-    if (!streams.AppendElement(OwningNonNull<DOMMediaStream>(
-            *info->GetMediaStream()),
-            fallible)) {
+    if (track.GetTrackId().empty()) {
       MOZ_ASSERT(false);
-      return NS_ERROR_FAILURE;
-    }
-
-    // Set the principal used for creating the tracks. This makes the stream
-    // data (audio/video samples) accessible to the receiving page. We're
-    // only certain that privacy hasn't been requested if we're connected.
-    nsCOMPtr<nsIPrincipal> principal;
-    nsIDocument* doc = GetWindow()->GetExtantDoc();
-    MOZ_ASSERT(doc);
-    if (mDtlsConnected && !PrivacyRequested()) {
-      principal = doc->NodePrincipal();
-    } else {
-      // we're either certain that we need isolation for the streams, OR
-      // we're not sure and we can fix the stream in SetDtlsConnected
-      principal =  NullPrincipal::CreateWithInheritedAttributes(doc->NodePrincipal());
+      return NS_ERROR_UNEXPECTED;
     }
 
-    // We need to select unique ids, just use max + 1
-    TrackID maxTrackId = 0;
-    {
-      nsTArray<RefPtr<dom::MediaStreamTrack>> domTracks;
-      info->GetMediaStream()->GetTracks(domTracks);
-      for (auto& track : domTracks) {
-        maxTrackId = std::max(maxTrackId, track->mTrackID);
-      }
+    nsString trackId = NS_ConvertUTF8toUTF16(track.GetTrackId().c_str());
+
+    dom::Sequence<nsString> streamIds;
+    for (const std::string& streamId : track.GetStreamIds()) {
+      // If this fails, oh well.
+      streamIds.AppendElement(
+          NS_ConvertASCIItoUTF16(streamId.c_str()), fallible);
     }
 
-    for (RefPtr<JsepTrack>& track : tracks) {
-      std::string webrtcTrackId(track->GetTrackId());
-      if (!info->HasTrack(webrtcTrackId)) {
-        RefPtr<RemoteTrackSource> source =
-          new RemoteTrackSource(principal, nsString());
-        TrackID trackID = ++maxTrackId;
-        RefPtr<MediaStreamTrack> domTrack;
-        nsAutoPtr<MediaSegment> segment;
-        if (track->GetMediaType() == SdpMediaSection::kAudio) {
-          domTrack =
-            info->GetMediaStream()->CreateDOMTrack(trackID,
-                                                   MediaSegment::AUDIO,
-                                                   source);
-          info->GetMediaStream()->AddTrackInternal(domTrack);
-          segment = new AudioSegment;
-        } else {
-          domTrack =
-            info->GetMediaStream()->CreateDOMTrack(trackID,
-                                                   MediaSegment::VIDEO,
-                                                   source);
-          info->GetMediaStream()->AddTrackInternal(domTrack);
-          segment = new VideoSegment;
-        }
-
-        StartTrack(info->GetMediaStream()->GetInputStream()->AsSourceStream(),
-                   trackID, Move(segment));
-        info->AddTrack(webrtcTrackId, domTrack);
-        CSFLogDebug(logTag, "Added remote track %s/%s",
-                    info->GetId().c_str(), webrtcTrackId.c_str());
-
-        domTrack->AssignId(NS_ConvertUTF8toUTF16(webrtcTrackId.c_str()));
-        aPco->OnAddTrack(*domTrack, streams, jrv);
-        if (jrv.Failed()) {
-          CSFLogError(logTag, ": OnAddTrack(%s) failed! Error: %u",
-                      webrtcTrackId.c_str(),
-                      jrv.ErrorCodeAsInt());
-        }
-      }
-    }
-
-    if (newStream) {
-      aPco->OnAddStream(*info->GetMediaStream(), jrv);
-      if (jrv.Failed()) {
-        CSFLogError(logTag, ": OnAddStream() failed! Error: %u",
-                    jrv.ErrorCodeAsInt());
-      }
+    JSErrorResult jrv;
+    aPco->OnTrack(trackId, streamIds, jrv);
+    if (jrv.Failed()) {
+      CSFLogError(logTag, ": OnTrack(%s) failed! Error: %u",
+          track.GetTrackId().c_str(),
+          jrv.ErrorCodeAsInt());
     }
   }
+
   return NS_OK;
 }
 
-void
-PeerConnectionImpl::RemoveOldRemoteTracks(RefPtr<PeerConnectionObserver>& aPco)
-{
-  JSErrorResult jrv;
-
-  std::vector<RefPtr<JsepTrack>> removedTracks =
-    mJsepSession->GetRemoteTracksRemoved();
-
-  for (auto& removedTrack : removedTracks) {
-    const std::string& streamId = removedTrack->GetStreamId();
-    const std::string& trackId = removedTrack->GetTrackId();
-
-    RefPtr<RemoteSourceStreamInfo> info = mMedia->GetRemoteStreamById(streamId);
-    if (!info) {
-      MOZ_ASSERT(false, "A stream/track was removed that wasn't in PCMedia. "
-                        "This is a bug.");
-      continue;
-    }
-
-    mMedia->RemoveRemoteTrack(streamId, trackId);
-
-    DOMMediaStream* stream = info->GetMediaStream();
-    nsTArray<RefPtr<MediaStreamTrack>> tracks;
-    stream->GetTracks(tracks);
-    for (auto& track : tracks) {
-      if (PeerConnectionImpl::GetTrackId(*track) == trackId) {
-        aPco->OnRemoveTrack(*track, jrv);
-        break;
-      }
-    }
-
-    // We might be holding the last ref, but that's ok.
-    if (!info->GetTrackCount()) {
-      aPco->OnRemoveStream(*stream, jrv);
-    }
-  }
-}
-
 NS_IMETHODIMP
 PeerConnectionImpl::SetRemoteDescription(int32_t action, const char* aSDP)
 {
   PC_AUTO_ENTER_API_CALL(true);
 
   if (!aSDP) {
     CSFLogError(logTag, "%s - aSDP is NULL", __FUNCTION__);
     return NS_ERROR_FAILURE;
@@ -2063,16 +1899,17 @@ PeerConnectionImpl::SetRemoteDescription
     case IPeerConnection::kActionRollback:
       sdpType = mozilla::kJsepSdpRollback;
       break;
     default:
       MOZ_ASSERT(false);
       return NS_ERROR_FAILURE;
   }
 
+  size_t transceiverCount = mJsepSession->GetTransceivers().size();
   nsresult nrv = mJsepSession->SetRemoteDescription(sdpType,
                                                     mRemoteRequestedSDP);
   if (NS_FAILED(nrv)) {
     Error error;
     switch (nrv) {
       case NS_ERROR_INVALID_ARG:
         error = kInvalidSessionDescription;
         break;
@@ -2083,29 +1920,74 @@ PeerConnectionImpl::SetRemoteDescription
         error = kInternalError;
     }
 
     std::string errorString = mJsepSession->GetLastError();
     CSFLogError(logTag, "%s: pc = %s, error = %s",
                 __FUNCTION__, mHandle.c_str(), errorString.c_str());
     pco->OnSetRemoteDescriptionError(error, ObString(errorString.c_str()), jrv);
   } else {
-    nrv = CreateNewRemoteTracks(pco);
+    // Iterate over the JSEP transceivers that were just created
+    while (transceiverCount < mJsepSession->GetTransceivers().size()) {
+      RefPtr<JsepTransceiver> jsepTransceiver =
+        mJsepSession->GetTransceivers()[transceiverCount++];
+
+      if (jsepTransceiver->mReceiving.GetMediaType() ==
+          SdpMediaSection::MediaType::kApplication) {
+        continue;
+      }
+
+      // Audio or video transceiver, need to tell JS about it.
+      RefPtr<TransceiverImpl> transceiverImpl =
+        CreateTransceiverImpl(jsepTransceiver, nullptr, jrv);
+      if (jrv.Failed()) {
+        return NS_ERROR_FAILURE;
+      }
+
+      const JsepTrack& receiving(jsepTransceiver->mReceiving);
+      CSFLogInfo(logTag, "%s: pc = %s, asking JS to create transceiver for %s",
+                  __FUNCTION__, mHandle.c_str(), receiving.GetTrackId().c_str());
+      switch (receiving.GetMediaType()) {
+        case SdpMediaSection::MediaType::kAudio:
+          pco->OnTransceiverNeeded(
+              NS_ConvertASCIItoUTF16("audio"), *transceiverImpl, jrv);
+          break;
+        case SdpMediaSection::MediaType::kVideo:
+          pco->OnTransceiverNeeded(
+              NS_ConvertASCIItoUTF16("video"), *transceiverImpl, jrv);
+          break;
+        default:
+          MOZ_RELEASE_ASSERT(false);
+      }
+
+      if (jrv.Failed()) {
+        nsresult rv = jrv.StealNSResult();
+        CSFLogError(logTag, "%s: pc = %s, OnTransceiverNeeded failed. "
+                    "This should never happen. rv = %d",
+                    __FUNCTION__, mHandle.c_str(), static_cast<int>(rv));
+        MOZ_CRASH();
+        return NS_ERROR_FAILURE;
+      }
+    }
+
+    UpdateSignalingState(sdpType == mozilla::kJsepSdpRollback);
+
+    // This needs to come first, because it is what prompts JS to sync their
+    // transceivers with ours.
+    pco->OnSetRemoteDescriptionSuccess(jrv);
+
+    nrv = FireOnTrackEvents(pco);
     if (NS_FAILED(nrv)) {
       // aPco was already notified, just return early.
       return NS_OK;
     }
 
-    RemoveOldRemoteTracks(pco);
-
-    pco->OnSetRemoteDescriptionSuccess(jrv);
     startCallTelem();
   }
 
-  UpdateSignalingState(sdpType == mozilla::kJsepSdpRollback);
   return NS_OK;
 }
 
 // WebRTC uses highres time relative to the UNIX epoch (Jan 1, 1970, UTC).
 
 nsresult
 PeerConnectionImpl::GetTimeSinceEpoch(DOMHighResTimeStamp *result) {
   MOZ_ASSERT(NS_IsMainThread());
@@ -2295,173 +2177,77 @@ PeerConnectionImpl::PrincipalChanged(Med
   nsIDocument* doc = GetWindow()->GetExtantDoc();
   if (doc) {
     mMedia->UpdateSinkIdentity_m(aTrack, doc->NodePrincipal(), mPeerIdentity);
   } else {
     CSFLogInfo(logTag, "Can't update sink principal; document gone");
   }
 }
 
-std::string
-PeerConnectionImpl::GetTrackId(const MediaStreamTrack& aTrack)
-{
-  nsString wideTrackId;
-  aTrack.GetId(wideTrackId);
-  return NS_ConvertUTF16toUTF8(wideTrackId).get();
-}
-
-std::string
-PeerConnectionImpl::GetStreamId(const DOMMediaStream& aStream)
-{
-  nsString wideStreamId;
-  aStream.GetId(wideStreamId);
-  return NS_ConvertUTF16toUTF8(wideStreamId).get();
-}
-
 void
 PeerConnectionImpl::OnMediaError(const std::string& aError)
 {
   CSFLogError(logTag, "Encountered media error! %s", aError.c_str());
   // TODO: Let content know about this somehow.
 }
 
 nsresult
 PeerConnectionImpl::AddTrack(MediaStreamTrack& aTrack,
                              const Sequence<OwningNonNull<DOMMediaStream>>& aStreams)
 {
   PC_AUTO_ENTER_API_CALL(true);
-
-  if (!aStreams.Length()) {
-    CSFLogError(logTag, "%s: At least one stream arg required", __FUNCTION__);
-    return NS_ERROR_FAILURE;
-  }
-
-  return AddTrack(aTrack, aStreams[0]);
-}
-
-nsresult
-PeerConnectionImpl::AddTrack(MediaStreamTrack& aTrack,
-                             DOMMediaStream& aMediaStream)
-{
-  std::string streamId = PeerConnectionImpl::GetStreamId(aMediaStream);
-  std::string trackId = PeerConnectionImpl::GetTrackId(aTrack);
-  nsresult res = mMedia->AddTrack(aMediaStream, streamId, aTrack, trackId);
-  if (NS_FAILED(res)) {
-    return res;
-  }
-
-  CSFLogDebug(logTag, "Added track (%s) to stream %s",
-                      trackId.c_str(), streamId.c_str());
-
-  aTrack.AddPrincipalChangeObserver(this);
-  PrincipalChanged(&aTrack);
-
-  if (aTrack.AsAudioStreamTrack()) {
-    res = AddTrackToJsepSession(SdpMediaSection::kAudio, streamId, trackId);
-    if (NS_FAILED(res)) {
-      return res;
-    }
-  }
-
-  if (aTrack.AsVideoStreamTrack()) {
-    if (!Preferences::GetBool("media.peerconnection.video.enabled", true)) {
-      // Before this code was moved, this would silently ignore just like it
-      // does now. Is this actually what we want to do?
-      return NS_OK;
-    }
-
-    res = AddTrackToJsepSession(SdpMediaSection::kVideo, streamId, trackId);
-    if (NS_FAILED(res)) {
-      return res;
-    }
-  }
-  OnNegotiationNeeded();
   return NS_OK;
 }
 
-RefPtr<MediaPipeline>
-PeerConnectionImpl::GetMediaPipelineForTrack(MediaStreamTrack& aRecvTrack)
-{
-  for (size_t i = 0; i < mMedia->RemoteStreamsLength(); ++i) {
-    if (mMedia->GetRemoteStreamByIndex(i)->GetMediaStream()->
-        HasTrack(aRecvTrack)) {
-      auto& pipelines = mMedia->GetRemoteStreamByIndex(i)->GetPipelines();
-      std::string trackId = PeerConnectionImpl::GetTrackId(aRecvTrack);
-      auto it = pipelines.find(trackId);
-      if (it != pipelines.end()) {
-        return it->second;
-      }
-    }
-  }
-
-  return nullptr;
-}
-
 nsresult
 PeerConnectionImpl::AddRIDExtension(MediaStreamTrack& aRecvTrack,
                                     unsigned short aExtensionId)
 {
-  RefPtr<MediaPipeline> pipeline = GetMediaPipelineForTrack(aRecvTrack);
-  if (pipeline) {
-    pipeline->AddRIDExtension_m(aExtensionId);
-  }
-  return NS_OK;
+  return mMedia->AddRIDExtension(aRecvTrack, aExtensionId);
 }
 
 nsresult
 PeerConnectionImpl::AddRIDFilter(MediaStreamTrack& aRecvTrack,
                                  const nsAString& aRid)
 {
-  RefPtr<MediaPipeline> pipeline = GetMediaPipelineForTrack(aRecvTrack);
-  if (pipeline) {
-    pipeline->AddRIDFilter_m(NS_ConvertUTF16toUTF8(aRid).get());
-  }
-  return NS_OK;
+  return mMedia->AddRIDFilter(aRecvTrack, aRid);
 }
 
 NS_IMETHODIMP
 PeerConnectionImpl::RemoveTrack(MediaStreamTrack& aTrack) {
   PC_AUTO_ENTER_API_CALL(true);
 
-  std::string trackId = PeerConnectionImpl::GetTrackId(aTrack);
-
-  nsString wideTrackId;
-  aTrack.GetId(wideTrackId);
-  for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
-    if (mDTMFStates[i].mTrackId == wideTrackId) {
-      mDTMFStates[i].mSendTimer->Cancel();
-      mDTMFStates.RemoveElementAt(i);
+  std::vector<RefPtr<TransceiverImpl>>& transceivers =
+    mMedia->GetTransceivers();
+
+  nsresult rv = NS_ERROR_INVALID_ARG;
+
+  for (RefPtr<TransceiverImpl>& transceiver : transceivers) {
+    if (transceiver->HasSendTrack(&aTrack)) {
+      // TODO(bug XXXXX): Move DTMF stuff to TransceiverImpl
+      for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
+        if (mDTMFStates[i].mTransceiver.get() == transceiver.get()) {
+          mDTMFStates[i].mSendTimer->Cancel();
+          mDTMFStates.RemoveElementAt(i);
+          break;
+        }
+      }
+
+      rv = transceiver->UpdateSendTrack(nullptr);
       break;
     }
   }
 
-  RefPtr<LocalSourceStreamInfo> info = media()->GetLocalStreamByTrackId(trackId);
-
-  if (!info) {
-    CSFLogError(logTag, "%s: Unknown stream", __FUNCTION__);
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  nsresult rv =
-    mJsepSession->RemoveTrack(info->GetId(), trackId);
-
   if (NS_FAILED(rv)) {
-    CSFLogError(logTag, "%s: Unknown stream/track ids %s %s",
-                __FUNCTION__,
-                info->GetId().c_str(),
-                trackId.c_str());
+    CSFLogError(logTag, "Error updating send track on transceiver");
     return rv;
   }
 
-  media()->RemoveLocalTrack(info->GetId(), trackId);
-
   aTrack.RemovePrincipalChangeObserver(this);
 
-  OnNegotiationNeeded();
-
   return NS_OK;
 }
 
 static int GetDTMFToneCode(uint16_t c)
 {
   const char* DTMF_TONECODES = "0123456789*#ABCD";
 
   if (c == ',') {
@@ -2469,71 +2255,87 @@ static int GetDTMFToneCode(uint16_t c)
     return -1;
   }
 
   const char* i = strchr(DTMF_TONECODES, c);
   MOZ_ASSERT(i);
   return i - DTMF_TONECODES;
 }
 
+OwningNonNull<DOMMediaStream>
+PeerConnectionImpl::CreateReceiveStreamWithTrack(
+    SdpMediaSection::MediaType type) {
+
+  OwningNonNull<DOMMediaStream> stream = MakeMediaStream();
+
+  // Set the principal used for creating the tracks. This makes the stream
+  // data (audio/video samples) accessible to the receiving page. We're
+  // only certain that privacy hasn't been requested if we're connected.
+  nsCOMPtr<nsIPrincipal> principal;
+  nsIDocument* doc = GetWindow()->GetExtantDoc();
+  MOZ_ASSERT(doc);
+  if (mDtlsConnected && !PrivacyRequested()) {
+    principal = doc->NodePrincipal();
+  } else {
+    // we're either certain that we need isolation for the streams, OR
+    // we're not sure and we can fix the stream in SetDtlsConnected
+    principal =  NullPrincipal::CreateWithInheritedAttributes(doc->NodePrincipal());
+  }
+
+  RefPtr<RemoteTrackSource> source =
+    new RemoteTrackSource(principal, nsString());
+
+  RefPtr<MediaStreamTrack> track;
+  switch (type) {
+    case SdpMediaSection::MediaType::kAudio:
+      track = stream->CreateDOMTrack(kAudioTrack, MediaSegment::AUDIO, source);
+      break;
+    case SdpMediaSection::MediaType::kVideo:
+      track = stream->CreateDOMTrack(kVideoTrack, MediaSegment::VIDEO, source);
+      break;
+    default:
+      MOZ_ASSERT(false, "Bad media kind; our JS passed some garbage");
+  }
+  stream->AddTrackInternal(track);
+
+  return stream;
+}
+
 NS_IMETHODIMP
-PeerConnectionImpl::InsertDTMF(mozilla::dom::RTCRtpSender& sender,
+PeerConnectionImpl::InsertDTMF(TransceiverImpl& transceiver,
                                const nsAString& tones, uint32_t duration,
                                uint32_t interToneGap) {
   PC_AUTO_ENTER_API_CALL(false);
 
   // Check values passed in from PeerConnection.js
   MOZ_ASSERT(duration >= 40, "duration must be at least 40");
   MOZ_ASSERT(duration <= 6000, "duration must be at most 6000");
   MOZ_ASSERT(interToneGap >= 30, "interToneGap must be at least 30");
 
   JSErrorResult jrv;
 
-  // Retrieve track
-  RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
-  if (jrv.Failed()) {
-    NS_WARNING("Failed to retrieve track for RTCRtpSender!");
-    return jrv.StealNSResult();
-  }
-
-  nsString senderTrackId;
-  mst->GetId(senderTrackId);
-
   // Attempt to locate state for the DTMFSender
   DTMFState* state = nullptr;
   for (auto& dtmfState : mDTMFStates) {
-    if (dtmfState.mTrackId == senderTrackId) {
+    if (dtmfState.mTransceiver.get() == &transceiver) {
       state = &dtmfState;
       break;
     }
   }
 
   // No state yet, create a new one
   if (!state) {
     state = mDTMFStates.AppendElement();
-    state->mPeerConnectionImpl = this;
-    state->mTrackId = senderTrackId;
+    state->mPCObserver = mPCObserver;
+    state->mTransceiver = &transceiver;
     state->mSendTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
     MOZ_ASSERT(state->mSendTimer);
   }
   MOZ_ASSERT(state);
 
-  auto trackPairs = mJsepSession->GetNegotiatedTrackPairs();
-  state->mLevel = -1;
-  for (auto& trackPair : trackPairs) {
-    if (state->mTrackId.EqualsASCII(trackPair.mSending->GetTrackId().c_str())) {
-      if (trackPair.HasBundleLevel()) {
-        state->mLevel = trackPair.BundleLevel();
-      } else {
-        state->mLevel = trackPair.mLevel;
-      }
-      break;
-    }
-  }
-
   state->mTones = tones;
   state->mDuration = duration;
   state->mInterToneGap = interToneGap;
   if (!state->mTones.IsEmpty()) {
     state->mSendTimer->InitWithNamedFuncCallback(DTMFSendTimerCallback_m, state, 0,
                                                  nsITimer::TYPE_ONE_SHOT,
                                                  "DTMFSendTimerCallback_m");
   }
@@ -2549,220 +2351,83 @@ PeerConnectionImpl::GetDTMFToneBuffer(mo
 
   // Retrieve track
   RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
   if (jrv.Failed()) {
     NS_WARNING("Failed to retrieve track for RTCRtpSender!");
     return jrv.StealNSResult();
   }
 
-  nsString senderTrackId;
-  mst->GetId(senderTrackId);
-
   // Attempt to locate state for the DTMFSender
   for (auto& dtmfState : mDTMFStates) {
-    if (dtmfState.mTrackId == senderTrackId) {
+    if (dtmfState.mTransceiver->HasSendTrack(mst)) {
       outToneBuffer = dtmfState.mTones;
       break;
     }
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
-PeerConnectionImpl::ReplaceTrack(MediaStreamTrack& aThisTrack,
-                                 MediaStreamTrack& aWithTrack) {
+PeerConnectionImpl::ReplaceTrackNoRenegotiation(TransceiverImpl& aTransceiver,
+                                                MediaStreamTrack* aWithTrack) {
   PC_AUTO_ENTER_API_CALL(true);
 
-  nsString trackId;
-  aThisTrack.GetId(trackId);
+  RefPtr<dom::MediaStreamTrack> oldSendTrack(aTransceiver.GetSendTrack());
+  if (oldSendTrack) {
+    oldSendTrack->RemovePrincipalChangeObserver(this);
+  }
+
+  nsresult rv = aTransceiver.UpdateSendTrack(aWithTrack);
+
+  if (NS_FAILED(rv)) {
+    CSFLogError(logTag,
+                "Failed to update transceiver: %d", static_cast<int>(rv));
+    return rv;
+  }
 
   for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
-    if (mDTMFStates[i].mTrackId == trackId) {
+    if (mDTMFStates[i].mTransceiver.get() == &aTransceiver) {
       mDTMFStates[i].mSendTimer->Cancel();
       mDTMFStates.RemoveElementAt(i);
       break;
     }
   }
 
-  RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
-  if (!pco) {
-    return NS_ERROR_UNEXPECTED;
-  }
-  JSErrorResult jrv;
-
-  if (&aThisTrack == &aWithTrack) {
-    pco->OnReplaceTrackSuccess(jrv);
-    if (jrv.Failed()) {
-      CSFLogError(logTag, "Error firing replaceTrack success callback");
-      return NS_ERROR_UNEXPECTED;
-    }
-    return NS_OK;
-  }
-
-  nsString thisKind;
-  aThisTrack.GetKind(thisKind);
-  nsString withKind;
-  aWithTrack.GetKind(withKind);
-
-  if (thisKind != withKind) {
-    pco->OnReplaceTrackError(kIncompatibleMediaStreamTrack,
-                             ObString(mJsepSession->GetLastError().c_str()),
-                             jrv);
-    if (jrv.Failed()) {
-      CSFLogError(logTag, "Error firing replaceTrack success callback");
-      return NS_ERROR_UNEXPECTED;
-    }
-    return NS_OK;
-  }
-  std::string origTrackId = PeerConnectionImpl::GetTrackId(aThisTrack);
-  std::string newTrackId = PeerConnectionImpl::GetTrackId(aWithTrack);
-
-  RefPtr<LocalSourceStreamInfo> info =
-    media()->GetLocalStreamByTrackId(origTrackId);
-  if (!info) {
-    CSFLogError(logTag, "Could not find stream from trackId");
-    return NS_ERROR_UNEXPECTED;
+  if (aWithTrack) {
+    aWithTrack->AddPrincipalChangeObserver(this);
+    PrincipalChanged(aWithTrack);
   }
 
-  std::string origStreamId = info->GetId();
-  std::string newStreamId =
-    PeerConnectionImpl::GetStreamId(*aWithTrack.mOwningStream);
-
-  nsresult rv = mJsepSession->ReplaceTrack(origStreamId,
-                                           origTrackId,
-                                           newStreamId,
-                                           newTrackId);
-  if (NS_FAILED(rv)) {
-    pco->OnReplaceTrackError(kInvalidMediastreamTrack,
-                             ObString(mJsepSession->GetLastError().c_str()),
-                             jrv);
-    if (jrv.Failed()) {
-      CSFLogError(logTag, "Error firing replaceTrack error callback");
-      return NS_ERROR_UNEXPECTED;
-    }
-    return NS_OK;
-  }
-
-  rv = media()->ReplaceTrack(origStreamId,
-                             origTrackId,
-                             aWithTrack,
-                             newStreamId,
-                             newTrackId);
-
-  if (NS_FAILED(rv)) {
-    CSFLogError(logTag, "Unexpected error in ReplaceTrack: %d",
-                        static_cast<int>(rv));
-    pco->OnReplaceTrackError(kInvalidMediastreamTrack,
-                             ObString("Failed to replace track"),
-                             jrv);
-    if (jrv.Failed()) {
-      CSFLogError(logTag, "Error firing replaceTrack error callback");
-      return NS_ERROR_UNEXPECTED;
-    }
-    return NS_OK;
-  }
-  aThisTrack.RemovePrincipalChangeObserver(this);
-  aWithTrack.AddPrincipalChangeObserver(this);
-  PrincipalChanged(&aWithTrack);
-
   // We update the media pipelines here so we can apply different codec
   // settings for different sources (e.g. screensharing as opposed to camera.)
   // TODO: We should probably only do this if the source has in fact changed.
 
-  if (NS_FAILED((rv = mMedia->UpdateMediaPipelines(*mJsepSession)))) {
+  if (NS_FAILED((rv = mMedia->UpdateMediaPipelines()))) {
     CSFLogError(logTag, "Error Updating MediaPipelines");
     return rv;
   }
 
+  RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
+  if (!pco) {
+    CSFLogError(logTag, "Error getting PC observer");
+    return NS_ERROR_UNEXPECTED;
+  }
+  JSErrorResult jrv;
+
   pco->OnReplaceTrackSuccess(jrv);
   if (jrv.Failed()) {
     CSFLogError(logTag, "Error firing replaceTrack success callback");
     return NS_ERROR_UNEXPECTED;
   }
 
   return NS_OK;
 }
 
-NS_IMETHODIMP
-PeerConnectionImpl::SetParameters(MediaStreamTrack& aTrack,
-                                  const RTCRtpParameters& aParameters) {
-  PC_AUTO_ENTER_API_CALL(true);
-
-  std::vector<JsepTrack::JsConstraints> constraints;
-  if (aParameters.mEncodings.WasPassed()) {
-    for (auto& encoding : aParameters.mEncodings.Value()) {
-      JsepTrack::JsConstraints constraint;
-      if (encoding.mRid.WasPassed()) {
-        constraint.rid = NS_ConvertUTF16toUTF8(encoding.mRid.Value()).get();
-      }
-      if (encoding.mMaxBitrate.WasPassed()) {
-        constraint.constraints.maxBr = encoding.mMaxBitrate.Value();
-      }
-      constraint.constraints.scaleDownBy = encoding.mScaleResolutionDownBy;
-      constraints.push_back(constraint);
-    }
-  }
-  return SetParameters(aTrack, constraints);
-}
-
-nsresult
-PeerConnectionImpl::SetParameters(
-    MediaStreamTrack& aTrack,
-    const std::vector<JsepTrack::JsConstraints>& aConstraints)
-{
-  std::string trackId = PeerConnectionImpl::GetTrackId(aTrack);
-  RefPtr<LocalSourceStreamInfo> info = media()->GetLocalStreamByTrackId(trackId);
-  if (!info) {
-    CSFLogError(logTag, "%s: Unknown stream", __FUNCTION__);
-    return NS_ERROR_INVALID_ARG;
-  }
-  std::string streamId = info->GetId();
-
-  return mJsepSession->SetParameters(streamId, trackId, aConstraints);
-}
-
-NS_IMETHODIMP
-PeerConnectionImpl::GetParameters(MediaStreamTrack& aTrack,
-                                  RTCRtpParameters& aOutParameters) {
-  PC_AUTO_ENTER_API_CALL(true);
-
-  std::vector<JsepTrack::JsConstraints> constraints;
-  nsresult rv = GetParameters(aTrack, &constraints);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  aOutParameters.mEncodings.Construct();
-  for (auto& constraint : constraints) {
-    RTCRtpEncodingParameters encoding;
-    encoding.mRid.Construct(NS_ConvertASCIItoUTF16(constraint.rid.c_str()));
-    encoding.mMaxBitrate.Construct(constraint.constraints.maxBr);
-    encoding.mScaleResolutionDownBy = constraint.constraints.scaleDownBy;
-    aOutParameters.mEncodings.Value().AppendElement(Move(encoding), fallible);
-  }
-  return NS_OK;
-}
-
-nsresult
-PeerConnectionImpl::GetParameters(
-    MediaStreamTrack& aTrack,
-    std::vector<JsepTrack::JsConstraints>* aOutConstraints)
-{
-  std::string trackId = PeerConnectionImpl::GetTrackId(aTrack);
-  RefPtr<LocalSourceStreamInfo> info = media()->GetLocalStreamByTrackId(trackId);
-  if (!info) {
-    CSFLogError(logTag, "%s: Unknown stream", __FUNCTION__);
-    return NS_ERROR_INVALID_ARG;
-  }
-  std::string streamId = info->GetId();
-
-  return mJsepSession->GetParameters(streamId, trackId, aOutConstraints);
-}
-
 nsresult
 PeerConnectionImpl::CalculateFingerprint(
     const std::string& algorithm,
     std::vector<uint8_t>* fingerprint) const {
   uint8_t buf[DtlsIdentity::HASH_ALGORITHM_MAX_LENGTH];
   size_t len = 0;
 
   MOZ_ASSERT(fingerprint);
@@ -3056,20 +2721,20 @@ void
 PeerConnectionImpl::ShutdownMedia()
 {
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
 
   if (!mMedia)
     return;
 
   // before we destroy references to local tracks, detach from them
-  for(uint32_t i = 0; i < media()->LocalStreamsLength(); ++i) {
-    LocalSourceStreamInfo *info = media()->GetLocalStreamByIndex(i);
-    for (const auto& pair : info->GetMediaStreamTracks()) {
-      pair.second->RemovePrincipalChangeObserver(this);
+  for(RefPtr<TransceiverImpl>& transceiver : mMedia->GetTransceivers()) {
+    RefPtr<dom::MediaStreamTrack> track = transceiver->GetSendTrack();
+    if (track) {
+      track->RemovePrincipalChangeObserver(this);
     }
   }
 
   // End of call to be recorded in Telemetry
   if (!mStartTime.IsNull()){
     TimeDuration timeDelta = TimeStamp::Now() - mStartTime;
     Telemetry::Accumulate(Telemetry::WEBRTC_CALL_DURATION,
                           timeDelta.ToSeconds());
@@ -3094,53 +2759,42 @@ PeerConnectionImpl::SetSignalingState_m(
       (aSignalingState == PCImplSignalingState::SignalingStable &&
        mSignalingState == PCImplSignalingState::SignalingHaveRemoteOffer &&
        !rollback)) {
     mMedia->EnsureTransports(*mJsepSession);
   }
 
   mSignalingState = aSignalingState;
 
-  bool fireNegotiationNeeded = false;
   if (mSignalingState == PCImplSignalingState::SignalingStable) {
     if (mMedia->GetIceRestartState() ==
             PeerConnectionMedia::ICE_RESTART_PROVISIONAL) {
       if (rollback) {
         RollbackIceRestart();
       } else {
         mMedia->CommitIceRestart();
       }
     }
 
-    // Either negotiation is done, or we've rolled back. In either case, we
-    // need to re-evaluate whether further negotiation is required.
-    mNegotiationNeeded = false;
     // If we're rolling back a local offer, we might need to remove some
-    // transports, but nothing further needs to be done.
+    // transports, and stomp some MediaPipeline setup, but nothing further
+    // needs to be done.
     mMedia->ActivateOrRemoveTransports(*mJsepSession, mForceIceTcp);
+    mMedia->UpdateTransceiverTransports(*mJsepSession);
+    if (NS_FAILED(mMedia->UpdateMediaPipelines())) {
+      CSFLogError(logTag, "Error Updating MediaPipelines");
+      NS_ASSERTION(false, "Error Updating MediaPipelines in SetSignalingState_m()");
+      // XXX what now?  Not much we can do but keep going, without major restructuring
+    }
+
     if (!rollback) {
-      if (NS_FAILED(mMedia->UpdateMediaPipelines(*mJsepSession))) {
-        CSFLogError(logTag, "Error Updating MediaPipelines");
-        NS_ASSERTION(false, "Error Updating MediaPipelines in SetSignalingState_m()");
-        // XXX what now?  Not much we can do but keep going, without major restructuring
-      }
       InitializeDataChannel();
       mMedia->StartIceChecks(*mJsepSession);
     }
 
-    if (!mJsepSession->AllLocalTracksAreAssigned()) {
-      CSFLogInfo(logTag, "Not all local tracks were assigned to an "
-                 "m-section, either because the offerer did not offer"
-                 " to receive enough tracks, or because tracks were "
-                 "added after CreateOffer/Answer, but before "
-                 "offer/answer completed. This requires "
-                 "renegotiation.");
-      fireNegotiationNeeded = true;
-    }
-
     // Telemetry: record info on the current state of streams/renegotiations/etc
     // Note: this code gets run on rollbacks as well!
 
     // Update the max channels used with each direction for each type
     uint16_t receiving[SdpMediaSection::kMediaTypes];
     uint16_t sending[SdpMediaSection::kMediaTypes];
     mJsepSession->CountTracks(receiving, sending);
     for (size_t i = 0; i < SdpMediaSection::kMediaTypes; i++) {
@@ -3163,28 +2817,21 @@ PeerConnectionImpl::SetSignalingState_m(
   }
 
   RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
   if (!pco) {
     return;
   }
   JSErrorResult rv;
   pco->OnStateChange(PCObserverStateType::SignalingState, rv);
-
-  if (fireNegotiationNeeded) {
-    // We don't use MaybeFireNegotiationNeeded here, since content might have
-    // already cased a transition from stable.
-    OnNegotiationNeeded();
-  }
 }
 
 void
 PeerConnectionImpl::UpdateSignalingState(bool rollback) {
-  mozilla::JsepSignalingState state =
-      mJsepSession->GetState();
+  mozilla::JsepSignalingState state = mJsepSession->GetState();
 
   PCImplSignalingState newState;
 
   switch(state) {
     case kJsepStateStable:
       newState = PCImplSignalingState::SignalingStable;
       break;
     case kJsepStateHaveLocalOffer:
@@ -3576,36 +3223,18 @@ PeerConnectionImpl::BuildStatsQuery_m(
       query->report->mLocalSdp.Construct(
           NS_ConvertASCIItoUTF16(localDescription.c_str()));
       query->report->mRemoteSdp.Construct(
           NS_ConvertASCIItoUTF16(remoteDescription.c_str()));
     }
   }
 
   // Gather up pipelines from mMedia so they may be inspected on STS
-
-  std::string trackId;
-  if (aSelector) {
-    trackId = PeerConnectionImpl::GetTrackId(*aSelector);
-  }
-
-  for (int i = 0, len = mMedia->LocalStreamsLength(); i < len; i++) {
-    for (auto pipeline : mMedia->GetLocalStreamByIndex(i)->GetPipelines()) {
-      if (!aSelector || pipeline.second->trackid() == trackId) {
-        query->pipelines.AppendElement(pipeline.second);
-      }
-    }
-  }
-  for (int i = 0, len = mMedia->RemoteStreamsLength(); i < len; i++) {
-    for (auto pipeline : mMedia->GetRemoteStreamByIndex(i)->GetPipelines()) {
-      if (!aSelector || pipeline.second->trackid() == trackId) {
-        query->pipelines.AppendElement(pipeline.second);
-      }
-    }
-  }
+  mMedia->GetTransmitPipelinesMatching(aSelector, &query->pipelines);
+  mMedia->GetReceivePipelinesMatching(aSelector, &query->pipelines);
 
   if (!aSelector) {
     query->grabAllLevels = true;
   }
 
   return rv;
 }
 
@@ -3709,17 +3338,17 @@ PeerConnectionImpl::ExecuteStatsQuery_s(
 
   for (size_t p = 0; p < query->pipelines.Length(); ++p) {
     const MediaPipeline& mp = *query->pipelines[p];
     bool isAudio = (mp.Conduit()->type() == MediaSessionConduit::AUDIO);
     nsString mediaType = isAudio ?
         NS_LITERAL_STRING("audio") : NS_LITERAL_STRING("video");
     nsString idstr = mediaType;
     idstr.AppendLiteral("_");
-    idstr.AppendInt(mp.level());
+    idstr.AppendInt((uint32_t)p);
 
     // TODO(@@NG):ssrcs handle Conduits having multiple stats at the same level
     // This is pending spec work
     // Gather pipeline stats.
     switch (mp.direction()) {
       case MediaPipeline::TRANSMIT: {
         nsString localId = NS_LITERAL_STRING("outbound_rtp_") + idstr;
         nsString remoteId;
@@ -3999,64 +3628,16 @@ void PeerConnectionImpl::DeliverStatsRep
 }
 
 void
 PeerConnectionImpl::RecordLongtermICEStatistics() {
   WebrtcGlobalInformation::StoreLongTermICEStatistics(*this);
 }
 
 void
-PeerConnectionImpl::OnNegotiationNeeded()
-{
-  if (mSignalingState != PCImplSignalingState::SignalingStable) {
-    // We will check whether we need to renegotiate when we reach stable again
-    return;
-  }
-
-  if (mNegotiationNeeded) {
-    return;
-  }
-
-  mNegotiationNeeded = true;
-
-  RUN_ON_THREAD(mThread,
-                WrapRunnableNM(&MaybeFireNegotiationNeeded_static, mHandle),
-                NS_DISPATCH_NORMAL);
-}
-
-/* static */
-void
-PeerConnectionImpl::MaybeFireNegotiationNeeded_static(
-    const std::string& pcHandle)
-{
-  PeerConnectionWrapper wrapper(pcHandle);
-  if (!wrapper.impl()) {
-    return;
-  }
-
-  wrapper.impl()->MaybeFireNegotiationNeeded();
-}
-
-void
-PeerConnectionImpl::MaybeFireNegotiationNeeded()
-{
-  if (!mNegotiationNeeded) {
-    return;
-  }
-
-  RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
-  if (!pco) {
-    return;
-  }
-
-  JSErrorResult rv;
-  pco->OnNegotiationNeeded(rv);
-}
-
-void
 PeerConnectionImpl::IceStreamReady(NrIceMediaStream *aStream)
 {
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
   MOZ_ASSERT(aStream);
 
   CSFLogDebug(logTag, "%s: %s", __FUNCTION__, aStream->name().c_str());
 }
 
@@ -4070,40 +3651,16 @@ PeerConnectionImpl::startCallTelem() {
   // Start time for calls
   mStartTime = TimeStamp::Now();
 
   // Increment session call counter
   // If we want to track Loop calls independently here, we need two histograms.
   Telemetry::Accumulate(Telemetry::WEBRTC_CALL_COUNT_2, 1);
 }
 
-NS_IMETHODIMP
-PeerConnectionImpl::GetLocalStreams(nsTArray<RefPtr<DOMMediaStream > >& result)
-{
-  PC_AUTO_ENTER_API_CALL_NO_CHECK();
-  for(uint32_t i=0; i < media()->LocalStreamsLength(); i++) {
-    LocalSourceStreamInfo *info = media()->GetLocalStreamByIndex(i);
-    NS_ENSURE_TRUE(info, NS_ERROR_UNEXPECTED);
-    result.AppendElement(info->GetMediaStream());
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-PeerConnectionImpl::GetRemoteStreams(nsTArray<RefPtr<DOMMediaStream > >& result)
-{
-  PC_AUTO_ENTER_API_CALL_NO_CHECK();
-  for(uint32_t i=0; i < media()->RemoteStreamsLength(); i++) {
-    RemoteSourceStreamInfo *info = media()->GetRemoteStreamByIndex(i);
-    NS_ENSURE_TRUE(info, NS_ERROR_UNEXPECTED);
-    result.AppendElement(info->GetMediaStream());
-  }
-  return NS_OK;
-}
-
 void
 PeerConnectionImpl::DTMFSendTimerCallback_m(nsITimer* timer, void* closure)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   auto state = static_cast<DTMFState*>(closure);
 
   nsString eventTone;
@@ -4121,41 +3678,36 @@ PeerConnectionImpl::DTMFSendTimerCallbac
                                                    "DTMFSendTimerCallback_m");
     } else {
       // Reset delay if necessary
       state->mSendTimer->InitWithNamedFuncCallback(DTMFSendTimerCallback_m, state,
                                                    state->mDuration + state->mInterToneGap,
                                                    nsITimer::TYPE_ONE_SHOT,
                                                    "DTMFSendTimerCallback_m");
 
-      RefPtr<AudioSessionConduit> conduit =
-        state->mPeerConnectionImpl->mMedia->GetAudioConduit(state->mLevel);
-
-      if (conduit) {
-        uint32_t duration = state->mDuration;
-        state->mPeerConnectionImpl->mSTSThread->Dispatch(WrapRunnableNM([conduit, tone, duration] () {
-            //Note: We default to channel 0, not inband, and 6dB attenuation.
-            //      here. We might want to revisit these choices in the future.
-            conduit->InsertDTMFTone(0, tone, true, duration, 6);
-          }), NS_DISPATCH_NORMAL);
-      }
-
+      state->mTransceiver->InsertDTMFTone(tone, state->mDuration);
     }
   } else {
     state->mSendTimer->Cancel();
   }
 
-  RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(state->mPeerConnectionImpl->mPCObserver);
+  RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(state->mPCObserver);
   if (!pco) {
     NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
     return;
   }
 
   JSErrorResult jrv;
-  pco->OnDTMFToneChange(state->mTrackId, eventTone, jrv);
+  pco->OnDTMFToneChange(*state->mTransceiver->GetSendTrack(), eventTone, jrv);
 
   if (jrv.Failed()) {
     NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
     return;
   }
 }
 
+PeerConnectionImpl::DTMFState::DTMFState()
+{}
+
+PeerConnectionImpl::DTMFState::~DTMFState()
+{}
+
 }  // end mozilla namespace
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -26,16 +26,17 @@
 #include "nsIThread.h"
 
 #include "signaling/src/jsep/JsepSession.h"
 #include "signaling/src/jsep/JsepSessionImpl.h"
 #include "signaling/src/sdp/SdpMediaSection.h"
 
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/PeerConnectionImplEnumsBinding.h"
+#include "mozilla/dom/RTCRtpTransceiverBinding.h"
 #include "PrincipalChangeObserver.h"
 #include "StreamTracks.h"
 
 #include "mozilla/TimeStamp.h"
 #include "mozilla/net/DataChannel.h"
 #include "VideoUtils.h"
 #include "VideoSegment.h"
 #include "mozilla/dom/RTCStatsReportBinding.h"
@@ -54,16 +55,17 @@ class nsDOMDataChannel;
 namespace mozilla {
 class DataChannel;
 class DtlsIdentity;
 class NrIceCtx;
 class NrIceMediaStream;
 class NrIceStunServer;
 class NrIceTurnServer;
 class MediaPipeline;
+class TransceiverImpl;
 
 class DOMMediaStream;
 
 namespace dom {
 class RTCCertificate;
 struct RTCConfiguration;
 class RTCDTMFSender;
 struct RTCIceServer;
@@ -244,17 +246,17 @@ public:
 
   NS_DECL_THREADSAFE_ISUPPORTS
 
   bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector);
 
   static already_AddRefed<PeerConnectionImpl>
       Constructor(const mozilla::dom::GlobalObject& aGlobal, ErrorResult& rv);
   static PeerConnectionImpl* CreatePeerConnection();
-  already_AddRefed<DOMMediaStream> MakeMediaStream();
+  OwningNonNull<DOMMediaStream> MakeMediaStream();
 
   nsresult CreateRemoteSourceStreamInfo(RefPtr<RemoteSourceStreamInfo>* aInfo,
                                         const std::string& aId);
 
   // DataConnection observers
   void NotifyDataChannel(already_AddRefed<mozilla::DataChannel> aChannel)
     // PeerConnectionImpl only inherits from mozilla::DataChannelConnection
     // inside libxul.
@@ -351,19 +353,17 @@ public:
 
   NS_IMETHODIMP SetLocalDescription (int32_t aAction, const char* aSDP);
 
   void SetLocalDescription (int32_t aAction, const nsAString& aSDP, ErrorResult &rv)
   {
     rv = SetLocalDescription(aAction, NS_ConvertUTF16toUTF8(aSDP).get());
   }
 
-  nsresult CreateNewRemoteTracks(RefPtr<PeerConnectionObserver>& aPco);
-
-  void RemoveOldRemoteTracks(RefPtr<PeerConnectionObserver>& aPco);
+  nsresult FireOnTrackEvents(RefPtr<PeerConnectionObserver>& aPco);
 
   NS_IMETHODIMP SetRemoteDescription (int32_t aAction, const char* aSDP);
 
   void SetRemoteDescription (int32_t aAction, const nsAString& aSDP, ErrorResult &rv)
   {
     rv = SetRemoteDescription(aAction, NS_ConvertUTF16toUTF8(aSDP).get());
   }
 
@@ -403,34 +403,44 @@ public:
                                mozilla::dom::MediaStreamTrack& aTrack)
   {
     rv = RemoveTrack(aTrack);
   }
 
   nsresult
   AddTrack(mozilla::dom::MediaStreamTrack& aTrack, DOMMediaStream& aStream);
 
+  already_AddRefed<TransceiverImpl> CreateTransceiverImpl(
+      const nsAString& aKind,
+      RefPtr<dom::MediaStreamTrack> aSendTrack,
+      ErrorResult& rv);
+
+  OwningNonNull<DOMMediaStream> CreateReceiveStreamWithTrack(
+      SdpMediaSection::MediaType type);
+
+  bool CheckNegotiationNeeded(ErrorResult &rv);
+
   NS_IMETHODIMP_TO_ERRORRESULT(InsertDTMF, ErrorResult &rv,
-                               dom::RTCRtpSender& sender,
+                               TransceiverImpl& transceiver,
                                const nsAString& tones,
                                uint32_t duration, uint32_t interToneGap) {
-    rv = InsertDTMF(sender, tones, duration, interToneGap);
+    rv = InsertDTMF(transceiver, tones, duration, interToneGap);
   }
 
   NS_IMETHODIMP_TO_ERRORRESULT(GetDTMFToneBuffer, ErrorResult &rv,
                                dom::RTCRtpSender& sender,
                                nsAString& outToneBuffer) {
     rv = GetDTMFToneBuffer(sender, outToneBuffer);
   }
 
-  NS_IMETHODIMP_TO_ERRORRESULT(ReplaceTrack, ErrorResult &rv,
-                               mozilla::dom::MediaStreamTrack& aThisTrack,
-                               mozilla::dom::MediaStreamTrack& aWithTrack)
+  NS_IMETHODIMP_TO_ERRORRESULT(ReplaceTrackNoRenegotiation, ErrorResult &rv,
+                               TransceiverImpl& aTransceiver,
+                               mozilla::dom::MediaStreamTrack* aWithTrack)
   {
-    rv = ReplaceTrack(aThisTrack, aWithTrack);
+    rv = ReplaceTrackNoRenegotiation(aTransceiver, aWithTrack);
   }
 
   NS_IMETHODIMP_TO_ERRORRESULT(SetParameters, ErrorResult &rv,
                                dom::MediaStreamTrack& aTrack,
                                const dom::RTCRtpParameters& aParameters)
   {
     rv = SetParameters(aTrack, aParameters);
   }
@@ -566,28 +576,16 @@ public:
                                       const nsAString& aProtocol,
                                       uint16_t aType,
                                       bool outOfOrderAllowed,
                                       uint16_t aMaxTime,
                                       uint16_t aMaxNum,
                                       bool aExternalNegotiated,
                                       uint16_t aStream);
 
-  NS_IMETHODIMP_TO_ERRORRESULT(GetLocalStreams, ErrorResult &rv,
-                               nsTArray<RefPtr<DOMMediaStream > >& result)
-  {
-    rv = GetLocalStreams(result);
-  }
-
-  NS_IMETHODIMP_TO_ERRORRESULT(GetRemoteStreams, ErrorResult &rv,
-                               nsTArray<RefPtr<DOMMediaStream > >& result)
-  {
-    rv = GetRemoteStreams(result);
-  }
-
   // Called whenever something is unrecognized by the parser
   // May be called more than once and does not necessarily mean
   // that parsing was stopped, only that something was unrecognized.
   void OnSdpParseError(const char* errorMessage);
 
   // Called when OnLocal/RemoteDescriptionSuccess/Error
   // is called to start the list over.
   void ClearSdpParseErrorMessages();
@@ -616,19 +614,16 @@ public:
       RTCStatsQuery *query);
 
   static nsresult ExecuteStatsQuery_s(RTCStatsQuery *query);
 
   // for monitoring changes in track ownership
   // PeerConnectionMedia can't do it because it doesn't know about principals
   virtual void PrincipalChanged(dom::MediaStreamTrack* aTrack) override;
 
-  static std::string GetStreamId(const DOMMediaStream& aStream);
-  static std::string GetTrackId(const dom::MediaStreamTrack& track);
-
   void OnMediaError(const std::string& aError);
 
 private:
   virtual ~PeerConnectionImpl();
   PeerConnectionImpl(const PeerConnectionImpl&rhs);
   PeerConnectionImpl& operator=(PeerConnectionImpl);
   nsresult CalculateFingerprint(const std::string& algorithm,
                                 std::vector<uint8_t>* fingerprint) const;
@@ -667,24 +662,21 @@ private:
   nsresult GetDatachannelParameters(
       uint32_t* channels,
       uint16_t* localport,
       uint16_t* remoteport,
       uint32_t* maxmessagesize,
       bool*     mmsset,
       uint16_t* level) const;
 
-  static void DeferredAddTrackToJsepSession(const std::string& pcHandle,
-                                            SdpMediaSection::MediaType type,
-                                            const std::string& streamId,
-                                            const std::string& trackId);
-
-  nsresult AddTrackToJsepSession(SdpMediaSection::MediaType type,
-                                 const std::string& streamId,
-                                 const std::string& trackId);
+  nsresult AddTransceiverToJsepSession(RefPtr<JsepTransceiver>& transceiver);
+  already_AddRefed<TransceiverImpl> CreateTransceiverImpl(
+      RefPtr<JsepTransceiver>& aJsepTransceiver,
+      RefPtr<dom::MediaStreamTrack> aSendTrack,
+      ErrorResult& aRv);
 
   nsresult SetupIceRestart();
   nsresult RollbackIceRestart();
   void FinalizeIceRestart();
 
   static void GetStatsForPCObserver_s(
       const std::string& pcHandle,
       nsAutoPtr<RTCStatsQuery> query);
@@ -697,20 +689,16 @@ private:
 
   // When ICE completes, we record a bunch of statistics that outlive the
   // PeerConnection. This is just telemetry right now, but this can also
   // include things like dumping the RLogConnector somewhere, saving away
   // an RTCStatsReport somewhere so it can be inspected after the call is over,
   // or other things.
   void RecordLongtermICEStatistics();
 
-  void OnNegotiationNeeded();
-  static void MaybeFireNegotiationNeeded_static(const std::string& pcHandle);
-  void MaybeFireNegotiationNeeded();
-
   // Timecard used to measure processing time. This should be the first class
   // attribute so that we accurately measure the time required to instantiate
   // any other attributes of this class.
   Timecard *mTimeCard;
 
   mozilla::dom::PCImplSignalingState mSignalingState;
 
   // ICE State
@@ -766,48 +754,46 @@ private:
   bool mForceIceTcp;
   RefPtr<PeerConnectionMedia> mMedia;
 
   // The JSEP negotiation session.
   mozilla::UniquePtr<PCUuidGenerator> mUuidGen;
   mozilla::UniquePtr<mozilla::JsepSession> mJsepSession;
   std::string mPreviousIceUfrag; // used during rollback of ice restart
   std::string mPreviousIcePwd; // used during rollback of ice restart
-
   // Start time of ICE, used for telemetry
   mozilla::TimeStamp mIceStartTime;
   // Start time of call used for Telemetry
   mozilla::TimeStamp mStartTime;
 
   bool mHaveConfiguredCodecs;
 
   bool mHaveDataStream;
 
   unsigned int mAddCandidateErrorCount;
 
   bool mTrickle;
 
-  bool mNegotiationNeeded;
-
   bool mPrivateWindow;
 
   // Whether this PeerConnection is being counted as active by mWindow
   bool mActiveOnWindow;
 
   // storage for Telemetry data
   uint16_t mMaxReceiving[SdpMediaSection::kMediaTypes];
   uint16_t mMaxSending[SdpMediaSection::kMediaTypes];
 
   // DTMF
   struct DTMFState {
-    PeerConnectionImpl* mPeerConnectionImpl;
+    DTMFState();
+    ~DTMFState();
+    nsWeakPtr mPCObserver;
+    RefPtr<TransceiverImpl> mTransceiver;
     nsCOMPtr<nsITimer> mSendTimer;
-    nsString mTrackId;
     nsString mTones;
-    size_t mLevel;
     uint32_t mDuration;
     uint32_t mInterToneGap;
   };
 
   static void
   DTMFSendTimerCallback_m(nsITimer* timer, void*);
 
   nsTArray<DTMFState> mDTMFStates;
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
@@ -7,199 +7,57 @@
 #include <vector>
 
 #include "CSFLog.h"
 
 #include "nspr.h"
 
 #include "nricectx.h"
 #include "nricemediastream.h"
-#include "MediaPipelineFactory.h"
+#include "MediaPipelineFilter.h"
+#include "MediaPipeline.h"
 #include "PeerConnectionImpl.h"
 #include "PeerConnectionMedia.h"
-#include "AudioConduit.h"
-#include "VideoConduit.h"
 #include "runnable_utils.h"
 #include "transportlayerice.h"
 #include "transportlayerdtls.h"
 #include "signaling/src/jsep/JsepSession.h"
 #include "signaling/src/jsep/JsepTransport.h"
 
-#include "MediaSegment.h"
-#include "MediaStreamGraph.h"
-
-#include "MediaStreamGraphImpl.h"
-
 #include "nsContentUtils.h"
 #include "nsNetCID.h"
 #include "nsNetUtil.h"
 #include "nsIURI.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsICancelable.h"
 #include "nsILoadInfo.h"
 #include "nsIContentPolicy.h"
 #include "nsIProxyInfo.h"
 #include "nsIProtocolProxyService.h"
 
 #include "nsProxyRelease.h"
 
-#include "MediaStreamList.h"
 #include "nsIScriptGlobalObject.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Telemetry.h"
-#include "mozilla/dom/RTCStatsReportBinding.h"
-#include "MediaStreamTrack.h"
-#include "VideoStreamTrack.h"
-#include "MediaStreamError.h"
 #include "MediaManager.h"
-
-
+#include "WebrtcGmpVideoCodec.h"
 
 namespace mozilla {
 using namespace dom;
 
 static const char* logTag = "PeerConnectionMedia";
 
 //XXX(pkerr) What about bitrate settings? Going with the defaults for now.
 RefPtr<WebRtcCallWrapper>
 CreateCall()
 {
   return WebRtcCallWrapper::Create();
 }
 
-nsresult
-PeerConnectionMedia::ReplaceTrack(const std::string& aOldStreamId,
-                                  const std::string& aOldTrackId,
-                                  MediaStreamTrack& aNewTrack,
-                                  const std::string& aNewStreamId,
-                                  const std::string& aNewTrackId)
-{
-  RefPtr<LocalSourceStreamInfo> oldInfo(GetLocalStreamById(aOldStreamId));
-
-  if (!oldInfo) {
-    CSFLogError(logTag, "Failed to find stream id %s", aOldStreamId.c_str());
-    return NS_ERROR_NOT_AVAILABLE;
-  }
-
-  nsresult rv = AddTrack(*aNewTrack.mOwningStream, aNewStreamId,
-                         aNewTrack, aNewTrackId);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  RefPtr<LocalSourceStreamInfo> newInfo(GetLocalStreamById(aNewStreamId));
-
-  if (!newInfo) {
-    CSFLogError(logTag, "Failed to add track id %s", aNewTrackId.c_str());
-    MOZ_ASSERT(false);
-    return NS_ERROR_FAILURE;
-  }
-
-  rv = newInfo->TakePipelineFrom(oldInfo, aOldTrackId, aNewTrack, aNewTrackId);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  return RemoveLocalTrack(aOldStreamId, aOldTrackId);
-}
-
-static void
-PipelineReleaseRef_m(RefPtr<MediaPipeline> pipeline)
-{}
-
-static void
-PipelineDetachTransport_s(RefPtr<MediaPipeline> pipeline,
-                          nsCOMPtr<nsIThread> mainThread)
-{
-  pipeline->DetachTransport_s();
-  mainThread->Dispatch(
-      // Make sure we let go of our reference before dispatching
-      // If the dispatch fails, well, we're hosed anyway.
-      WrapRunnableNM(PipelineReleaseRef_m, pipeline.forget()),
-      NS_DISPATCH_NORMAL);
-}
-
-void
-SourceStreamInfo::EndTrack(MediaStream* stream, dom::MediaStreamTrack* track)
-{
-  if (!stream || !stream->AsSourceStream()) {
-    return;
-  }
-
-  class Message : public ControlMessage {
-   public:
-    Message(MediaStream* stream, TrackID track)
-      : ControlMessage(stream),
-        track_id_(track) {}
-
-    virtual void Run() override {
-      mStream->AsSourceStream()->EndTrack(track_id_);
-    }
-   private:
-    TrackID track_id_;
-  };
-
-  stream->GraphImpl()->AppendMessage(
-      MakeUnique<Message>(stream, track->mTrackID));
-}
-
-void
-SourceStreamInfo::RemoveTrack(const std::string& trackId)
-{
-  mTracks.erase(trackId);
-
-  RefPtr<MediaPipeline> pipeline = GetPipelineByTrackId_m(trackId);
-  if (pipeline) {
-    mPipelines.erase(trackId);
-    pipeline->ShutdownMedia_m();
-    mParent->GetSTSThread()->Dispatch(
-        WrapRunnableNM(PipelineDetachTransport_s,
-                       pipeline.forget(),
-                       mParent->GetMainThread()),
-        NS_DISPATCH_NORMAL);
-  }
-}
-
-void SourceStreamInfo::DetachTransport_s()
-{
-  ASSERT_ON_THREAD(mParent->GetSTSThread());
-  // walk through all the MediaPipelines and call the shutdown
-  // transport functions. Must be on the STS thread.
-  for (auto& pipeline : mPipelines) {
-    pipeline.second->DetachTransport_s();
-  }
-}
-
-void SourceStreamInfo::DetachMedia_m()
-{
-  ASSERT_ON_THREAD(mParent->GetMainThread());
-
-  // walk through all the MediaPipelines and call the shutdown
-  // media functions. Must be on the main thread.
-  for (auto& pipeline : mPipelines) {
-    pipeline.second->ShutdownMedia_m();
-  }
-  mMediaStream = nullptr;
-}
-
-already_AddRefed<PeerConnectionImpl>
-PeerConnectionImpl::Constructor(const dom::GlobalObject& aGlobal, ErrorResult& rv)
-{
-  RefPtr<PeerConnectionImpl> pc = new PeerConnectionImpl(&aGlobal);
-
-  CSFLogDebug(logTag, "Created PeerConnection: %p", pc.get());
-
-  return pc.forget();
-}
-
-PeerConnectionImpl* PeerConnectionImpl::CreatePeerConnection()
-{
-  PeerConnectionImpl *pc = new PeerConnectionImpl();
-
-  CSFLogDebug(logTag, "Created PeerConnection: %p", pc);
-
-  return pc;
-}
-
 NS_IMETHODIMP PeerConnectionMedia::ProtocolProxyQueryHandler::
 OnProxyAvailable(nsICancelable *request,
                  nsIChannel *aChannel,
                  nsIProxyInfo *proxyinfo,
                  nsresult result) {
 
   if (!pcm_->mProxyRequest) {
     // PeerConnectionMedia is no longer waiting
@@ -283,16 +141,21 @@ PeerConnectionMedia::PeerConnectionMedia
       mUuidGen(MakeUnique<PCUuidGenerator>()),
       mMainThread(mParent->GetMainThread()),
       mSTSThread(mParent->GetSTSThread()),
       mProxyResolveCompleted(false),
       mIceRestartState(ICE_RESTART_NONE),
       mLocalAddrsCompleted(false) {
 }
 
+PeerConnectionMedia::~PeerConnectionMedia()
+{
+  MOZ_RELEASE_ASSERT(!mMainThread);
+}
+
 void
 PeerConnectionMedia::InitLocalAddrs()
 {
   if (XRE_IsContentProcess()) {
     CSFLogDebug(logTag, "%s: Get stun addresses via IPC",
                 mParentHandle.c_str());
 
     nsCOMPtr<nsIEventTarget> target = mParent->GetWindow()
@@ -424,24 +287,27 @@ nsresult PeerConnectionMedia::Init(const
   mCall = CreateCall();
 
   return NS_OK;
 }
 
 void
 PeerConnectionMedia::EnsureTransports(const JsepSession& aSession)
 {
-  auto transports = aSession.GetTransports();
-  for (size_t i = 0; i < transports.size(); ++i) {
-    RefPtr<JsepTransport> transport = transports[i];
+  for (const auto& transceiver : aSession.GetTransceivers()) {
+    if (!transceiver->HasLevel()) {
+      continue;
+    }
+
+    RefPtr<JsepTransport> transport = transceiver->mTransport;
     RUN_ON_THREAD(
         GetSTSThread(),
         WrapRunnable(RefPtr<PeerConnectionMedia>(this),
                      &PeerConnectionMedia::EnsureTransport_s,
-                     i,
+                     transceiver->GetLevel(),
                      transport->mComponents),
         NS_DISPATCH_NORMAL);
   }
 
   GatherIfReady();
 }
 
 void
@@ -468,69 +334,95 @@ PeerConnectionMedia::EnsureTransport_s(s
     stream->SetLevel(aLevel);
     stream->SignalReady.connect(this, &PeerConnectionMedia::IceStreamReady_s);
     stream->SignalCandidate.connect(this,
                                     &PeerConnectionMedia::OnCandidateFound_s);
     mIceCtxHdlr->ctx()->SetStream(aLevel, stream);
   }
 }
 
-void
+nsresult
 PeerConnectionMedia::ActivateOrRemoveTransports(const JsepSession& aSession,
                                                 const bool forceIceTcp)
 {
-  auto transports = aSession.GetTransports();
-  for (size_t i = 0; i < transports.size(); ++i) {
-    RefPtr<JsepTransport> transport = transports[i];
+  for (const auto& transceiver : aSession.GetTransceivers()) {
+    if (!transceiver->HasLevel()) {
+      continue;
+    }
 
     std::string ufrag;
     std::string pwd;
     std::vector<std::string> candidates;
+    size_t components = 0;
 
-    if (transport->mComponents) {
-      MOZ_ASSERT(transport->mIce);
-      CSFLogDebug(logTag, "Transport %u is active", static_cast<unsigned>(i));
+    RefPtr<JsepTransport> transport = transceiver->mTransport;
+    unsigned level = transceiver->GetLevel();
+
+    bool levelHasTransport =
+      transport->mComponents &&
+      transport->mIce &&
+      (!transceiver->HasBundleLevel() || (transceiver->BundleLevel() == level));
+
+    if (levelHasTransport) {
+      CSFLogDebug(logTag, "ACTIVATING TRANSPORT! - PC %s: level=%u components=%u",
+                  mParentHandle.c_str(), (unsigned)level,
+                  (unsigned)transport->mComponents);
+
       ufrag = transport->mIce->GetUfrag();
       pwd = transport->mIce->GetPassword();
       candidates = transport->mIce->GetCandidates();
-    } else {
-      CSFLogDebug(logTag, "Transport %u is disabled", static_cast<unsigned>(i));
-      // Make sure the MediaPipelineFactory doesn't try to use these.
-      RemoveTransportFlow(i, false);
-      RemoveTransportFlow(i, true);
-    }
-
-    if (forceIceTcp) {
-      candidates.erase(std::remove_if(candidates.begin(),
-                                      candidates.end(),
-                                      [](const std::string & s) {
-                                        return s.find(" UDP ") != std::string::npos ||
-                                               s.find(" udp ") != std::string::npos; }),
-                       candidates.end());
+      components = transport->mComponents;
+      if (forceIceTcp) {
+        candidates.erase(std::remove_if(candidates.begin(),
+                                        candidates.end(),
+                                        [](const std::string & s) {
+                                          return s.find(" UDP ") != std::string::npos ||
+                                                 s.find(" udp ") != std::string::npos; }),
+                         candidates.end());
+      }
     }
 
     RUN_ON_THREAD(
         GetSTSThread(),
         WrapRunnable(RefPtr<PeerConnectionMedia>(this),
                      &PeerConnectionMedia::ActivateOrRemoveTransport_s,
-                     i,
-                     transport->mComponents,
+                     transceiver->GetLevel(),
+                     components,
                      ufrag,
                      pwd,
                      candidates),
         NS_DISPATCH_NORMAL);
   }
 
   // We can have more streams than m-lines due to rollback.
   RUN_ON_THREAD(
       GetSTSThread(),
       WrapRunnable(RefPtr<PeerConnectionMedia>(this),
                    &PeerConnectionMedia::RemoveTransportsAtOrAfter_s,
-                   transports.size()),
+                   aSession.GetTransceivers().size()),
       NS_DISPATCH_NORMAL);
+
+  return NS_OK;
+}
+
+nsresult
+PeerConnectionMedia::UpdateTransceiverTransports(const JsepSession& aSession)
+{
+  for (const auto& transceiver : aSession.GetTransceivers()) {
+    nsresult rv = UpdateTransportFlows(*transceiver);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+  }
+
+  for (const auto& transceiverImpl : mTransceivers) {
+    transceiverImpl->UpdateTransport(*this);
+  }
+
+  return NS_OK;
 }
 
 void
 PeerConnectionMedia::ActivateOrRemoveTransport_s(
     size_t aMLine,
     size_t aComponentCount,
     const std::string& aUfrag,
     const std::string& aPassword,
@@ -580,42 +472,210 @@ PeerConnectionMedia::ActivateOrRemoveTra
 void
 PeerConnectionMedia::RemoveTransportsAtOrAfter_s(size_t aMLine)
 {
   for (size_t i = aMLine; i < mIceCtxHdlr->ctx()->GetStreamCount(); ++i) {
     mIceCtxHdlr->ctx()->SetStream(i, nullptr);
   }
 }
 
-nsresult PeerConnectionMedia::UpdateMediaPipelines(
-    const JsepSession& session) {
-  auto trackPairs = session.GetNegotiatedTrackPairs();
-  MediaPipelineFactory factory(this);
+nsresult
+PeerConnectionMedia::UpdateMediaPipelines()
+{
+  // The GMP code is all the way on the other side of webrtc.org, and it is not
+  // feasible to plumb error information all the way back. So, we set up a
+  // handle to the PC (for the duration of this call) in a global variable.
+  // This allows the GMP code to report errors to the PC.
+  WebrtcGmpPCHandleSetter setter(mParentHandle);
+
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    nsresult rv = transceiver->UpdateConduit();
+    if (NS_FAILED(rv)) {
+      MOZ_CRASH();
+      return rv;
+    }
+
+    if (!transceiver->IsVideo()) {
+      rv = transceiver->SyncWithMatchingVideoConduits(mTransceivers);
+      if (NS_FAILED(rv)) {
+        MOZ_CRASH();
+        return rv;
+      }
+      // TODO: If there is no audio, we should probably de-sync. However, this
+      // has never been done before, and it is unclear whether it is safe...
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PeerConnectionMedia::UpdateTransportFlows(const JsepTransceiver& aTransceiver)
+{
+  if (!aTransceiver.HasLevel()) {
+    // Nothing to do
+    return NS_OK;
+  }
+
+  size_t transportLevel = aTransceiver.GetTransportLevel();
+
+  nsresult rv =
+    UpdateTransportFlow(transportLevel, false, *aTransceiver.mTransport);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  return UpdateTransportFlow(transportLevel, true, *aTransceiver.mTransport);
+}
+
+// Accessing the PCMedia should be safe here because we shouldn't
+// have enqueued this function unless it was still active and
+// the ICE data is destroyed on the STS.
+static void
+FinalizeTransportFlow_s(RefPtr<PeerConnectionMedia> aPCMedia,
+                        RefPtr<TransportFlow> aFlow, size_t aLevel,
+                        bool aIsRtcp,
+                        nsAutoPtr<PtrVector<TransportLayer> > aLayerList)
+{
+  TransportLayerIce* ice =
+      static_cast<TransportLayerIce*>(aLayerList->values.front());
+  ice->SetParameters(aPCMedia->ice_media_stream(aLevel),
+                     aIsRtcp ? 2 : 1);
+  nsAutoPtr<std::queue<TransportLayer*> > layerQueue(
+      new std::queue<TransportLayer*>);
+  for (auto& value : aLayerList->values) {
+    layerQueue->push(value);
+  }
+  aLayerList->values.clear();
+  (void)aFlow->PushLayers(layerQueue); // TODO(bug 854518): Process errors.
+}
+
+static void
+AddNewIceStreamForRestart_s(RefPtr<PeerConnectionMedia> aPCMedia,
+                            RefPtr<TransportFlow> aFlow,
+                            size_t aLevel,
+                            bool aIsRtcp)
+{
+  TransportLayerIce* ice =
+      static_cast<TransportLayerIce*>(aFlow->GetLayer("ice"));
+  ice->SetParameters(aPCMedia->ice_media_stream(aLevel),
+                     aIsRtcp ? 2 : 1);
+}
+
+nsresult
+PeerConnectionMedia::UpdateTransportFlow(
+    size_t aLevel,
+    bool aIsRtcp,
+    const JsepTransport& aTransport)
+{
+  if (aIsRtcp && aTransport.mComponents < 2) {
+    RemoveTransportFlow(aLevel, aIsRtcp);
+    return NS_OK;
+  }
+
+  if (!aIsRtcp && !aTransport.mComponents) {
+    RemoveTransportFlow(aLevel, aIsRtcp);
+    return NS_OK;
+  }
+
   nsresult rv;
 
-  for (auto pair : trackPairs) {
-    if (pair.mReceiving) {
+  RefPtr<TransportFlow> flow = GetTransportFlow(aLevel, aIsRtcp);
+  if (flow) {
+    if (IsIceRestarting()) {
+      CSFLogInfo(logTag, "Flow[%s]: detected ICE restart - level: %u rtcp: %d",
+                 flow->id().c_str(), (unsigned)aLevel, aIsRtcp);
 
-      rv = factory.CreateOrUpdateMediaPipeline(pair, *pair.mReceiving);
+      RefPtr<PeerConnectionMedia> pcMedia(this);
+      rv = GetSTSThread()->Dispatch(
+          WrapRunnableNM(AddNewIceStreamForRestart_s,
+                         pcMedia, flow, aLevel, aIsRtcp),
+          NS_DISPATCH_NORMAL);
       if (NS_FAILED(rv)) {
+        CSFLogError(logTag, "Failed to dispatch AddNewIceStreamForRestart_s");
         return rv;
       }
     }
 
-    if (pair.mSending) {
-      rv = factory.CreateOrUpdateMediaPipeline(pair, *pair.mSending);
-      if (NS_FAILED(rv)) {
-        return rv;
-      }
+    return NS_OK;
+  }
+
+  std::ostringstream osId;
+  osId << mParentHandle << ":" << aLevel << "," << (aIsRtcp ? "rtcp" : "rtp");
+  flow = new TransportFlow(osId.str());
+
+  // The media streams are made on STS so we need to defer setup.
+  auto ice = MakeUnique<TransportLayerIce>();
+  auto dtls = MakeUnique<TransportLayerDtls>();
+  dtls->SetRole(aTransport.mDtls->GetRole() ==
+                        JsepDtlsTransport::kJsepDtlsClient
+                    ? TransportLayerDtls::CLIENT
+                    : TransportLayerDtls::SERVER);
+
+  RefPtr<DtlsIdentity> pcid = mParent->Identity();
+  if (!pcid) {
+    CSFLogError(logTag, "Failed to get DTLS identity.");
+    return NS_ERROR_FAILURE;
+  }
+  dtls->SetIdentity(pcid);
+
+  const SdpFingerprintAttributeList& fingerprints =
+      aTransport.mDtls->GetFingerprints();
+  for (const auto& fingerprint : fingerprints.mFingerprints) {
+    std::ostringstream ss;
+    ss << fingerprint.hashFunc;
+    rv = dtls->SetVerificationDigest(ss.str(), &fingerprint.fingerprint[0],
+                                     fingerprint.fingerprint.size());
+    if (NS_FAILED(rv)) {
+      CSFLogError(logTag, "Could not set fingerprint");
+      return rv;
     }
   }
 
-  for (auto& stream : mRemoteSourceStreams) {
-    stream->StartReceiving();
+  std::vector<uint16_t> srtpCiphers;
+  srtpCiphers.push_back(SRTP_AES128_CM_HMAC_SHA1_80);
+  srtpCiphers.push_back(SRTP_AES128_CM_HMAC_SHA1_32);
+
+  rv = dtls->SetSrtpCiphers(srtpCiphers);
+  if (NS_FAILED(rv)) {
+    CSFLogError(logTag, "Couldn't set SRTP ciphers");
+    return rv;
+  }
+
+  // Always permits negotiation of the confidential mode.
+  // Only allow non-confidential (which is an allowed default),
+  // if we aren't confidential.
+  std::set<std::string> alpn;
+  std::string alpnDefault = "";
+  alpn.insert("c-webrtc");
+  if (!mParent->PrivacyRequested()) {
+    alpnDefault = "webrtc";
+    alpn.insert(alpnDefault);
   }
+  rv = dtls->SetAlpn(alpn, alpnDefault);
+  if (NS_FAILED(rv)) {
+    CSFLogError(logTag, "Couldn't set ALPN");
+    return rv;
+  }
+
+  nsAutoPtr<PtrVector<TransportLayer> > layers(new PtrVector<TransportLayer>);
+  layers->values.push_back(ice.release());
+  layers->values.push_back(dtls.release());
+
+  RefPtr<PeerConnectionMedia> pcMedia(this);
+  rv = GetSTSThread()->Dispatch(
+      WrapRunnableNM(FinalizeTransportFlow_s, pcMedia, flow, aLevel, aIsRtcp,
+                     layers),
+      NS_DISPATCH_NORMAL);
+  if (NS_FAILED(rv)) {
+    CSFLogError(logTag, "Failed to dispatch FinalizeTransportFlow_s");
+    return rv;
+  }
+
+  AddTransportFlow(aLevel, aIsRtcp, flow);
 
   return NS_OK;
 }
 
 void
 PeerConnectionMedia::StartIceChecks(const JsepSession& aSession)
 {
   nsCOMPtr<nsIRunnable> runnable(
@@ -966,152 +1026,67 @@ PeerConnectionMedia::EnsureIceGathering_
 
   // If there are no streams, we're probably in a situation where we've rolled
   // back while still waiting for our proxy configuration to come back. Make
   // sure content knows that the rollback has stuck wrt gathering.
   IceGatheringStateChange_s(mIceCtxHdlr->ctx().get(),
                             NrIceCtx::ICE_CTX_GATHER_COMPLETE);
 }
 
-nsresult
-PeerConnectionMedia::AddTrack(DOMMediaStream& aMediaStream,
-                              const std::string& streamId,
-                              MediaStreamTrack& aTrack,
-                              const std::string& trackId)
-{
-  ASSERT_ON_THREAD(mMainThread);
-
-  CSFLogDebug(logTag, "%s: MediaStream: %p", __FUNCTION__, &aMediaStream);
-
-  RefPtr<LocalSourceStreamInfo> localSourceStream =
-    GetLocalStreamById(streamId);
-
-  if (!localSourceStream) {
-    localSourceStream = new LocalSourceStreamInfo(&aMediaStream, this, streamId);
-    mLocalSourceStreams.AppendElement(localSourceStream);
-  }
-
-  localSourceStream->AddTrack(trackId, &aTrack);
-  return NS_OK;
-}
-
-nsresult
-PeerConnectionMedia::RemoveLocalTrack(const std::string& streamId,
-                                      const std::string& trackId)
-{
-  ASSERT_ON_THREAD(mMainThread);
-
-  CSFLogDebug(logTag, "%s: stream: %s track: %s", __FUNCTION__,
-                      streamId.c_str(), trackId.c_str());
-
-  RefPtr<LocalSourceStreamInfo> localSourceStream =
-    GetLocalStreamById(streamId);
-  if (!localSourceStream) {
-    return NS_ERROR_ILLEGAL_VALUE;
-  }
-
-  localSourceStream->RemoveTrack(trackId);
-  if (!localSourceStream->GetTrackCount()) {
-    mLocalSourceStreams.RemoveElement(localSourceStream);
-  }
-  return NS_OK;
-}
-
-nsresult
-PeerConnectionMedia::RemoveRemoteTrack(const std::string& streamId,
-                                       const std::string& trackId)
-{
-  ASSERT_ON_THREAD(mMainThread);
-
-  CSFLogDebug(logTag, "%s: stream: %s track: %s", __FUNCTION__,
-                      streamId.c_str(), trackId.c_str());
-
-  RefPtr<RemoteSourceStreamInfo> remoteSourceStream =
-    GetRemoteStreamById(streamId);
-  if (!remoteSourceStream) {
-    return NS_ERROR_ILLEGAL_VALUE;
-  }
-
-  remoteSourceStream->RemoveTrack(trackId);
-  if (!remoteSourceStream->GetTrackCount()) {
-    mRemoteSourceStreams.RemoveElement(remoteSourceStream);
-  }
-  return NS_OK;
-}
-
 void
 PeerConnectionMedia::SelfDestruct()
 {
   ASSERT_ON_THREAD(mMainThread);
 
   CSFLogDebug(logTag, "%s: ", __FUNCTION__);
 
-  // Shut down the media
-  for (uint32_t i=0; i < mLocalSourceStreams.Length(); ++i) {
-    mLocalSourceStreams[i]->DetachMedia_m();
-  }
-
-  for (uint32_t i=0; i < mRemoteSourceStreams.Length(); ++i) {
-    mRemoteSourceStreams[i]->DetachMedia_m();
-  }
-
   if (mStunAddrsRequest) {
     mStunAddrsRequest->Cancel();
     mStunAddrsRequest = nullptr;
   }
 
   if (mProxyRequest) {
     mProxyRequest->Cancel(NS_ERROR_ABORT);
     mProxyRequest = nullptr;
   }
 
+  for (auto transceiver : mTransceivers) {
+    // transceivers are garbage-collected, so we need to poke them to perform
+    // cleanup right now so the appropriate events fire.
+    transceiver->Shutdown_m();
+  }
+
   // Shutdown the transport (async)
   RUN_ON_THREAD(mSTSThread, WrapRunnable(
       this, &PeerConnectionMedia::ShutdownMediaTransport_s),
                 NS_DISPATCH_NORMAL);
 
   CSFLogDebug(logTag, "%s: Media shut down", __FUNCTION__);
 }
 
 void
 PeerConnectionMedia::SelfDestruct_m()
 {
   CSFLogDebug(logTag, "%s: ", __FUNCTION__);
 
   ASSERT_ON_THREAD(mMainThread);
 
-  mLocalSourceStreams.Clear();
-  mRemoteSourceStreams.Clear();
-
   mMainThread = nullptr;
 
   // Final self-destruct.
   this->Release();
 }
 
 void
 PeerConnectionMedia::ShutdownMediaTransport_s()
 {
   ASSERT_ON_THREAD(mSTSThread);
 
   CSFLogDebug(logTag, "%s: ", __FUNCTION__);
 
-  // Here we access m{Local|Remote}SourceStreams off the main thread.
-  // That's OK because by here PeerConnectionImpl has forgotten about us,
-  // so there is no chance of getting a call in here from outside.
-  // The dispatches from SelfDestruct() and to SelfDestruct_m() provide
-  // memory barriers that protect us from badness.
-  for (uint32_t i=0; i < mLocalSourceStreams.Length(); ++i) {
-    mLocalSourceStreams[i]->DetachTransport_s();
-  }
-
-  for (uint32_t i=0; i < mRemoteSourceStreams.Length(); ++i) {
-    mRemoteSourceStreams[i]->DetachTransport_s();
-  }
-
   disconnect_all();
   mTransportFlows.clear();
 
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
   NrIceStats stats = mIceCtxHdlr->Destroy();
 
   CSFLogDebug(logTag, "Ice Telemetry: stun (retransmits: %d)"
                       "   turn (401s: %d   403s: %d   438s: %d)",
@@ -1130,96 +1105,104 @@ PeerConnectionMedia::ShutdownMediaTransp
 
   mIceCtxHdlr = nullptr;
 
   // we're holding a ref to 'this' that's released by SelfDestruct_m
   mMainThread->Dispatch(WrapRunnable(this, &PeerConnectionMedia::SelfDestruct_m),
                         NS_DISPATCH_NORMAL);
 }
 
-LocalSourceStreamInfo*
-PeerConnectionMedia::GetLocalStreamByIndex(int aIndex)
-{
-  ASSERT_ON_THREAD(mMainThread);
-  if(aIndex < 0 || aIndex >= (int) mLocalSourceStreams.Length()) {
-    return nullptr;
-  }
-
-  MOZ_ASSERT(mLocalSourceStreams[aIndex]);
-  return mLocalSourceStreams[aIndex];
-}
-
-LocalSourceStreamInfo*
-PeerConnectionMedia::GetLocalStreamById(const std::string& id)
+nsresult
+PeerConnectionMedia::AddTransceiver(
+    RefPtr<JsepTransceiver> aJsepTransceiver,
+    OwningNonNull<DOMMediaStream>& aReceiveStream,
+    RefPtr<dom::MediaStreamTrack>& aSendTrack,
+    RefPtr<TransceiverImpl>* aTransceiverImpl)
 {
-  ASSERT_ON_THREAD(mMainThread);
-  for (size_t i = 0; i < mLocalSourceStreams.Length(); ++i) {
-    if (id == mLocalSourceStreams[i]->GetId()) {
-      return mLocalSourceStreams[i];
-    }
-  }
+  RefPtr<TransceiverImpl> transceiver = new TransceiverImpl(
+      mParent->GetHandle(),
+      aJsepTransceiver,
+      mMainThread.get(),
+      mSTSThread.get(),
+      aReceiveStream,
+      aSendTrack,
+      mCall);
 
-  return nullptr;
-}
-
-LocalSourceStreamInfo*
-PeerConnectionMedia::GetLocalStreamByTrackId(const std::string& id)
-{
-  ASSERT_ON_THREAD(mMainThread);
-  for (RefPtr<LocalSourceStreamInfo>& info : mLocalSourceStreams) {
-    if (info->HasTrack(id)) {
-      return info;
+  if (aSendTrack) {
+    // implement checking for peerIdentity (where failure == black/silence)
+    nsIDocument* doc = mParent->GetWindow()->GetExtantDoc();
+    if (doc) {
+      transceiver->UpdateSinkIdentity(nullptr,
+                                      doc->NodePrincipal(),
+                                      mParent->GetPeerIdentity());
+    } else {
+      MOZ_CRASH();
+      return NS_ERROR_FAILURE; // Don't remove this till we know it's safe.
     }
   }
 
-  return nullptr;
+  mTransceivers.push_back(transceiver);
+  *aTransceiverImpl = transceiver;
+
+  return NS_OK;
 }
 
-RemoteSourceStreamInfo*
-PeerConnectionMedia::GetRemoteStreamByIndex(size_t aIndex)
+void
+PeerConnectionMedia::GetTransmitPipelinesMatching(
+    MediaStreamTrack* aTrack,
+    nsTArray<RefPtr<MediaPipeline>>* aPipelines)
 {
-  ASSERT_ON_THREAD(mMainThread);
-  MOZ_ASSERT(mRemoteSourceStreams.SafeElementAt(aIndex));
-  return mRemoteSourceStreams.SafeElementAt(aIndex);
-}
-
-RemoteSourceStreamInfo*
-PeerConnectionMedia::GetRemoteStreamById(const std::string& id)
-{
-  ASSERT_ON_THREAD(mMainThread);
-  for (size_t i = 0; i < mRemoteSourceStreams.Length(); ++i) {
-    if (id == mRemoteSourceStreams[i]->GetId()) {
-      return mRemoteSourceStreams[i];
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    if (transceiver->HasSendTrack(aTrack)) {
+      aPipelines->AppendElement(transceiver->GetSendPipeline());
     }
   }
 
-  return nullptr;
+  if (!aPipelines->Length()) {
+    CSFLogWarn(logTag, "%s: none found for %p", __FUNCTION__, aTrack);
+  }
 }
 
-RemoteSourceStreamInfo*
-PeerConnectionMedia::GetRemoteStreamByTrackId(const std::string& id)
+void
+PeerConnectionMedia::GetReceivePipelinesMatching(
+    MediaStreamTrack* aTrack,
+    nsTArray<RefPtr<MediaPipeline>>* aPipelines)
 {
-  ASSERT_ON_THREAD(mMainThread);
-  for (RefPtr<RemoteSourceStreamInfo>& info : mRemoteSourceStreams) {
-    if (info->HasTrack(id)) {
-      return info;
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    if (transceiver->HasReceiveTrack(aTrack)) {
+      aPipelines->AppendElement(transceiver->GetReceivePipeline());
     }
   }
 
-  return nullptr;
+  if (!aPipelines->Length()) {
+    CSFLogWarn(logTag, "%s: none found for %p", __FUNCTION__, aTrack);
+  }
 }
 
-
 nsresult
-PeerConnectionMedia::AddRemoteStream(RefPtr<RemoteSourceStreamInfo> aInfo)
+PeerConnectionMedia::AddRIDExtension(MediaStreamTrack& aRecvTrack,
+                                     unsigned short aExtensionId)
 {
-  ASSERT_ON_THREAD(mMainThread);
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    if (transceiver->HasReceiveTrack(&aRecvTrack)) {
+      transceiver->AddRIDExtension(aExtensionId);
+    }
+  }
+  return NS_OK;
+}
 
-  mRemoteSourceStreams.AppendElement(aInfo);
-
+nsresult
+PeerConnectionMedia::AddRIDFilter(MediaStreamTrack& aRecvTrack,
+                                  const nsAString& aRid)
+{
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    if (transceiver->HasReceiveTrack(&aRecvTrack)) {
+      transceiver->AddRIDFilter(aRid);
+    }
+  }
   return NS_OK;
 }
 
 void
 PeerConnectionMedia::IceGatheringStateChange_s(NrIceCtx* ctx,
                                                NrIceCtx::GatheringState state)
 {
   ASSERT_ON_THREAD(mSTSThread);
@@ -1455,282 +1438,65 @@ void
 PeerConnectionMedia::ConnectDtlsListener_s(const RefPtr<TransportFlow>& aFlow)
 {
   TransportLayer* dtls = aFlow->GetLayer(TransportLayerDtls::ID());
   if (dtls) {
     dtls->SignalStateChange.connect(this, &PeerConnectionMedia::DtlsConnected_s);
   }
 }
 
-nsresult
-LocalSourceStreamInfo::TakePipelineFrom(RefPtr<LocalSourceStreamInfo>& info,
-                                        const std::string& oldTrackId,
-                                        MediaStreamTrack& aNewTrack,
-                                        const std::string& newTrackId)
-{
-  if (mPipelines.count(newTrackId)) {
-    CSFLogError(logTag, "%s: Pipeline already exists for %s/%s",
-                __FUNCTION__, mId.c_str(), newTrackId.c_str());
-    return NS_ERROR_INVALID_ARG;
-  }
-
-  RefPtr<MediaPipeline> pipeline(info->ForgetPipelineByTrackId_m(oldTrackId));
-
-  if (!pipeline) {
-    // Replacetrack can potentially happen in the middle of offer/answer, before
-    // the pipeline has been created.
-    CSFLogInfo(logTag, "%s: Replacing track before the pipeline has been "
-                       "created, nothing to do.", __FUNCTION__);
-    return NS_OK;
-  }
-
-  nsresult rv =
-    static_cast<MediaPipelineTransmit*>(pipeline.get())->ReplaceTrack(aNewTrack);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  mPipelines[newTrackId] = pipeline;
-
-  return NS_OK;
-}
-
 /**
  * Tells you if any local track is isolated to a specific peer identity.
  * Obviously, we want all the tracks to be isolated equally so that they can
  * all be sent or not.  We check once when we are setting a local description
  * and that determines if we flip the "privacy requested" bit on.  Once the bit
  * is on, all media originating from this peer connection is isolated.
  *
  * @returns true if any track has a peerIdentity set on it
  */
 bool
 PeerConnectionMedia::AnyLocalTrackHasPeerIdentity() const
 {
   ASSERT_ON_THREAD(mMainThread);
 
-  for (uint32_t u = 0; u < mLocalSourceStreams.Length(); u++) {
-    for (auto pair : mLocalSourceStreams[u]->GetMediaStreamTracks()) {
-      if (pair.second->GetPeerIdentity() != nullptr) {
-        return true;
-      }
+  for (const RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    if (transceiver->GetSendTrack() &&
+        transceiver->GetSendTrack()->GetPeerIdentity()) {
+      return true;
     }
   }
   return false;
 }
 
 void
 PeerConnectionMedia::UpdateRemoteStreamPrincipals_m(nsIPrincipal* aPrincipal)
 {
   ASSERT_ON_THREAD(mMainThread);
 
-  for (uint32_t u = 0; u < mRemoteSourceStreams.Length(); u++) {
-    mRemoteSourceStreams[u]->UpdatePrincipal_m(aPrincipal);
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    transceiver->UpdatePrincipal(aPrincipal);
   }
 }
 
 void
 PeerConnectionMedia::UpdateSinkIdentity_m(MediaStreamTrack* aTrack,
                                           nsIPrincipal* aPrincipal,
                                           const PeerIdentity* aSinkIdentity)
 {
   ASSERT_ON_THREAD(mMainThread);
 
-  for (uint32_t u = 0; u < mLocalSourceStreams.Length(); u++) {
-    mLocalSourceStreams[u]->UpdateSinkIdentity_m(aTrack, aPrincipal,
-                                                 aSinkIdentity);
-  }
-}
-
-void
-LocalSourceStreamInfo::UpdateSinkIdentity_m(MediaStreamTrack* aTrack,
-                                            nsIPrincipal* aPrincipal,
-                                            const PeerIdentity* aSinkIdentity)
-{
-  for (auto& pipeline_ : mPipelines) {
-    MediaPipelineTransmit* pipeline =
-      static_cast<MediaPipelineTransmit*>(pipeline_.second.get());
-    pipeline->UpdateSinkIdentity_m(aTrack, aPrincipal, aSinkIdentity);
-  }
-}
-
-void RemoteSourceStreamInfo::UpdatePrincipal_m(nsIPrincipal* aPrincipal)
-{
-  // This blasts away the existing principal.
-  // We only do this when we become certain that the all tracks are safe to make
-  // accessible to the script principal.
-  for (auto& trackPair : mTracks) {
-    MOZ_RELEASE_ASSERT(trackPair.second);
-    RemoteTrackSource& source =
-      static_cast<RemoteTrackSource&>(trackPair.second->GetSource());
-    source.SetPrincipal(aPrincipal);
-
-    RefPtr<MediaPipeline> pipeline = GetPipelineByTrackId_m(trackPair.first);
-    if (pipeline) {
-      MOZ_ASSERT(pipeline->direction() == MediaPipeline::RECEIVE);
-      static_cast<MediaPipelineReceive*>(pipeline.get())
-        ->SetPrincipalHandle_m(MakePrincipalHandle(aPrincipal));
-    }
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    transceiver->UpdateSinkIdentity(aTrack, aPrincipal, aSinkIdentity);
   }
 }
 
 bool
 PeerConnectionMedia::AnyCodecHasPluginID(uint64_t aPluginID)
 {
-  for (uint32_t i=0; i < mLocalSourceStreams.Length(); ++i) {
-    if (mLocalSourceStreams[i]->AnyCodecHasPluginID(aPluginID)) {
-      return true;
-    }
-  }
-  for (uint32_t i=0; i < mRemoteSourceStreams.Length(); ++i) {
-    if (mRemoteSourceStreams[i]->AnyCodecHasPluginID(aPluginID)) {
-      return true;
-    }
-  }
-  return false;
-}
-
-bool
-SourceStreamInfo::AnyCodecHasPluginID(uint64_t aPluginID)
-{
-  // Scan the videoConduits for this plugin ID
-  for (auto& pipeline : mPipelines) {
-    if (pipeline.second->Conduit()->CodecPluginID() == aPluginID) {
+  for (RefPtr<TransceiverImpl>& transceiver : mTransceivers) {
+    if (transceiver->ConduitHasPluginID(aPluginID)) {
       return true;
     }
   }
   return false;
 }
 
-nsresult
-SourceStreamInfo::StorePipeline(
-    const std::string& trackId,
-    const RefPtr<mozilla::MediaPipeline>& aPipeline)
-{
-  MOZ_ASSERT(mPipelines.find(trackId) == mPipelines.end());
-  if (mPipelines.find(trackId) != mPipelines.end()) {
-    CSFLogError(logTag, "%s: Storing duplicate track", __FUNCTION__);
-    return NS_ERROR_FAILURE;
-  }
-
-  mPipelines[trackId] = aPipeline;
-  return NS_OK;
-}
-
-void
-RemoteSourceStreamInfo::DetachMedia_m()
-{
-  for (auto& webrtcIdAndTrack : mTracks) {
-    EndTrack(mMediaStream->GetInputStream(), webrtcIdAndTrack.second);
-  }
-  SourceStreamInfo::DetachMedia_m();
-}
-
-void
-RemoteSourceStreamInfo::RemoveTrack(const std::string& trackId)
-{
-  auto it = mTracks.find(trackId);
-  if (it != mTracks.end()) {
-    EndTrack(mMediaStream->GetInputStream(), it->second);
-  }
-
-  SourceStreamInfo::RemoveTrack(trackId);
-}
-
-void
-RemoteSourceStreamInfo::SyncPipeline(
-  RefPtr<MediaPipelineReceive> aPipeline)
-{
-  // See if we have both audio and video here, and if so cross the streams and
-  // sync them
-  // TODO: Do we need to prevent multiple syncs if there is more than one audio
-  // or video track in a single media stream? What are we supposed to do in this
-  // case?
-  for (auto i = mPipelines.begin(); i != mPipelines.end(); ++i) {
-    if (i->second->IsVideo() != aPipeline->IsVideo()) {
-      // Ok, we have one video, one non-video - cross the streams!
-      WebrtcAudioConduit *audio_conduit =
-        static_cast<WebrtcAudioConduit*>(aPipeline->IsVideo() ?
-                                                  i->second->Conduit() :
-                                                  aPipeline->Conduit());
-      WebrtcVideoConduit *video_conduit =
-        static_cast<WebrtcVideoConduit*>(aPipeline->IsVideo() ?
-                                                  aPipeline->Conduit() :
-                                                  i->second->Conduit());
-      video_conduit->SyncTo(audio_conduit);
-      CSFLogDebug(logTag, "Syncing %p to %p, %s to %s",
-                          video_conduit, audio_conduit,
-                          i->first.c_str(), aPipeline->trackid().c_str());
-    }
-  }
-}
-
-void
-RemoteSourceStreamInfo::StartReceiving()
-{
-  if (mReceiving || mPipelines.empty()) {
-    return;
-  }
-
-  mReceiving = true;
-
-  SourceMediaStream* source = GetMediaStream()->GetInputStream()->AsSourceStream();
-  source->SetPullEnabled(true);
-  // AdvanceKnownTracksTicksTime(HEAT_DEATH_OF_UNIVERSE) means that in
-  // theory per the API, we can't add more tracks before that
-  // time. However, the impl actually allows it, and it avoids a whole
-  // bunch of locking that would be required (and potential blocking)
-  // if we used smaller values and updated them on each NotifyPull.
-  source->AdvanceKnownTracksTime(STREAM_TIME_MAX);
-  CSFLogDebug(logTag, "Finished adding tracks to MediaStream %p", source);
-}
-
-RefPtr<MediaPipeline> SourceStreamInfo::GetPipelineByTrackId_m(
-    const std::string& trackId) {
-  ASSERT_ON_THREAD(mParent->GetMainThread());
-
-  // Refuse to hand out references if we're tearing down.
-  // (Since teardown involves a dispatch to and from STS before MediaPipelines
-  // are released, it is safe to start other dispatches to and from STS with a
-  // RefPtr<MediaPipeline>, since that reference won't be the last one
-  // standing)
-  if (mMediaStream) {
-    if (mPipelines.count(trackId)) {
-      return mPipelines[trackId];
-    }
-  }
-
-  return nullptr;
-}
-
-already_AddRefed<MediaPipeline>
-LocalSourceStreamInfo::ForgetPipelineByTrackId_m(const std::string& trackId)
-{
-  ASSERT_ON_THREAD(mParent->GetMainThread());
-
-  // Refuse to hand out references if we're tearing down.
-  // (Since teardown involves a dispatch to and from STS before MediaPipelines
-  // are released, it is safe to start other dispatches to and from STS with a
-  // RefPtr<MediaPipeline>, since that reference won't be the last one
-  // standing)
-  if (mMediaStream) {
-    if (mPipelines.count(trackId)) {
-      RefPtr<MediaPipeline> pipeline(mPipelines[trackId]);
-      mPipelines.erase(trackId);
-      return pipeline.forget();
-    }
-  }
-
-  return nullptr;
-}
-
-auto
-RemoteTrackSource::ApplyConstraints(
-    nsPIDOMWindowInner* aWindow,
-    const dom::MediaTrackConstraints& aConstraints,
-    dom::CallerType aCallerType) -> already_AddRefed<PledgeVoid>
-{
-  RefPtr<PledgeVoid> p = new PledgeVoid();
-  p->Reject(new dom::MediaStreamError(aWindow,
-                                      NS_LITERAL_STRING("OverconstrainedError"),
-                                      NS_LITERAL_STRING("")));
-  return p.forget();
-}
-
 } // namespace mozilla
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
@@ -4,239 +4,53 @@
 
 #ifndef _PEER_CONNECTION_MEDIA_H_
 #define _PEER_CONNECTION_MEDIA_H_
 
 #include <string>
 #include <vector>
 #include <map>
 
-#include "nspr.h"
-#include "prlock.h"
-
 #include "mozilla/RefPtr.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/net/StunAddrsRequestChild.h"
-#include "nsComponentManagerUtils.h"
 #include "nsIProtocolProxyCallback.h"
 
-#include "signaling/src/jsep/JsepSession.h"
-#include "AudioSegment.h"
-
-#include "Layers.h"
-#include "VideoUtils.h"
-#include "ImageLayers.h"
-#include "VideoSegment.h"
-#include "MediaStreamTrack.h"
+#include "TransceiverImpl.h"
 
 class nsIPrincipal;
 
 namespace mozilla {
 class DataChannel;
 class PeerIdentity;
-class MediaPipelineFactory;
 namespace dom {
 struct RTCInboundRTPStreamStats;
 struct RTCOutboundRTPStreamStats;
+class MediaStreamTrack;
 }
 }
 
 #include "nricectxhandler.h"
 #include "nriceresolver.h"
 #include "nricemediastream.h"
-#include "MediaPipeline.h"
 
 namespace mozilla {
 
 class PeerConnectionImpl;
 class PeerConnectionMedia;
 class PCUuidGenerator;
-
-class SourceStreamInfo {
-public:
-  SourceStreamInfo(DOMMediaStream* aMediaStream,
-                   PeerConnectionMedia *aParent,
-                   const std::string& aId)
-      : mMediaStream(aMediaStream),
-        mParent(aParent),
-        mId(aId) {
-    MOZ_ASSERT(mMediaStream);
-  }
-
-  SourceStreamInfo(already_AddRefed<DOMMediaStream>& aMediaStream,
-                   PeerConnectionMedia *aParent,
-                   const std::string& aId)
-      : mMediaStream(aMediaStream),
-        mParent(aParent),
-        mId(aId) {
-    MOZ_ASSERT(mMediaStream);
-  }
-
-  virtual ~SourceStreamInfo() {}
-
-  DOMMediaStream* GetMediaStream() const {
-    return mMediaStream;
-  }
-
-  nsresult StorePipeline(const std::string& trackId,
-                         const RefPtr<MediaPipeline>& aPipeline);
-
-  virtual void AddTrack(const std::string& trackId,
-                        const RefPtr<dom::MediaStreamTrack>& aTrack)
-  {
-    mTracks.insert(std::make_pair(trackId, aTrack));
-  }
-  virtual void RemoveTrack(const std::string& trackId);
-  bool HasTrack(const std::string& trackId) const
-  {
-    return !!mTracks.count(trackId);
-  }
-  size_t GetTrackCount() const { return mTracks.size(); }
-
-  // This method exists for stats and the unittests.
-  // It allows visibility into the pipelines and flows.
-  const std::map<std::string, RefPtr<MediaPipeline>>&
-  GetPipelines() const { return mPipelines; }
-  RefPtr<MediaPipeline> GetPipelineByTrackId_m(const std::string& trackId);
-  // This is needed so PeerConnectionImpl can unregister itself as
-  // PrincipalChangeObserver from each track.
-  const std::map<std::string, RefPtr<dom::MediaStreamTrack>>&
-  GetMediaStreamTracks() const { return mTracks; }
-  dom::MediaStreamTrack* GetTrackById(const std::string& trackId) const
-  {
-    auto it = mTracks.find(trackId);
-    if (it == mTracks.end()) {
-      return nullptr;
-    }
-
-    return it->second;
-  }
-  const std::string& GetId() const { return mId; }
-
-  void DetachTransport_s();
-  virtual void DetachMedia_m();
-  bool AnyCodecHasPluginID(uint64_t aPluginID);
-protected:
-  void EndTrack(MediaStream* stream, dom::MediaStreamTrack* track);
-  RefPtr<DOMMediaStream> mMediaStream;
-  PeerConnectionMedia *mParent;
-  const std::string mId;
-  // These get set up before we generate our local description, the pipelines
-  // and conduits are set up once offer/answer completes.
-  std::map<std::string, RefPtr<dom::MediaStreamTrack>> mTracks;
-  std::map<std::string, RefPtr<MediaPipeline>> mPipelines;
-};
-
-// TODO(ekr@rtfm.com): Refactor {Local,Remote}SourceStreamInfo
-// bug 837539.
-class LocalSourceStreamInfo : public SourceStreamInfo {
-  ~LocalSourceStreamInfo() {
-    mMediaStream = nullptr;
-  }
-public:
-  LocalSourceStreamInfo(DOMMediaStream *aMediaStream,
-                        PeerConnectionMedia *aParent,
-                        const std::string& aId)
-     : SourceStreamInfo(aMediaStream, aParent, aId) {}
+class MediaPipeline;
+class MediaPipelineFilter;
+class JsepSession;
 
-  nsresult TakePipelineFrom(RefPtr<LocalSourceStreamInfo>& info,
-                            const std::string& oldTrackId,
-                            dom::MediaStreamTrack& aNewTrack,
-                            const std::string& newTrackId);
-
-  void UpdateSinkIdentity_m(dom::MediaStreamTrack* aTrack,
-                            nsIPrincipal* aPrincipal,
-                            const PeerIdentity* aSinkIdentity);
-
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LocalSourceStreamInfo)
-
-private:
-  already_AddRefed<MediaPipeline> ForgetPipelineByTrackId_m(
-      const std::string& trackId);
-};
-
-class RemoteTrackSource : public dom::MediaStreamTrackSource
-{
-public:
-  explicit RemoteTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel)
-    : dom::MediaStreamTrackSource(aPrincipal, aLabel) {}
-
-  dom::MediaSourceEnum GetMediaSource() const override
-  {
-    return dom::MediaSourceEnum::Other;
-  }
-
-  already_AddRefed<PledgeVoid>
-  ApplyConstraints(nsPIDOMWindowInner* aWindow,
-                   const dom::MediaTrackConstraints& aConstraints,
-                   dom::CallerType aCallerType) override;
-
-  void Stop() override
-  {
-    // XXX (Bug 1314270): Implement rejection logic if necessary when we have
-    //                    clarity in the spec.
-  }
-
-  void SetPrincipal(nsIPrincipal* aPrincipal)
-  {
-    mPrincipal = aPrincipal;
-    PrincipalChanged();
-  }
-
-protected:
-  virtual ~RemoteTrackSource() {}
-};
-
-class RemoteSourceStreamInfo : public SourceStreamInfo {
-  ~RemoteSourceStreamInfo() {}
- public:
-  RemoteSourceStreamInfo(already_AddRefed<DOMMediaStream> aMediaStream,
-                         PeerConnectionMedia *aParent,
-                         const std::string& aId)
-    : SourceStreamInfo(aMediaStream, aParent, aId),
-      mReceiving(false)
-  {
-  }
-
-  void DetachMedia_m() override;
-  void RemoveTrack(const std::string& trackId) override;
-  void SyncPipeline(RefPtr<MediaPipelineReceive> aPipeline);
-
-  void UpdatePrincipal_m(nsIPrincipal* aPrincipal);
-
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteSourceStreamInfo)
-
-  void AddTrack(const std::string& trackId,
-                const RefPtr<dom::MediaStreamTrack>& aTrack) override
-  {
-    SourceStreamInfo::AddTrack(trackId, aTrack);
-  }
-
-  TrackID GetNumericTrackId(const std::string& trackId) const
-  {
-    dom::MediaStreamTrack* track = GetTrackById(trackId);
-    if (!track) {
-      return TRACK_INVALID;
-    }
-    return track->mTrackID;
-  }
-
-  void StartReceiving();
-
- private:
-  // True iff SetPullEnabled(true) has been called on the DOMMediaStream. This
-  // happens when offer/answer concludes.
-  bool mReceiving;
-};
-
+// TODO(bug XXXXX): If we move the TransceiverImpl stuff out of here, this will
+// be a class that handles just the transport stuff, and we can rename it to
+// something more explanatory (say, PeerConnectionTransportManager).
 class PeerConnectionMedia : public sigslot::has_slots<> {
-  ~PeerConnectionMedia()
-  {
-    MOZ_RELEASE_ASSERT(!mMainThread);
-  }
+  ~PeerConnectionMedia();
 
  public:
   explicit PeerConnectionMedia(PeerConnectionImpl *parent);
 
   enum IceRestartState { ICE_RESTART_NONE,
                          ICE_RESTART_PROVISIONAL,
                          ICE_RESTART_COMMITTED
   };
@@ -259,18 +73,21 @@ class PeerConnectionMedia : public sigsl
     return mIceCtxHdlr->ctx()->GetStreamCount();
   }
 
   // Ensure ICE transports exist that we might need when offer/answer concludes
   void EnsureTransports(const JsepSession& aSession);
 
   // Activate or remove ICE transports at the conclusion of offer/answer,
   // or when rollback occurs.
-  void ActivateOrRemoveTransports(const JsepSession& aSession,
-                                  const bool forceIceTcp);
+  nsresult ActivateOrRemoveTransports(const JsepSession& aSession,
+                                      const bool forceIceTcp);
+
+  // Update the transports on the TransceiverImpls
+  nsresult UpdateTransceiverTransports(const JsepSession& aSession);
 
   // Start ICE checks.
   void StartIceChecks(const JsepSession& session);
 
   bool IsIceRestarting() const;
   IceRestartState GetIceRestartState() const;
 
   // Begin ICE restart
@@ -286,61 +103,48 @@ class PeerConnectionMedia : public sigsl
   // Process a trickle ICE candidate.
   void AddIceCandidate(const std::string& candidate, const std::string& mid,
                        uint32_t aMLine);
 
   // Handle notifications of network online/offline events.
   void UpdateNetworkState(bool online);
 
   // Handle complete media pipelines.
-  nsresult UpdateMediaPipelines(const JsepSession& session);
+  // This updates codec parameters, starts/stops send/receive, and other
+  // stuff that doesn't necessarily require negotiation. This can be called at
+  // any time, not just when an offer/answer exchange completes.
+  // TODO: Let's move this to PeerConnectionImpl
+  nsresult UpdateMediaPipelines();
 
-  // Add a track (main thread only)
-  nsresult AddTrack(DOMMediaStream& aMediaStream,
-                    const std::string& streamId,
-                    dom::MediaStreamTrack& aTrack,
-                    const std::string& trackId);
-
-  nsresult RemoveLocalTrack(const std::string& streamId,
-                            const std::string& trackId);
-  nsresult RemoveRemoteTrack(const std::string& streamId,
-                            const std::string& trackId);
+  // TODO: Let's move the TransceiverImpl stuff to PeerConnectionImpl.
+  nsresult AddTransceiver(
+      RefPtr<JsepTransceiver> aJsepTransceiver,
+      OwningNonNull<DOMMediaStream>& aReceiveStream,
+      RefPtr<dom::MediaStreamTrack>& aSendTrack,
+      RefPtr<TransceiverImpl>* aTransceiverImpl);
 
-  // Get a specific local stream
-  uint32_t LocalStreamsLength()
-  {
-    return mLocalSourceStreams.Length();
-  }
-  LocalSourceStreamInfo* GetLocalStreamByIndex(int index);
-  LocalSourceStreamInfo* GetLocalStreamById(const std::string& id);
-  LocalSourceStreamInfo* GetLocalStreamByTrackId(const std::string& id);
+  void GetTransmitPipelinesMatching(
+      dom::MediaStreamTrack* aTrack,
+      nsTArray<RefPtr<MediaPipeline>>* aPipelines);
 
-  // Get a specific remote stream
-  uint32_t RemoteStreamsLength()
-  {
-    return mRemoteSourceStreams.Length();
-  }
+  void GetReceivePipelinesMatching(
+      dom::MediaStreamTrack* aTrack,
+      nsTArray<RefPtr<MediaPipeline>>* aPipelines);
 
-  RemoteSourceStreamInfo* GetRemoteStreamByIndex(size_t index);
-  RemoteSourceStreamInfo* GetRemoteStreamById(const std::string& id);
-  RemoteSourceStreamInfo* GetRemoteStreamByTrackId(const std::string& id);
+  nsresult AddRIDExtension(dom::MediaStreamTrack& aRecvTrack,
+                           unsigned short aExtensionId);
 
-  // Add a remote stream.
-  nsresult AddRemoteStream(RefPtr<RemoteSourceStreamInfo> aInfo);
-
-  nsresult ReplaceTrack(const std::string& aOldStreamId,
-                        const std::string& aOldTrackId,
-                        dom::MediaStreamTrack& aNewTrack,
-                        const std::string& aNewStreamId,
-                        const std::string& aNewTrackId);
+  nsresult AddRIDFilter(dom::MediaStreamTrack& aRecvTrack,
+                        const nsAString& aRid);
 
   // In cases where the peer isn't yet identified, we disable the pipeline (not
   // the stream, that would potentially affect others), so that it sends
   // black/silence.  Once the peer is identified, re-enable those streams.
   // aTrack will be set if this update came from a principal change on aTrack.
+  // TODO: Move to PeerConnectionImpl
   void UpdateSinkIdentity_m(dom::MediaStreamTrack* aTrack,
                             nsIPrincipal* aPrincipal,
                             const PeerIdentity* aSinkIdentity);
   // this determines if any track is peerIdentity constrained
   bool AnyLocalTrackHasPeerIdentity() const;
   // When we finally learn who is on the other end, we need to change the ownership
   // on streams
   void UpdateRemoteStreamPrincipals_m(nsIPrincipal* aPrincipal);
@@ -361,79 +165,48 @@ class PeerConnectionMedia : public sigsl
     int index_inner = GetTransportFlowIndex(aStreamIndex, aIsRtcp);
 
     if (mTransportFlows.find(index_inner) == mTransportFlows.end())
       return nullptr;
 
     return mTransportFlows[index_inner];
   }
 
+  // Used by PCImpl in a couple of places. Might be good to move that code in
+  // here.
+  std::vector<RefPtr<TransceiverImpl>>& GetTransceivers()
+  {
+    return mTransceivers;
+  }
+
   // Add a transport flow
   void AddTransportFlow(int aIndex, bool aRtcp,
                         const RefPtr<TransportFlow> &aFlow);
   void RemoveTransportFlow(int aIndex, bool aRtcp);
   void ConnectDtlsListener_s(const RefPtr<TransportFlow>& aFlow);
   void DtlsConnected_s(TransportLayer* aFlow,
                        TransportLayer::State state);
   static void DtlsConnected_m(const std::string& aParentHandle,
                               bool aPrivacyRequested);
 
-  RefPtr<AudioSessionConduit> GetAudioConduit(size_t level) {
-    auto it = mConduits.find(level);
-    if (it == mConduits.end()) {
-      return nullptr;
-    }
-
-    if (it->second.first) {
-      MOZ_ASSERT(false, "In GetAudioConduit, we found a video conduit!");
-      return nullptr;
-    }
-
-    return RefPtr<AudioSessionConduit>(
-        static_cast<AudioSessionConduit*>(it->second.second.get()));
-  }
-
-  RefPtr<VideoSessionConduit> GetVideoConduit(size_t level) {
-    auto it = mConduits.find(level);
-    if (it == mConduits.end()) {
-      return nullptr;
-    }
-
-    if (!it->second.first) {
-      MOZ_ASSERT(false, "In GetVideoConduit, we found an audio conduit!");
-      return nullptr;
-    }
-
-    return RefPtr<VideoSessionConduit>(
-        static_cast<VideoSessionConduit*>(it->second.second.get()));
-  }
-
-  void AddVideoConduit(size_t level, const RefPtr<VideoSessionConduit> &aConduit) {
-    mConduits[level] = std::make_pair(true, aConduit);
-  }
-
-  // Add a conduit
-  void AddAudioConduit(size_t level, const RefPtr<AudioSessionConduit> &aConduit) {
-    mConduits[level] = std::make_pair(false, aConduit);
-  }
-
   // ICE state signals
   sigslot::signal2<NrIceCtx*, NrIceCtx::GatheringState>
       SignalIceGatheringStateChange;
   sigslot::signal2<NrIceCtx*, NrIceCtx::ConnectionState>
       SignalIceConnectionStateChange;
   // This passes a candidate:... attribute  and level
   sigslot::signal2<const std::string&, uint16_t> SignalCandidate;
   // This passes address, port, level of the default candidate.
   sigslot::signal5<const std::string&, uint16_t,
                    const std::string&, uint16_t, uint16_t>
       SignalUpdateDefaultCandidate;
   sigslot::signal1<uint16_t>
       SignalEndOfLocalCandidates;
 
+  // TODO: Move to PeerConnectionImpl
   RefPtr<WebRtcCallWrapper> mCall;
 
  private:
   void InitLocalAddrs(); // for stun local address IPC request
   nsresult InitProxy();
   class ProtocolProxyQueryHandler : public nsIProtocolProxyCallback {
    public:
     explicit ProtocolProxyQueryHandler(PeerConnectionMedia *pcm) :
@@ -473,16 +246,20 @@ class PeerConnectionMedia : public sigsl
   void EnsureTransport_s(size_t aLevel, size_t aComponentCount);
   void ActivateOrRemoveTransport_s(
       size_t aMLine,
       size_t aComponentCount,
       const std::string& aUfrag,
       const std::string& aPassword,
       const std::vector<std::string>& aCandidateList);
   void RemoveTransportsAtOrAfter_s(size_t aMLine);
+  nsresult UpdateTransportFlows(const JsepTransceiver& transceiver);
+  nsresult UpdateTransportFlow(size_t aLevel,
+                               bool aIsRtcp,
+                               const JsepTransport& aTransport);
 
   void GatherIfReady();
   void FlushIceCtxOperationQueueIfReady();
   void PerformOrEnqueueIceCtxOperation(nsIRunnable* runnable);
   void EnsureIceGathering_s(bool aDefaultRouteOnly, bool aProxyOnly);
   void StartIceChecks_s(bool aIsControlling,
                         bool aIsOfferer,
                         bool aIsIceLite,
@@ -539,25 +316,17 @@ class PeerConnectionMedia : public sigsl
   }
 
   // The parent PC
   PeerConnectionImpl *mParent;
   // and a loose handle on it for event driven stuff
   std::string mParentHandle;
   std::string mParentName;
 
-  // A list of streams returned from GetUserMedia
-  // This is only accessed on the main thread (with one special exception)
-  nsTArray<RefPtr<LocalSourceStreamInfo> > mLocalSourceStreams;
-
-  // A list of streams provided by the other side
-  // This is only accessed on the main thread (with one special exception)
-  nsTArray<RefPtr<RemoteSourceStreamInfo> > mRemoteSourceStreams;
-
-  std::map<size_t, std::pair<bool, RefPtr<MediaSessionConduit>>> mConduits;
+  std::vector<RefPtr<TransceiverImpl>> mTransceivers;
 
   // ICE objects
   RefPtr<NrIceCtxHandler> mIceCtxHdlr;
 
   // DNS
   RefPtr<NrIceResolver> mDNSResolver;
 
   // Transport flows: even is RTP, odd is RTCP
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/peerconnection/RemoteTrackSource.h
@@ -0,0 +1,56 @@
+/* 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/. */
+
+#ifndef _REMOTE_TRACK_SOURCE_H_
+#define _REMOTE_TRACK_SOURCE_H_
+
+#include "MediaStreamTrack.h"
+#include "MediaStreamError.h"
+
+namespace mozilla {
+
+class RemoteTrackSource : public dom::MediaStreamTrackSource
+{
+public:
+  explicit RemoteTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel)
+    : dom::MediaStreamTrackSource(aPrincipal, aLabel) {}
+
+  dom::MediaSourceEnum GetMediaSource() const override
+  {
+    return dom::MediaSourceEnum::Other;
+  }
+
+  already_AddRefed<PledgeVoid>
+  ApplyConstraints(nsPIDOMWindowInner* aWindow,
+                   const dom::MediaTrackConstraints& aConstraints,
+                   dom::CallerType aCallerType) override
+  {
+    RefPtr<PledgeVoid> p = new PledgeVoid();
+    p->Reject(
+        new dom::MediaStreamError(aWindow,
+                                  NS_LITERAL_STRING("OverconstrainedError"),
+                                  NS_LITERAL_STRING("")));
+    return p.forget();
+  }
+
+  void Stop() override
+  {
+    // XXX (Bug 1314270): Implement rejection logic if necessary when we have
+    //                    clarity in the spec.
+  }
+
+  void SetPrincipal(nsIPrincipal* aPrincipal)
+  {
+    mPrincipal = aPrincipal;
+    PrincipalChanged();
+  }
+
+protected:
+  virtual ~RemoteTrackSource() {}
+};
+
+} // namespace mozilla
+
+#endif // _REMOTE_TRACK_SOURCE_H_
+
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
@@ -0,0 +1,1030 @@
+/* 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/. */
+
+#include "TransceiverImpl.h"
+#include "mtransport/runnable_utils.h"
+#include "mozilla/UniquePtr.h"
+#include <sstream>
+#include <string>
+#include <vector>
+#include <queue>
+#include "AudioConduit.h"
+#include "VideoConduit.h"
+#include "MediaStreamGraph.h"
+#include "MediaPipeline.h"
+#include "MediaPipelineFilter.h"
+#include "jsep/JsepTrack.h"
+#include "MediaStreamGraphImpl.h"
+#include "logging.h"
+#include "MediaEngine.h"
+#include "nsIPrincipal.h"
+#include "MediaSegment.h"
+#include "RemoteTrackSource.h"
+#include "MediaConduitInterface.h"
+#include "PeerConnectionMedia.h"
+#include "mozilla/dom/RTCRtpReceiverBinding.h"
+#include "mozilla/dom/RTCRtpSenderBinding.h"
+#include "mozilla/dom/RTCRtpTransceiverBinding.h"
+#include "mozilla/dom/TransceiverImplBinding.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mediapipeline")
+
+TransceiverImpl::TransceiverImpl(
+    const std::string& aPCHandle,
+    RefPtr<JsepTransceiver> aJsepTransceiver,
+    nsIEventTarget* aMainThread,
+    nsIEventTarget* aStsThread,
+    OwningNonNull<DOMMediaStream>& aReceiveStream,
+    RefPtr<dom::MediaStreamTrack>& aSendTrack,
+    RefPtr<WebRtcCallWrapper>& aCallWrapper) :
+  mPCHandle(aPCHandle),
+  mJsepTransceiver(aJsepTransceiver),
+  mHaveStartedReceiving(false),
+  mMainThread(aMainThread),
+  mStsThread(aStsThread),
+  mReceiveStream(aReceiveStream),
+  mSendTrack(aSendTrack),
+  mCallWrapper(aCallWrapper)
+{
+  if (IsVideo()) {
+    InitVideo();
+  } else {
+    InitAudio();
+  }
+
+  mConduit->SetPCHandle(mPCHandle);
+
+  SourceMediaStream* source(mReceiveStream->GetInputStream()->AsSourceStream());
+  mReceivePipeline->AttachMedia(source);
+  StartReceiveStream();
+
+  mTransmitPipeline = new MediaPipelineTransmit(
+      mPCHandle,
+      mMainThread.get(),
+      mStsThread.get(),
+      IsVideo(),
+      mSendTrack,
+      mConduit);
+}
+
+NS_IMPL_ISUPPORTS0(TransceiverImpl)
+
+void
+TransceiverImpl::InitAudio()
+{
+  mConduit = AudioSessionConduit::Create();
+
+  mReceivePipeline = new MediaPipelineReceiveAudio(
+      mPCHandle,
+      mMainThread.get(),
+      mStsThread.get(),
+      static_cast<AudioSessionConduit*>(mConduit.get()));
+}
+
+void
+TransceiverImpl::InitVideo()
+{
+  mConduit = VideoSessionConduit::Create(mCallWrapper);
+
+  mReceivePipeline = new MediaPipelineReceiveVideo(
+      mPCHandle,
+      mMainThread.get(),
+      mStsThread.get(),
+      static_cast<VideoSessionConduit*>(mConduit.get()));
+}
+
+nsresult
+TransceiverImpl::UpdateSinkIdentity(dom::MediaStreamTrack* aTrack,
+                                    nsIPrincipal* aPrincipal,
+                                    const PeerIdentity* aSinkIdentity)
+{
+  mTransmitPipeline->UpdateSinkIdentity_m(aTrack, aPrincipal, aSinkIdentity);
+  return NS_OK;
+}
+
+void
+TransceiverImpl::Shutdown_m()
+{
+  mReceivePipeline->Shutdown_m();
+  mTransmitPipeline->Shutdown_m();
+  mReceivePipeline = nullptr;
+  mTransmitPipeline = nullptr;
+  mSendTrack = nullptr;
+  mConduit = nullptr;
+  RUN_ON_THREAD(mStsThread, WrapRelease(mRtpFlow.forget()), NS_DISPATCH_NORMAL);
+  RUN_ON_THREAD(mStsThread, WrapRelease(mRtcpFlow.forget()), NS_DISPATCH_NORMAL);
+}
+
+TransceiverImpl::~TransceiverImpl()
+{}
+
+nsresult
+TransceiverImpl::UpdateSendTrack(dom::MediaStreamTrack* aSendTrack)
+{
+  MOZ_MTLOG(ML_DEBUG, mPCHandle << "[" << mMid << "]: "
+                      "In " << __FUNCTION__ << "(" << aSendTrack << ")");
+  mSendTrack = aSendTrack;
+  return mTransmitPipeline->ReplaceTrack(mSendTrack);
+}
+
+nsresult
+TransceiverImpl::UpdateTransport(PeerConnectionMedia& aTransportManager)
+{
+  if (!mJsepTransceiver->HasLevel()) {
+    return NS_OK;
+  }
+
+  ASSERT_ON_THREAD(mMainThread);
+  nsAutoPtr<MediaPipelineFilter> filter;
+
+  mRtpFlow = aTransportManager.GetTransportFlow(
+      mJsepTransceiver->GetTransportLevel(), false);
+  mRtcpFlow = aTransportManager.GetTransportFlow(
+      mJsepTransceiver->GetTransportLevel(), true);
+
+  if (mJsepTransceiver->HasBundleLevel() &&
+      mJsepTransceiver->mReceiving.GetNegotiatedDetails()) {
+    filter = new MediaPipelineFilter;
+
+    // Add remote SSRCs so we can distinguish which RTP packets actually
+    // belong to this pipeline (also RTCP sender reports).
+    for (unsigned int ssrc : mJsepTransceiver->mReceiving.GetSsrcs()) {
+      filter->AddRemoteSSRC(ssrc);
+    }
+
+    // TODO(bug 1105005): Tell the filter about the mid for this track
+
+    // Add unique payload types as a last-ditch fallback
+    auto uniquePts =
+      mJsepTransceiver->mReceiving.GetNegotiatedDetails()->GetUniquePayloadTypes();
+    for (unsigned char& uniquePt : uniquePts) {
+      filter->AddUniquePT(uniquePt);
+    }
+  }
+
+  mReceivePipeline->UpdateTransport_m(mRtpFlow, mRtcpFlow, filter);
+  mTransmitPipeline->UpdateTransport_m(mRtpFlow, mRtcpFlow, nsAutoPtr<MediaPipelineFilter>());
+  return NS_OK;
+}
+
+nsresult
+TransceiverImpl::UpdateConduit()
+{
+  MOZ_MTLOG(ML_DEBUG, mPCHandle << "[" << mMid << "]: "
+                      "In UpdateConduit");
+
+  if (mJsepTransceiver->IsAssociated()) {
+    mMid = mJsepTransceiver->GetMid();
+  } else {
+    mMid.clear();
+  }
+
+  MOZ_MTLOG(ML_DEBUG, mPCHandle << "[" << mMid << "]: "
+            "Stopping transmit/receive conduits");
+  mConduit->StopReceiving();
+  mConduit->StopTransmitting();
+
+  if (mJsepTransceiver->IsStopped()) {
+    MOZ_MTLOG(ML_DEBUG, this << " " << mPCHandle << " [" << mMid << "]: "
+              "Transceiver is stopped. (receive pipeline = " << mReceivePipeline << ")");
+    // Make sure that stats queries stop working on this transceiver.
+    UpdateSendTrack(nullptr);
+    mHaveStartedReceiving = false;
+    return NS_OK;
+  }
+
+  // NOTE(pkerr) - the Call API requires the both local_ssrc and remote_ssrc be
+  // set to a non-zero value or the CreateVideo...Stream call will fail.
+  if (mJsepTransceiver->mSending.GetSsrcs().empty()) {
+    MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                        "No local SSRC set! (Should be set regardless of "
+                        "whether we're sending RTP; we need a local SSRC in "
+                        "all cases)");
+    return NS_ERROR_FAILURE;
+  }
+
+  if(!mConduit->SetLocalSSRCs(mJsepTransceiver->mSending.GetSsrcs())) {
+    MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                        "SetLocalSSRCs failed");
+    return NS_ERROR_FAILURE;
+  }
+
+  mConduit->SetLocalCNAME(mJsepTransceiver->mSending.GetCNAME().c_str());
+
+  nsresult rv;
+
+  if (IsVideo()) {
+    rv = UpdateVideoConduit();
+  } else {
+    rv = UpdateAudioConduit();
+  }
+
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  if (mJsepTransceiver->mReceiving.GetActive()) {
+    MOZ_ASSERT(mReceiveStream);
+    mConduit->StartReceiving();
+    mHaveStartedReceiving = true;
+  } else {
+    mConduit->StopReceiving(); // Just in case...
+  }
+
+  if (mJsepTransceiver->mSending.GetActive()) {
+    if (!mSendTrack) {
+      MOZ_MTLOG(ML_WARNING, mPCHandle << "[" << mMid << "]: "
+                            "Starting transmit conduit without send track!");
+    }
+    mConduit->StartTransmitting();
+    mTransmitPipeline->AttachToTrack();
+  } else {
+    mTransmitPipeline->DetachFromTrack();
+    mConduit->StopTransmitting(); // Just in case...
+  }
+
+  return NS_OK;
+}
+
+nsresult
+TransceiverImpl::UpdatePrincipal(nsIPrincipal* aPrincipal)
+{
+  nsTArray<RefPtr<dom::MediaStreamTrack>> receiveTracks;
+  mReceiveStream->GetTracks(receiveTracks);
+  if (receiveTracks.Length() != 1) {
+    MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                        "mReceiveStream doesn't have exactly one track (it has "
+                        << receiveTracks.Length() << ")");
+    MOZ_CRASH();
+    return NS_ERROR_FAILURE;
+  }
+
+  // This blasts away the existing principal.
+  // We only do this when we become certain that the all tracks are safe to make
+  // accessible to the script principal.
+  RemoteTrackSource& source =
+    static_cast<RemoteTrackSource&>(receiveTracks[0]->GetSource());
+  source.SetPrincipal(aPrincipal);
+
+  mReceivePipeline->SetPrincipalHandle_m(MakePrincipalHandle(aPrincipal));
+  return NS_OK;
+}
+
+nsresult
+TransceiverImpl::SyncWithMatchingVideoConduits(
+    std::vector<RefPtr<TransceiverImpl>>& transceivers)
+{
+  if (IsVideo()) {
+    MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                        << __FUNCTION__ << " called when transceiver is not "
+                        "video! This should never happen.");
+    MOZ_CRASH();
+    return NS_ERROR_UNEXPECTED;
+  }
+
+  std::set<std::string> myReceiveStreamIds;
+  myReceiveStreamIds.insert(mJsepTransceiver->mReceiving.GetStreamIds().begin(),
+                            mJsepTransceiver->mReceiving.GetStreamIds().end());
+
+  for (RefPtr<TransceiverImpl>& transceiver : transceivers) {
+    if (!transceiver->IsVideo()) {
+      // |this| is an audio transceiver, so we skip other audio transceivers
+      continue;
+    }
+
+    // Maybe could make this more efficient by cacheing this set, but probably
+    // not worth it.
+    for (const std::string& streamId :
+         transceiver->mJsepTransceiver->mReceiving.GetStreamIds()) {
+      if (myReceiveStreamIds.count(streamId)) {
+        // Ok, we have one video, one non-video - cross the streams!
+        WebrtcAudioConduit *audio_conduit =
+          static_cast<WebrtcAudioConduit*>(mConduit.get());
+        WebrtcVideoConduit *video_conduit =
+          static_cast<WebrtcVideoConduit*>(transceiver->mConduit.get());
+
+        video_conduit->SyncTo(audio_conduit);
+        MOZ_MTLOG(ML_DEBUG, "Syncing " << video_conduit << " to "
+                            << audio_conduit);
+      }
+    }
+  }
+
+  return NS_OK;
+}
+
+bool
+TransceiverImpl::ConduitHasPluginID(uint64_t aPluginID)
+{
+  return mConduit->CodecPluginID() == aPluginID;
+}
+
+bool
+TransceiverImpl::HasSendTrack(const dom::MediaStreamTrack* aSendTrack) const
+{
+  if (!mSendTrack) {
+    return false;
+  }
+
+  if (!aSendTrack) {
+    return true;
+  }
+
+  return mSendTrack.get() == aSendTrack;
+}
+
+void
+TransceiverImpl::SyncWithJS(dom::RTCRtpTransceiver& aJsTransceiver,
+                            ErrorResult& aRv)
+{
+  MOZ_MTLOG(ML_DEBUG, "Syncing with JS transceiver");
+
+  // Update stopped, both ways, since either JSEP or JS can stop these
+  if (mJsepTransceiver->IsStopped()) {
+    // We don't call Stop(), because that causes another sync
+    aJsTransceiver.SetStopped(aRv);
+  } else if (aJsTransceiver.GetStopped(aRv)) {
+    mJsepTransceiver->Stop();
+  }
+
+  // Lots of this in here for simple getters that should never fail. Lame.
+  // Just propagate the exception and let JS log it.
+  if (aRv.Failed()) {
+    return;
+  }
+
+  // Update direction from JS only
+  dom::RTCRtpTransceiverDirection direction = aJsTransceiver.GetDirection(aRv);
+
+  if (aRv.Failed()) {
+    return;
+  }
+
+  switch (direction) {
+    case dom::RTCRtpTransceiverDirection::Sendrecv:
+      mJsepTransceiver->mJsDirection =
+        SdpDirectionAttribute::Direction::kSendrecv;
+      break;
+    case dom::RTCRtpTransceiverDirection::Sendonly:
+      mJsepTransceiver->mJsDirection =
+        SdpDirectionAttribute::Direction::kSendonly;
+      break;
+    case dom::RTCRtpTransceiverDirection::Recvonly:
+      mJsepTransceiver->mJsDirection =
+        SdpDirectionAttribute::Direction::kRecvonly;
+      break;
+    case dom::RTCRtpTransceiverDirection::Inactive:
+      mJsepTransceiver->mJsDirection =
+        SdpDirectionAttribute::Direction::kInactive;
+      break;
+    default:
+      MOZ_ASSERT(false);
+      aRv = NS_ERROR_INVALID_ARG;
+      return;
+  }
+
+  // Update send track ids in JSEP
+  RefPtr<dom::RTCRtpSender> sender = aJsTransceiver.GetSender(aRv);
+  if (aRv.Failed()) {
+    return;
+  }
+
+  RefPtr<dom::MediaStreamTrack> sendTrack = sender->GetTrack(aRv);
+  if (aRv.Failed()) {
+    return;
+  }
+
+  if (sendTrack) {
+    nsString wideTrackId;
+    sendTrack->GetId(wideTrackId);
+    std::string trackId = NS_ConvertUTF16toUTF8(wideTrackId).get();
+    MOZ_ASSERT(!trackId.empty());
+
+    nsTArray<RefPtr<DOMMediaStream>> streams;
+    sender->MozGetStreams(streams, aRv);
+    if (aRv.Failed()) {
+      return;
+    }
+
+    std::vector<std::string> streamIds;
+    for (const auto& stream : streams) {
+      nsString wideStreamId;
+      stream->GetId(wideStreamId);
+      std::string streamId = NS_ConvertUTF16toUTF8(wideStreamId).get();
+      MOZ_ASSERT(!streamId.empty());
+      streamIds.push_back(streamId);
+    }
+
+    mJsepTransceiver->mSending.UpdateTrack(streamIds, trackId);
+  }
+
+  // Update RTCRtpParameters
+  // TODO: Both ways for things like ssrc, codecs, header extensions, etc
+
+  dom::RTCRtpParameters parameters;
+  sender->GetParameters(parameters, aRv);
+
+  if (aRv.Failed()) {
+    return;
+  }
+
+  std::vector<JsepTrack::JsConstraints> constraints;
+
+  if (parameters.mEncodings.WasPassed()) {
+    for (auto& encoding : parameters.mEncodings.Value()) {
+      JsepTrack::JsConstraints constraint;
+      if (encoding.mRid.WasPassed()) {
+        // TODO: Either turn on the RID RTP header extension in JsepSession, or
+        // just leave that extension on all the time?
+        constraint.rid = NS_ConvertUTF16toUTF8(encoding.mRid.Value()).get();
+      }
+      if (encoding.mMaxBitrate.WasPassed()) {
+        constraint.constraints.maxBr = encoding.mMaxBitrate.Value();
+      }
+      constraint.constraints.scaleDownBy = encoding.mScaleResolutionDownBy;
+      constraints.push_back(constraint);
+    }
+  }
+
+  // TODO: Update conduits?
+
+  mJsepTransceiver->mSending.SetJsConstraints(constraints);
+
+  // Update webrtc track id in JS; the ids in SDP are not surfaced to content,
+  // because they don't follow the rules that track/stream ids must. Our JS
+  // code must be able to map the SDP ids to the actual tracks/streams, and
+  // this is how the mapping for track ids is updated.
+  RefPtr<dom::RTCRtpReceiver> receiver = aJsTransceiver.GetReceiver(aRv);
+  if (aRv.Failed()) {
+    return;
+  }
+
+  nsString webrtcTrackId =
+    NS_ConvertUTF8toUTF16(mJsepTransceiver->mReceiving.GetTrackId().c_str());
+  MOZ_MTLOG(ML_DEBUG, "Setting webrtc track id: "
+      << mJsepTransceiver->mReceiving.GetTrackId().c_str());
+  receiver->SetWebrtcTrackId(webrtcTrackId, aRv);
+
+  if (aRv.Failed()) {
+    return;
+  }
+
+  // mid from JSEP
+  if (mJsepTransceiver->IsAssociated()) {
+    aJsTransceiver.SetMid(
+        NS_ConvertUTF8toUTF16(mJsepTransceiver->GetMid().c_str()),
+        aRv);
+  } else {
+    aJsTransceiver.UnsetMid(aRv);
+  }
+
+  if (aRv.Failed()) {
+    return;
+  }
+
+  // currentDirection from JSEP, but not if "this transceiver has never been
+  // represented in an offer/answer exchange"
+  if (mJsepTransceiver->HasLevel()) {
+    dom::RTCRtpTransceiverDirection currentDirection;
+    if (mJsepTransceiver->mSending.GetActive()) {
+      if (mJsepTransceiver->mReceiving.GetActive()) {
+        currentDirection = dom::RTCRtpTransceiverDirection::Sendrecv;
+      } else {
+        currentDirection = dom::RTCRtpTransceiverDirection::Sendonly;
+      }
+    } else {
+      if (mJsepTransceiver->mReceiving.GetActive()) {
+        currentDirection = dom::RTCRtpTransceiverDirection::Recvonly;
+      } else {
+        currentDirection = dom::RTCRtpTransceiverDirection::Inactive;
+      }
+    }
+
+    aJsTransceiver.SetCurrentDirection(currentDirection, aRv);
+
+    if (aRv.Failed()) {
+      return;
+    }
+  }
+
+  // AddTrack magic from JS
+  if (aJsTransceiver.GetAddTrackMagic(aRv)) {
+    mJsepTransceiver->SetAddTrackMagic();
+  }
+
+  if (aRv.Failed()) {
+    return;
+  }
+
+  if (mJsepTransceiver->IsRemoved()) {
+    aJsTransceiver.Remove(aRv);
+  }
+}
+
+void
+TransceiverImpl::InsertDTMFTone(int tone, uint32_t duration)
+{
+  RefPtr<AudioSessionConduit> conduit(static_cast<AudioSessionConduit*>(
+        mConduit.get()));
+  mStsThread->Dispatch(WrapRunnableNM([conduit, tone, duration] () {
+        //Note: We default to channel 0, not inband, and 6dB attenuation.
+        //      here. We might want to revisit these choices in the future.
+        conduit->InsertDTMFTone(0, tone, true, duration, 6);
+        }), NS_DISPATCH_NORMAL);
+}
+
+bool
+TransceiverImpl::HasReceiveTrack(const dom::MediaStreamTrack* aRecvTrack) const
+{
+  if (!mHaveStartedReceiving) {
+    return false;
+  }
+
+  if (!aRecvTrack) {
+    return true;
+  }
+
+  return mReceiveStream->HasTrack(*aRecvTrack);
+}
+
+bool
+TransceiverImpl::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+                            JS::MutableHandle<JSObject*> aReflector)
+{
+  return dom::TransceiverImplBinding::Wrap(aCx, this, aGivenProto, aReflector);
+}
+
+already_AddRefed<dom::MediaStreamTrack>
+TransceiverImpl::GetReceiveTrack()
+{
+  nsTArray<RefPtr<dom::MediaStreamTrack>> receiveTracks;
+  mReceiveStream->GetTracks(receiveTracks);
+  if (receiveTracks.Length() != 1) {
+    return nullptr;
+  }
+
+  return receiveTracks[0].forget();
+}
+
+RefPtr<MediaPipeline>
+TransceiverImpl::GetSendPipeline()
+{
+  return mTransmitPipeline;
+}
+
+RefPtr<MediaPipeline>
+TransceiverImpl::GetReceivePipeline()
+{
+  MOZ_MTLOG(ML_WARNING, mPCHandle << "[" << mMid << "]: " << __FUNCTION__ <<
+            " returning " << mReceivePipeline.get());
+  return mReceivePipeline;
+}
+
+void
+TransceiverImpl::AddRIDExtension(unsigned short aExtensionId)
+{
+  mReceivePipeline->AddRIDExtension_m(aExtensionId);
+}
+
+void
+TransceiverImpl::AddRIDFilter(const nsAString& aRid)
+{
+  mReceivePipeline->AddRIDFilter_m(NS_ConvertUTF16toUTF8(aRid).get());
+}
+
+static std::vector<JsepCodecDescription*>
+GetCodecs(const JsepTrackNegotiatedDetails& aDetails)
+{
+  // We do not try to handle cases where a codec is not used on the primary
+  // encoding.
+  if (aDetails.GetEncodingCount()) {
+    return aDetails.GetEncoding(0).GetCodecs();
+  }
+  return std::vector<JsepCodecDescription*>();
+}
+
+static nsresult
+JsepCodecDescToCodecConfig(const JsepCodecDescription& aCodec,
+                           AudioCodecConfig** aConfig)
+{
+  MOZ_ASSERT(aCodec.mType == SdpMediaSection::kAudio);
+  if (aCodec.mType != SdpMediaSection::kAudio)
+    return NS_ERROR_INVALID_ARG;
+
+  const JsepAudioCodecDescription& desc =
+      static_cast<const JsepAudioCodecDescription&>(aCodec);
+
+  uint16_t pt;
+
+  if (!desc.GetPtAsInt(&pt)) {
+    MOZ_MTLOG(ML_ERROR, "Invalid payload type: " << desc.mDefaultPt);
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  *aConfig = new AudioCodecConfig(pt,
+                                  desc.mName,
+                                  desc.mClock,
+                                  desc.mPacketSize,
+                                  desc.mForceMono ? 1 : desc.mChannels,
+                                  desc.mBitrate,
+                                  desc.mFECEnabled);
+  (*aConfig)->mMaxPlaybackRate = desc.mMaxPlaybackRate;
+  (*aConfig)->mDtmfEnabled = desc.mDtmfEnabled;
+
+  return NS_OK;
+}
+
+static nsresult
+NegotiatedDetailsToAudioCodecConfigs(const JsepTrackNegotiatedDetails& aDetails,
+                                     PtrVector<AudioCodecConfig>* aConfigs)
+{
+  std::vector<JsepCodecDescription*> codecs(GetCodecs(aDetails));
+  for (const JsepCodecDescription* codec : codecs) {
+    AudioCodecConfig* config;
+    if (NS_FAILED(JsepCodecDescToCodecConfig(*codec, &config))) {
+      return NS_ERROR_INVALID_ARG;
+    }
+    aConfigs->values.push_back(config);
+  }
+
+  if (aConfigs->values.empty()) {
+    MOZ_MTLOG(ML_ERROR, "Can't set up a conduit with 0 codecs");
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+TransceiverImpl::UpdateAudioConduit()
+{
+  RefPtr<AudioSessionConduit> conduit = static_cast<AudioSessionConduit*>(
+      mConduit.get());
+
+  if (mJsepTransceiver->mReceiving.GetNegotiatedDetails() &&
+      mJsepTransceiver->mReceiving.GetActive()) {
+    const auto& details(*mJsepTransceiver->mReceiving.GetNegotiatedDetails());
+    PtrVector<AudioCodecConfig> configs;
+    nsresult rv = NegotiatedDetailsToAudioCodecConfigs(details, &configs);
+
+    if (NS_FAILED(rv)) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "Failed to convert JsepCodecDescriptions to "
+                          "AudioCodecConfigs (recv).");
+      return rv;
+    }
+
+    auto error = conduit->ConfigureRecvMediaCodecs(configs.values);
+
+    if (error) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "ConfigureRecvMediaCodecs failed: " << error);
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  if (mJsepTransceiver->mSending.GetNegotiatedDetails() &&
+      mJsepTransceiver->mSending.GetActive()) {
+    const auto& details(*mJsepTransceiver->mSending.GetNegotiatedDetails());
+    PtrVector<AudioCodecConfig> configs;
+    nsresult rv = NegotiatedDetailsToAudioCodecConfigs(details, &configs);
+
+    if (NS_FAILED(rv)) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "Failed to convert JsepCodecDescriptions to "
+                          "AudioCodecConfigs (send).");
+      return rv;
+    }
+
+    if (configs.values.size() > 1
+        && configs.values.back()->mName == "telephone-event") {
+      // we have a telephone event codec, so we need to make sure
+      // the dynamic pt is set properly
+      conduit->SetDtmfPayloadType(configs.values.back()->mType,
+                                  configs.values.back()->mFreq);
+    }
+
+    auto error = conduit->ConfigureSendMediaCodec(configs.values[0]);
+    if (error) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "ConfigureSendMediaCodec failed: " << error);
+      return NS_ERROR_FAILURE;
+    }
+
+    const SdpExtmapAttributeList::Extmap* audioLevelExt =
+        details.GetExt("urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+
+    if (audioLevelExt) {
+      error = conduit->EnableAudioLevelExtension(true, audioLevelExt->entry);
+
+      if (error) {
+        MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                            "EnableAudioLevelExtension failed: " << error);
+        return NS_ERROR_FAILURE;
+      }
+    }
+  }
+
+  return NS_OK;
+}
+
+static nsresult
+JsepCodecDescToCodecConfig(const JsepCodecDescription& aCodec,
+                           VideoCodecConfig** aConfig)
+{
+  MOZ_ASSERT(aCodec.mType == SdpMediaSection::kVideo);
+  if (aCodec.mType != SdpMediaSection::kVideo) {
+    MOZ_ASSERT(false, "JsepCodecDescription has wrong type");
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  const JsepVideoCodecDescription& desc =
+      static_cast<const JsepVideoCodecDescription&>(aCodec);
+
+  uint16_t pt;
+
+  if (!desc.GetPtAsInt(&pt)) {
+    MOZ_MTLOG(ML_ERROR, "Invalid payload type: " << desc.mDefaultPt);
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  UniquePtr<VideoCodecConfigH264> h264Config;
+
+  if (desc.mName == "H264") {
+    h264Config = MakeUnique<VideoCodecConfigH264>();
+    size_t spropSize = sizeof(h264Config->sprop_parameter_sets);
+    strncpy(h264Config->sprop_parameter_sets,
+            desc.mSpropParameterSets.c_str(),
+            spropSize);
+    h264Config->sprop_parameter_sets[spropSize - 1] = '\0';
+    h264Config->packetization_mode = desc.mPacketizationMode;
+    h264Config->profile_level_id = desc.mProfileLevelId;
+    h264Config->tias_bw = 0; // TODO. Issue 165.
+  }
+
+  VideoCodecConfig* configRaw;
+  configRaw = new VideoCodecConfig(
+      pt, desc.mName, desc.mConstraints, h264Config.get());
+
+  configRaw->mAckFbTypes = desc.mAckFbTypes;
+  configRaw->mNackFbTypes = desc.mNackFbTypes;
+  configRaw->mCcmFbTypes = desc.mCcmFbTypes;
+  configRaw->mRembFbSet = desc.RtcpFbRembIsSet();
+  configRaw->mFECFbSet = desc.mFECEnabled;
+  if (desc.mFECEnabled) {
+    configRaw->mREDPayloadType = desc.mREDPayloadType;
+    configRaw->mULPFECPayloadType = desc.mULPFECPayloadType;
+  }
+
+  *aConfig = configRaw;
+  return NS_OK;
+}
+
+static nsresult
+NegotiatedDetailsToVideoCodecConfigs(const JsepTrackNegotiatedDetails& aDetails,
+                                     PtrVector<VideoCodecConfig>* aConfigs)
+{
+  std::vector<JsepCodecDescription*> codecs(GetCodecs(aDetails));
+  for (const JsepCodecDescription* codec : codecs) {
+    VideoCodecConfig* config;
+    if (NS_FAILED(JsepCodecDescToCodecConfig(*codec, &config))) {
+      return NS_ERROR_INVALID_ARG;
+    }
+
+    config->mTias = aDetails.GetTias();
+
+    for (size_t i = 0; i < aDetails.GetEncodingCount(); ++i) {
+      const JsepTrackEncoding& jsepEncoding(aDetails.GetEncoding(i));
+      if (jsepEncoding.HasFormat(codec->mDefaultPt)) {
+        VideoCodecConfig::SimulcastEncoding encoding;
+        encoding.rid = jsepEncoding.mRid;
+        encoding.constraints = jsepEncoding.mConstraints;
+        config->mSimulcastEncodings.push_back(encoding);
+      }
+    }
+
+    aConfigs->values.push_back(config);
+  }
+
+  return NS_OK;
+}
+
+nsresult
+TransceiverImpl::UpdateVideoConduit()
+{
+  RefPtr<VideoSessionConduit> conduit = static_cast<VideoSessionConduit*>(
+      mConduit.get());
+
+  // NOTE(pkerr) - this is new behavior. Needed because the CreateVideoReceiveStream
+  // method of the Call API will assert (in debug) and fail if a value is not provided
+  // for the remote_ssrc that will be used by the far-end sender.
+  if (!mJsepTransceiver->mReceiving.GetSsrcs().empty()) {
+    MOZ_MTLOG(ML_DEBUG, mPCHandle << "[" << mMid << "]: "
+              "Setting remote SSRC " <<
+              mJsepTransceiver->mReceiving.GetSsrcs().front());
+    conduit->SetRemoteSSRC(mJsepTransceiver->mReceiving.GetSsrcs().front());
+  }
+
+  if (mJsepTransceiver->mReceiving.GetNegotiatedDetails() &&
+      mJsepTransceiver->mReceiving.GetActive()) {
+    const auto& details(*mJsepTransceiver->mReceiving.GetNegotiatedDetails());
+
+    UpdateVideoExtmap(details, false);
+
+    PtrVector<VideoCodecConfig> configs;
+    nsresult rv = NegotiatedDetailsToVideoCodecConfigs(details, &configs);
+
+    if (NS_FAILED(rv)) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "Failed to convert JsepCodecDescriptions to "
+                          "VideoCodecConfigs (recv).");
+      return rv;
+    }
+
+    auto error = conduit->ConfigureRecvMediaCodecs(configs.values);
+
+    if (error) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "ConfigureRecvMediaCodecs failed: " << error);
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  // It is possible for SDP to signal that there is a send track, but there not
+  // actually be a send track, according to the specification; all that needs to
+  // happen is for the transceiver to be configured to send...
+  if (mJsepTransceiver->mSending.GetNegotiatedDetails() &&
+      mJsepTransceiver->mSending.GetActive() &&
+      mSendTrack) {
+    const auto& details(*mJsepTransceiver->mSending.GetNegotiatedDetails());
+
+    UpdateVideoExtmap(details, true);
+
+    nsresult rv = ConfigureVideoCodecMode(*conduit);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+
+    PtrVector<VideoCodecConfig> configs;
+    rv = NegotiatedDetailsToVideoCodecConfigs(details, &configs);
+
+    if (NS_FAILED(rv)) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "Failed to convert JsepCodecDescriptions to "
+                          "VideoCodecConfigs (send).");
+      return rv;
+    }
+
+    auto error = conduit->ConfigureSendMediaCodec(configs.values[0]);
+    if (error) {
+      MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                          "ConfigureSendMediaCodec failed: " << error);
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  return NS_OK;
+}
+
+nsresult
+TransceiverImpl::ConfigureVideoCodecMode(VideoSessionConduit& aConduit)
+{
+  RefPtr<mozilla::dom::VideoStreamTrack> videotrack =
+    mSendTrack->AsVideoStreamTrack();
+
+  if (!videotrack) {
+    MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                        "mSendTrack is not video! This should never happen!");
+    MOZ_CRASH();
+    return NS_ERROR_FAILURE;
+  }
+
+  dom::MediaSourceEnum source = videotrack->GetSource().GetMediaSource();
+  webrtc::VideoCodecMode mode = webrtc::kRealtimeVideo;
+  switch (source) {
+    case dom::MediaSourceEnum::Browser:
+    case dom::MediaSourceEnum::Screen:
+    case dom::MediaSourceEnum::Application:
+    case dom::MediaSourceEnum::Window:
+      mode = webrtc::kScreensharing;
+      break;
+
+    case dom::MediaSourceEnum::Camera:
+    default:
+      mode = webrtc::kRealtimeVideo;
+      break;
+  }
+
+  auto error = aConduit.ConfigureCodecMode(mode);
+  if (error) {
+    MOZ_MTLOG(ML_ERROR, mPCHandle << "[" << mMid << "]: "
+                        "ConfigureCodecMode failed: " << error);
+    return NS_ERROR_FAILURE;
+  }
+
+  return NS_OK;
+}
+
+void
+TransceiverImpl::UpdateVideoExtmap(const JsepTrackNegotiatedDetails& aDetails,
+                                   bool aSending)
+{
+  std::vector<webrtc::RtpExtension> extmaps;
+  // @@NG read extmap from track
+  aDetails.ForEachRTPHeaderExtension(
+    [&extmaps](const SdpExtmapAttributeList::Extmap& extmap)
+  {
+    extmaps.emplace_back(extmap.extensionname,extmap.entry);
+  });
+
+  RefPtr<VideoSessionConduit> conduit = static_cast<VideoSessionConduit*>(
+      mConduit.get());
+
+  if (!extmaps.empty()) {
+    conduit->SetLocalRTPExtensions(aSending, extmaps);
+  }
+}
+
+static void StartTrack(MediaStream* aSource,
+                       nsAutoPtr<MediaSegment>&& aSegment)
+{
+  class Message : public ControlMessage {
+   public:
+    Message(MediaStream* aStream, nsAutoPtr<MediaSegment>&& aSegment)
+      : ControlMessage(aStream),
+        segment_(aSegment) {}
+
+    virtual void Run() override {
+      TrackRate track_rate = segment_->GetType() == MediaSegment::AUDIO ?
+        WEBRTC_DEFAULT_SAMPLE_RATE : mStream->GraphRate();
+      StreamTime current_end = mStream->GetTracksEnd();
+      MOZ_MTLOG(ML_DEBUG, "current_end = " << current_end);
+      TrackTicks current_ticks =
+        mStream->TimeToTicksRoundUp(track_rate, current_end);
+
+      // Add a track 'now' to avoid possible underrun, especially if we add
+      // a track "later".
+
+      if (current_end != 0L) {
+        MOZ_MTLOG(ML_DEBUG, "added track @ " << current_end << " -> "
+                  << mStream->StreamTimeToSeconds(current_end));
+      }
+
+      // To avoid assertions, we need to insert a dummy segment that covers up
+      // to the "start" time for the track
+      segment_->AppendNullData(current_ticks);
+      MOZ_MTLOG(ML_DEBUG, "segment_->GetDuration() = " << segment_->GetDuration());
+      if (segment_->GetType() == MediaSegment::AUDIO) {
+        MOZ_MTLOG(ML_DEBUG, "Calling AddAudioTrack");
+        mStream->AsSourceStream()->AddAudioTrack(
+            kAudioTrack,
+            WEBRTC_DEFAULT_SAMPLE_RATE,
+            0,
+            static_cast<AudioSegment*>(segment_.forget()));
+      } else {
+        mStream->AsSourceStream()->AddTrack(kVideoTrack, 0, segment_.forget());
+      }
+
+      mStream->AsSourceStream()->SetPullEnabled(true);
+      mStream->AsSourceStream()->AdvanceKnownTracksTime(STREAM_TIME_MAX);
+    }
+   private:
+    nsAutoPtr<MediaSegment> segment_;
+  };
+
+  aSource->GraphImpl()->AppendMessage(
+      MakeUnique<Message>(aSource, Move(aSegment)));
+  MOZ_MTLOG(ML_INFO, "Dispatched track-add on stream " << aSource);
+}
+
+void
+TransceiverImpl::StartReceiveStream()
+{
+  MOZ_MTLOG(ML_DEBUG, mPCHandle << "[" << mMid << "]: "
+                      "In StartReceiveStream");
+  // TODO: Can this be simplified? There's an awful lot of moving pieces here.
+  SourceMediaStream* source(mReceiveStream->GetInputStream()->AsSourceStream());
+  mReceiveStream->SetLogicalStreamStartTime(
+      mReceiveStream->GetPlaybackStream()->GetCurrentTime());
+
+  nsAutoPtr<MediaSegment> segment;
+  if (IsVideo()) {
+    segment = new VideoSegment;
+  } else {
+    segment = new AudioSegment;
+  }
+
+  StartTrack(source, Move(segment));
+}
+
+bool
+TransceiverImpl::IsVideo() const
+{
+  return mJsepTransceiver->mReceiving.GetMediaType() ==
+    SdpMediaSection::MediaType::kVideo;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
@@ -0,0 +1,145 @@
+/* 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/. */
+#ifndef _TRANSCEIVERIMPL_H_
+#define _TRANSCEIVERIMPL_H_
+
+#include <string>
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsIEventTarget.h"
+#include "nsTArray.h"
+#include "DOMMediaStream.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "ErrorList.h"
+#include "mtransport/transportflow.h"
+#include "signaling/src/jsep/JsepTrack.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+class PeerIdentity;
+class PeerConnectionMedia;
+class JsepTransceiver;
+class MediaSessionConduit;
+class VideoSessionConduit;
+class MediaPipelineReceive;
+class MediaPipelineTransmit;
+class MediaPipeline;
+class MediaPipelineFilter;
+class WebRtcCallWrapper;
+
+namespace dom {
+class RTCRtpTransceiver;
+}
+
+/**
+ * This is what ties all the various pieces that make up a transceiver
+ * together. This includes:
+ * DOMMediaStream, MediaStreamTrack, SourceMediaStream for rendering and capture
+ * TransportFlow for RTP transmission/reception
+ * Audio/VideoConduit for feeding RTP/RTCP into webrtc.org for decoding, and
+ * feeding audio/video frames into webrtc.org for encoding into RTP/RTCP.
+*/
+class TransceiverImpl : public nsISupports {
+public:
+  /**
+   * |aReceiveStream| is always set; this holds even if the remote end has not
+   * negotiated one for this transceiver. |aSendTrack| might or might not be
+   * set.
+   */
+  TransceiverImpl(const std::string& aPCHandle,
+                  RefPtr<JsepTransceiver> aJsepTransceiver,
+                  nsIEventTarget* aMainThread,
+                  nsIEventTarget* aStsThread,
+                  OwningNonNull<DOMMediaStream>& aReceiveStream,
+                  RefPtr<dom::MediaStreamTrack>& aSendTrack,
+                  RefPtr<WebRtcCallWrapper>& aCallWrapper);
+
+  nsresult UpdateSendTrack(dom::MediaStreamTrack* aSendTrack);
+
+  nsresult UpdateSinkIdentity(dom::MediaStreamTrack* aTrack,
+                              nsIPrincipal* aPrincipal,
+                              const PeerIdentity* aSinkIdentity);
+
+  nsresult UpdateTransport(PeerConnectionMedia& aTransportManager);
+
+  nsresult UpdateConduit();
+
+  nsresult UpdatePrincipal(nsIPrincipal* aPrincipal);
+
+  // TODO: We probably need to de-Sync when transceivers are stopped.
+  nsresult SyncWithMatchingVideoConduits(
+      std::vector<RefPtr<TransceiverImpl>>& transceivers);
+
+  void Shutdown_m();
+
+  bool ConduitHasPluginID(uint64_t aPluginID);
+
+  bool HasSendTrack(const dom::MediaStreamTrack* aSendTrack) const;
+
+  // This is so PCImpl can unregister from PrincipalChanged callbacks; maybe we
+  // should have TransceiverImpl handle these callbacks instead? It would need
+  // to be able to get a ref to PCImpl though.
+  RefPtr<dom::MediaStreamTrack> GetSendTrack()
+  {
+    return mSendTrack;
+  }
+
+  // for webidl
+  bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+                  JS::MutableHandle<JSObject*> aReflector);
+  already_AddRefed<dom::MediaStreamTrack> GetReceiveTrack();
+  void SyncWithJS(dom::RTCRtpTransceiver& aJsTransceiver, ErrorResult& aRv);
+
+  void InsertDTMFTone(int tone, uint32_t duration);
+
+  bool HasReceiveTrack(const dom::MediaStreamTrack* aReceiveTrack) const;
+
+  // TODO: These are for stats; try to find a cleaner way.
+  RefPtr<MediaPipeline> GetSendPipeline();
+
+  RefPtr<MediaPipeline> GetReceivePipeline();
+
+  void AddRIDExtension(unsigned short aExtensionId);
+
+  void AddRIDFilter(const nsAString& aRid);
+
+  bool IsVideo() const;
+
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+private:
+  virtual ~TransceiverImpl();
+  void InitAudio();
+  void InitVideo();
+  nsresult UpdateAudioConduit();
+  nsresult UpdateVideoConduit();
+  nsresult ConfigureVideoCodecMode(VideoSessionConduit& aConduit);
+  // This will eventually update audio extmap too
+  void UpdateVideoExtmap(const JsepTrackNegotiatedDetails& aDetails,
+                         bool aSending);
+  void StartReceiveStream();
+
+  const std::string mPCHandle;
+  RefPtr<JsepTransceiver> mJsepTransceiver;
+  std::string mMid;
+  bool mHaveStartedReceiving;
+  nsCOMPtr<nsIEventTarget> mMainThread;
+  nsCOMPtr<nsIEventTarget> mStsThread;
+  RefPtr<DOMMediaStream> mReceiveStream;
+  RefPtr<dom::MediaStreamTrack> mSendTrack;
+  // state for webrtc.org that is shared between all transceivers
+  RefPtr<WebRtcCallWrapper> mCallWrapper;
+  RefPtr<TransportFlow> mRtpFlow;
+  RefPtr<TransportFlow> mRtcpFlow;
+  RefPtr<MediaSessionConduit> mConduit;
+  RefPtr<MediaPipelineReceive> mReceivePipeline;
+  RefPtr<MediaPipelineTransmit> mTransmitPipeline;
+};
+
+} // namespace mozilla
+
+#endif // _TRANSCEIVERIMPL_H_
+
--- a/media/webrtc/signaling/src/sdp/SdpAttribute.h
+++ b/media/webrtc/signaling/src/sdp/SdpAttribute.h
@@ -249,16 +249,32 @@ operator|(SdpDirectionAttribute::Directi
 
 inline SdpDirectionAttribute::Direction
 operator&(SdpDirectionAttribute::Direction d1,
           SdpDirectionAttribute::Direction d2)
 {
   return (SdpDirectionAttribute::Direction)((unsigned)d1 & (unsigned)d2);
 }
 
+inline SdpDirectionAttribute::Direction
+operator|=(SdpDirectionAttribute::Direction& d1,
+           SdpDirectionAttribute::Direction d2)
+{
+  d1 = d1 | d2;
+  return d1;
+}
+
+inline SdpDirectionAttribute::Direction
+operator&=(SdpDirectionAttribute::Direction& d1,
+           SdpDirectionAttribute::Direction d2)
+{
+  d1 = d1 & d2;
+  return d1;
+}
+
 ///////////////////////////////////////////////////////////////////////////
 // a=dtls-message, draft-rescorla-dtls-in-sdp
 //-------------------------------------------------------------------------
 //   attribute               =/   dtls-message-attribute
 //
 //   dtls-message-attribute  =    "dtls-message" ":" role SP value
 //
 //   role                    =    "client" / "server"
@@ -533,17 +549,18 @@ public:
   RemoveMid(const std::string& mid)
   {
     for (auto i = mGroups.begin(); i != mGroups.end();) {
       auto tag = std::find(i->tags.begin(), i->tags.end(), mid);
       if (tag != i->tags.end()) {
         i->tags.erase(tag);
       }
 
-      if (i->tags.empty()) {
+      if (i->tags.empty() ||
+          ((i->tags.size() == 1) && (i->semantics == Semantics::kBundle))) {
         i = mGroups.erase(i);
       } else {
         ++i;
       }
     }
   }
 
   virtual void Serialize(std::ostream& os) const override;
--- a/media/webrtc/signaling/src/sdp/SdpHelper.cpp
+++ b/media/webrtc/signaling/src/sdp/SdpHelper.cpp
@@ -441,19 +441,19 @@ SdpHelper::SetDefaultAddresses(const std
           sdp::kInternet,
           ipVersion,
           defaultRtcpCandidateAddr));
   }
 }
 
 nsresult
 SdpHelper::GetIdsFromMsid(const Sdp& sdp,
-                                const SdpMediaSection& msection,
-                                std::string* streamId,
-                                std::string* trackId)
+                          const SdpMediaSection& msection,
+                          std::vector<std::string>* streamIds,
+                          std::string* trackId)
 {
   if (!sdp.GetAttributeList().HasAttribute(
         SdpAttribute::kMsidSemanticAttribute)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   auto& msidSemantics = sdp.GetAttributeList().GetMsidSemantic().mMsidSemantics;
   std::vector<SdpMsidAttributeList::Msid> allMsids;
@@ -481,24 +481,26 @@ SdpHelper::GetIdsFromMsid(const Sdp& sdp
   for (auto i = allMsids.begin(); i != allMsids.end(); ++i) {
     if (allMsidsAreWebrtc || webrtcMsids.count(i->identifier)) {
       if (i->appdata.empty()) {
         SDP_SET_ERROR("Invalid webrtc msid at level " << msection.GetLevel()
                        << ": Missing track id.");
         return NS_ERROR_INVALID_ARG;
       }
       if (!found) {
-        *streamId = i->identifier;
         *trackId = i->appdata;
         found = true;
-      } else if ((*streamId != i->identifier) || (*trackId != i->appdata)) {
-        MOZ_MTLOG(ML_WARNING, "Found multiple different webrtc msids in "
-                       "m-section " << msection.GetLevel() << ". The "
-                       "behavior w/o transceivers is undefined.");
+      } else if ((*trackId != i->appdata)) {
+        SDP_SET_ERROR("Found multiple different webrtc track ids in m-section "
+                       << msection.GetLevel() << ". The behavior here is "
+                       "undefined.");
+        return NS_ERROR_INVALID_ARG;
       }
+      streamIds->clear();
+      streamIds->push_back(i->identifier);
     }
   }
 
   if (!found) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return NS_OK;
--- a/media/webrtc/signaling/src/sdp/SdpHelper.h
+++ b/media/webrtc/signaling/src/sdp/SdpHelper.h
@@ -58,17 +58,17 @@ class SdpHelper {
         const Sdp& sdp,
         std::vector<SdpGroupAttributeList::Group>* groups) const;
 
     nsresult GetMidFromLevel(const Sdp& sdp,
                              uint16_t level,
                              std::string* mid);
     nsresult GetIdsFromMsid(const Sdp& sdp,
                             const SdpMediaSection& msection,
-                            std::string* streamId,
+                            std::vector<std::string>* streamId,
                             std::string* trackId);
     nsresult GetMsids(const SdpMediaSection& msection,
                       std::vector<SdpMsidAttributeList::Msid>* msids);
     nsresult ParseMsid(const std::string& msidAttribute,
                        std::string* streamId,
                        std::string* trackId);
     nsresult AddCandidateToSdp(Sdp* sdp,
                                const std::string& candidate,
--- a/media/webrtc/signaling/src/sdp/SdpMediaSection.h
+++ b/media/webrtc/signaling/src/sdp/SdpMediaSection.h
@@ -103,60 +103,65 @@ public:
   GetLevel() const
   {
     return mLevel;
   }
 
   inline bool
   IsReceiving() const
   {
-    return GetDirectionAttribute().mValue & sdp::kRecv;
+    return GetDirection() & sdp::kRecv;
   }
 
   inline bool
   IsSending() const
   {
-    return GetDirectionAttribute().mValue & sdp::kSend;
+    return GetDirection() & sdp::kSend;
   }
 
   inline void
   SetReceiving(bool receiving)
   {
-    auto direction = GetDirectionAttribute().mValue;
+    auto direction = GetDirection();
     if (direction & sdp::kSend) {
       SetDirection(receiving ?
                    SdpDirectionAttribute::kSendrecv :
                    SdpDirectionAttribute::kSendonly);
     } else {
       SetDirection(receiving ?
                    SdpDirectionAttribute::kRecvonly :
                    SdpDirectionAttribute::kInactive);
     }
   }
 
   inline void
   SetSending(bool sending)
   {
-    auto direction = GetDirectionAttribute().mValue;
+    auto direction = GetDirection();
     if (direction & sdp::kRecv) {
       SetDirection(sending ?
                    SdpDirectionAttribute::kSendrecv :
                    SdpDirectionAttribute::kRecvonly);
     } else {
       SetDirection(sending ?
                    SdpDirectionAttribute::kSendonly :
                    SdpDirectionAttribute::kInactive);
     }
   }
 
   inline void SetDirection(SdpDirectionAttribute::Direction direction)
   {
     GetAttributeList().SetAttribute(new SdpDirectionAttribute(direction));
   }
 
+  inline SdpDirectionAttribute::Direction GetDirection() const
+  {
+    return GetDirectionAttribute().mValue;
+  }
+
   const SdpFmtpAttributeList::Parameters* FindFmtp(const std::string& pt) const;
   void SetFmtp(const SdpFmtpAttributeList::Fmtp& fmtp);
   void RemoveFmtp(const std::string& pt);
   const SdpRtpmapAttributeList::Rtpmap* FindRtpmap(const std::string& pt) const;
   const SdpSctpmapAttributeList::Sctpmap* GetSctpmap() const;
   uint32_t GetSctpPort() const;
   bool GetMaxMessageSize(uint32_t* size) const;
   bool HasRtcpFb(const std::string& pt,