Bug 1290948 - Part 7: Update existing mochitests to track the spec fixes in previous parts. r+drno r=drno
☠☠ backed out by e077a6e6e842 ☠ ☠
authorByron Campen [:bwc] <docfaraday@gmail.com>
Wed, 23 Aug 2017 16:16:29 -0500
changeset 443519 8ff38e646037f6857357efe95370b725d070fdf4
parent 443518 314675023cd5dd745f7f1b9cccb537b762b495f2
child 443520 97a271bf671eb1e8adb9378ee4052e0d93a40a80
push id8527
push userCallek@gmail.com
push dateThu, 11 Jan 2018 21:05:50 +0000
treeherdermozilla-beta@95342d212a7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdrno
bugs1290948
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1290948 - Part 7: Update existing mochitests to track the spec fixes in previous parts. r+drno r=drno MozReview-Commit-ID: 95YyFm3cRB6
dom/media/tests/mochitest/head.js
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_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
--- a/dom/media/tests/mochitest/head.js
+++ b/dom/media/tests/mochitest/head.js
@@ -314,16 +314,17 @@ function setupEnvironment() {
 
   var defaultMochitestPrefs = {
     'set': [
       ['media.peerconnection.enabled', true],
       ['media.peerconnection.identity.enabled', true],
       ['media.peerconnection.identity.timeout', 120000],
       ['media.peerconnection.ice.stun_client_maximum_transmits', 14],
       ['media.peerconnection.ice.trickle_grace_period', 30000],
+      ['media.peerconnection.remoteTrackId.enabled', true],
       ['media.navigator.permission.disabled', true],
       ['media.navigator.streams.fake', FAKE_ENABLED],
       ['media.getusermedia.screensharing.enabled', true],
       ['media.getusermedia.screensharing.allowed_domains', "mochi.test"],
       ['media.getusermedia.audiocapture.enabled', true],
       ['media.recorder.audio_node.enabled', true]
     ]
   };
--- a/dom/media/tests/mochitest/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;
@@ -870,36 +872,70 @@ PeerConnectionWrapper.prototype = {
   get iceConnectionState() {
     return this._pc.iceConnectionState;
   },
 
   setIdentityProvider: function(provider, protocol, identity) {
     this._pc.setIdentityProvider(provider, protocol, identity);
   },
 
+  elementPrefix : direction =>
+  {
+    return [this.label, direction].join('_');
+  },
+
+  getMediaElementForTrack : function (track, direction)
+  {
+    var prefix = this.elementPrefix(direction);
+    return getMediaElementForTrack(track, prefix);
+  },
+
+  createMediaElementForTrack : function(track, direction)
+  {
+    var prefix = this.elementPrefix(direction);
+    return createMediaElementForTrack(track, prefix);
+  },
+
   ensureMediaElement : function(track, direction) {
-    const idPrefix = [this.label, direction].join('_');
-    var element = getMediaElementForTrack(track, idPrefix);
-
+    var prefix = this.elementPrefix(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
@@ -916,110 +952,147 @@ PeerConnectionWrapper.prototype = {
 
     ok(track.id, "track has id");
     ok(track.kind, "track has kind");
     ok(stream.id, "stream has id");
     this.expectedLocalTrackInfoById[track.id] = {
       type: track.kind,
       streamId: stream.id,
     };
+    this.expectedSignalledTrackInfoById[track.id] =
+      this.expectedLocalTrackInfoById[track.id];
+
+    this.addSendStream(stream);
 
     // This will create one media element per track, which might not be how
     // we set up things with the RTCPeerConnection. It's the only way
     // we can ensure all sent tracks are flowing however.
     this.ensureMediaElement(track, "local");
 
     return this.observedNegotiationNeeded;
   },
 
   /**
    * Callback when we get local media. Also an appropriate HTML media element
    * will be created and added to the content node.
    *
    * @param {MediaStream} stream
    *        Media stream to handle
    */
-  attachLocalStream : function(stream) {
+  attachLocalStream : function(stream, useAddTransceiver) {
     info("Got local media stream: (" + stream.id + ")");
 
     this.expectNegotiationNeeded();
+    if (useAddTransceiver) {
+      info("Using addTransceiver (on PC).");
+      stream.getTracks().forEach(track => {
+        var transceiver = this._pc.addTransceiver(track, {streams: [stream]});
+        is(transceiver.sender.track, track, "addTransceiver returns sender");
+      });
+    }
     // In order to test both the addStream and addTrack APIs, we do half one
     // way, half the other, at random.
-    if (Math.random() < 0.5) {
+    else if (Math.random() < 0.5) {
       info("Using addStream.");
       this._pc.addStream(stream);
       ok(this._pc.getSenders().find(sender => sender.track == stream.getTracks()[0]),
          "addStream returns sender");
     } else {
       info("Using addTrack (on PC).");
       stream.getTracks().forEach(track => {
         var sender = this._pc.addTrack(track, stream);
         is(sender.track, track, "addTrack returns sender");
       });
     }
 
+    this.addSendStream(stream);
+
     stream.getTracks().forEach(track => {
       ok(track.id, "track has id");
       ok(track.kind, "track has kind");
       this.expectedLocalTrackInfoById[track.id] = {
           type: track.kind,
           streamId: stream.id
         };
+      this.expectedSignalledTrackInfoById[track.id] =
+        this.expectedLocalTrackInfoById[track.id];
       this.ensureMediaElement(track, "local");
     });
+
+    return this.observedNegotiationNeeded;
   },
 
   removeSender : function(index) {
     var sender = this._pc.getSenders()[index];
     delete this.expectedLocalTrackInfoById[sender.track.id];
     this.expectNegotiationNeeded();
     this._pc.removeTrack(sender);
     return this.observedNegotiationNeeded;
   },
 
-  senderReplaceTrack : function(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))
+    );
+  },
+
+  getAllUserMediaAndAddStreams : async function(constraintsList) {
+    var streams = await this.getAllUserMedia(constraintsList);
+    if (!streams) {
+      return;
+    }
+    return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
+  },
+
+  getAllUserMediaAndAddTransceivers : async function(constraintsList) {
+    var streams = await this.getAllUserMedia(constraintsList);
+    if (!streams) {
+      return;
+    }
+    return Promise.all(streams.map(stream => this.attachLocalStream(stream, true)));
   },
 
   /**
    * 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 => {
@@ -1159,44 +1232,65 @@ 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]);
   },
 
+  getWebrtcTrackId: function(receiveTrack) {
+    let matchingTransceiver = this._pc.getTransceivers().find(
+        transceiver => transceiver.receiver.track == receiveTrack);
+    if (!matchingTransceiver) {
+      return null;
+    }
+
+    return matchingTransceiver.remoteTrackId;
+  },
+
   setupTrackEventHandler: function() {
     this._pc.addEventListener('track', event => {
-      info(this + ": 'ontrack' event fired for " + JSON.stringify(event.track));
+      info(this + ": 'ontrack' event fired for " + event.track.id +
+                  "(SDP msid is " + this.getWebrtcTrackId(event.track) +
+                  ")");
 
-      this.checkTrackIsExpected(event.track,
-                                this.expectedRemoteTrackInfoById,
-                                this.observedRemoteTrackInfoById);
+      // TODO(bug 1403238): Checking for remote tracks needs to be completely
+      // reworked, because with the latest spec the identifiers aren't the same
+      // as they are on the other end. Ultimately, what we need to check is
+      // whether the _transceivers_ are in line with what is expected, and
+      // whether the callbacks are consistent with the transceivers.
+      let trackId = this.getWebrtcTrackId(event.track);
+      ok(!this.observedRemoteTrackInfoById[trackId],
+         "track id " + trackId + " was not yet observed");
+      this.observedRemoteTrackInfoById[trackId] = {
+        type: event.track.kind
+      };
       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
@@ -1319,53 +1413,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() {
@@ -1456,32 +1544,66 @@ PeerConnectionWrapper.prototype = {
         return stats;
       }
       await wait(retryInterval);
     }
     throw new Error("Timeout checking for stats for track " + track.id
                     + " after at least" + timeout + "ms");
   },
 
+  getExpectedActiveReceiveTracks : function() {
+    return this._pc.getTransceivers()
+      .filter(t => {
+        return !t.stopped &&
+               t.currentDirection &&
+               (t.currentDirection != "inactive") &&
+               (t.currentDirection != "sendonly");
+      })
+      .map(t => {
+        info("Found transceiver that should be receiving RTP: mid=" + t.mid +
+             " currentDirection=" + t.currentDirection + " kind=" +
+             t.receiver.track.kind + " track-id=" + t.receiver.track.id);
+        return t.receiver.track;
+      });
+  },
+
+  getExpectedSendTracks : function() {
+    return Object.keys(this.expectedLocalTrackInfoById)
+              .map(id => this.findSendTrackByWebrtcId(id));
+  },
+
+  findReceiveTrackByWebrtcId : function(webrtcId) {
+    return this._pc.getReceivers().map(receiver => receiver.track)
+              .find(track => this.getWebrtcTrackId(track) == webrtcId);
+  },
+
+  // Send tracks use the same identifiers that go in the signaling
+  findSendTrackByWebrtcId : function(webrtcId) {
+    return this._pc.getSenders().map(sender => sender.track)
+              .filter(track => track) // strip out null
+              .find(track => track.id == webrtcId);
+  },
+
   /**
    * Wait for presence of video flow on all media elements and rtp flow on
    * all sending and receiving track involved in this test.
    *
    * @returns {Promise}
    *        A promise that resolves when media flows for all elements and tracks
    */
   waitForMediaFlow : function() {
     return Promise.all([].concat(
       this.localMediaElements.map(element => this.waitForMediaElementFlow(element)),
-      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) {
@@ -1517,69 +1639,100 @@ 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 : async function(audiocontext, from,
       cancel = wait(60000, new Error("Tone not detected"))) {
-    let inputElem = from.localMediaElements[0];
+    let localTransceivers = this._pc.getTransceivers()
+      .filter(t => t.mid)
+      .filter(t => t.receiver.track.kind == "audio")
+      .sort((t1, t2) => t1.mid < t2.mid);
+    let remoteTransceivers = from._pc.getTransceivers()
+      .filter(t => t.mid)
+      .filter(t => t.receiver.track.kind == "audio")
+      .sort((t1, t2) => t1.mid < t2.mid);
 
-    // As input we use the stream of |from|'s first available audio sender.
-    let inputSenderTracks = from._pc.getSenders().map(sn => sn.track);
-    let inputAudioStream = from._pc.getLocalStreams()
-      .find(s => inputSenderTracks.some(t => t.kind == "audio" && s.getTrackById(t.id)));
-    let inputAnalyser = new AudioStreamAnalyser(audiocontext, inputAudioStream);
+    is(localTransceivers.length, remoteTransceivers.length,
+       "Same number of associated audio transceivers on remote and local.");
 
-    // It would have been nice to have a working getReceivers() here, but until
-    // we do, let's use what remote streams we have.
-    let outputAudioStream = this._pc.getRemoteStreams()
-      .find(s => s.getAudioTracks().length > 0);
-    let outputAnalyser = new AudioStreamAnalyser(audiocontext, outputAudioStream);
+    for (let i = 0; i < localTransceivers.length; i++) {
+      is(localTransceivers[i].mid, remoteTransceivers[i].mid,
+         "Transceivers at index " + i + " have the same mid.");
 
-    let error = null;
-    cancel.then(e => error = e);
+      if (!remoteTransceivers[i].sender.track) {
+        continue;
+      }
 
-    let indexOfMax = data => 
-      data.reduce((max, val, i) => (val >= data[max]) ? i : max, 0);
-
-    await outputAnalyser.waitForAnalysisSuccess(() => {
-      if (error) {
-        throw error;
+      if (remoteTransceivers[i].currentDirection == "recvonly" ||
+          remoteTransceivers[i].currentDirection == "inactive") {
+        continue;
       }
 
-      let inputData = inputAnalyser.getByteFrequencyData();
-      let outputData = outputAnalyser.getByteFrequencyData();
+      let sendTrack = remoteTransceivers[i].sender.track;
+      let inputElem = from.getMediaElementForTrack(sendTrack, "local");
+      ok(inputElem,
+         "Remote wrapper should have a media element for track id " +
+         sendTrack.id);
+      let inputAudioStream = from.getStreamForSendTrack(sendTrack);
+      ok(inputAudioStream,
+         "Remote wrapper should have a stream for track id " + sendTrack.id);
+      let inputAnalyser =
+        new AudioStreamAnalyser(audiocontext, inputAudioStream);
+
+      let recvTrack = localTransceivers[i].receiver.track;
+      let outputAudioStream = this.getStreamForRecvTrack(recvTrack);
+      ok(outputAudioStream,
+         "Local wrapper should have a stream for track id " + recvTrack.id);
+      let outputAnalyser =
+        new AudioStreamAnalyser(audiocontext, outputAudioStream);
+
+      let error = null;
+      cancel.then(e => error = e);
+
+      let indexOfMax = data =>
+        data.reduce((max, val, i) => (val >= data[max]) ? i : max, 0);
 
-      let inputMax = indexOfMax(inputData);
-      let outputMax = indexOfMax(outputData);
-      info(`Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},`
-        + ` output[${outputMax}] = ${outputData[outputMax]}`);
-      if (!inputData[inputMax] || !outputData[outputMax]) {
-        return false;
-      }
+      await outputAnalyser.waitForAnalysisSuccess(() => {
+        if (error) {
+          throw error;
+        }
+
+        let inputData = inputAnalyser.getByteFrequencyData();
+        let outputData = outputAnalyser.getByteFrequencyData();
 
-      // When the input and output maxima are within reasonable distance (2% of
-      // total length, which means ~10 for length 512) from each other, we can
-      // be sure that the input tone has made it through the peer connection.
-      info(`input data length: ${inputData.length}`);
-      return Math.abs(inputMax - outputMax) < (inputData.length * 0.02);
-    });
+        let inputMax = indexOfMax(inputData);
+        let outputMax = indexOfMax(outputData);
+        info(`Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},`
+          + ` output[${outputMax}] = ${outputData[outputMax]}`);
+        if (!inputData[inputMax] || !outputData[outputMax]) {
+          return false;
+        }
+
+        // When the input and output maxima are within reasonable distance (2% of
+        // total length, which means ~10 for length 512) from each other, we can
+        // be sure that the input tone has made it through the peer connection.
+        info(`input data length: ${inputData.length}`);
+        return Math.abs(inputMax - outputMax) < (inputData.length * 0.02);
+      });
+    }
   },
 
   /**
    * Get stats from the "legacy" getStats callback interface
    */
   getStatsLegacy : function(selector, onSuccess, onFail) {
     let wrapper = stats => {
       info(this + ": Got legacy stats: " + JSON.stringify(stats));
@@ -1617,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
@@ -1650,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");
@@ -1729,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)");
@@ -1805,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.getAllUserMediaAndAddStreams(test.pcLocal.constraints);
   },
 
   function PC_REMOTE_GUM(test) {
-    return test.pcRemote.getAllUserMedia(test.pcRemote.constraints);
+    return test.pcRemote.getAllUserMediaAndAddStreams(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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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.getWebrtcTrackId(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.getAllUserMediaAndAddStreams([{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.getWebrtcTrackId(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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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.getAllUserMediaAndAddStreams([{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);
         },
+        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.getAllUserMediaAndAddStreams([{video: true, fake: true}]);
+        },
+        function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+          test.pcLocal.expectIceChecking();
+        },
+        function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+          test.pcRemote.expectIceChecking();
         },
       ],
       [
         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);
         },
+        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,61 @@
         // (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");
-        }
+        let videoTransceivers = test.pcLocal._pc.getTransceivers()
+          .filter(transceiver => {
+            return !transceiver.stopped &&
+                   transceiver.receiver.track.kind == "video" &&
+                   transceiver.sender.track;
+          });
+
+        ok(videoTransceivers.length,
+           "There is at least one non-stopped video transceiver with a track.");
+
+        videoTransceivers.forEach(transceiver => {
+            var stream = test.pcLocal._pc.getLocalStreams()[0];;
+            var track = transceiver.sender.track;
+            try {
+              test.pcLocal._pc.addTrack(track, stream);
+              ok(false, "addTrack existing track should fail");
+            } catch (e) {
+              is(e.name, "InvalidAccessError",
+                 "addTrack existing track should fail");
+            }
+          });
       }
     ]);
     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))
-            .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);
-        },
+        }
       ]
     );
 
     test.run();
    });
   });
 
 </script>
--- a/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
+++ b/dom/media/tests/mochitest/test_peerConnection_scaleResolution.html
@@ -13,73 +13,73 @@
   });
 
   const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
 
   var mustRejectWith = (msg, reason, f) =>
     f().then(() => ok(false, msg),
              e => is(e.name, reason, msg));
 
-  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
-        : (d.sdp = sdputils.removeAllButPayloadType(d.sdp, 126), d)))
-      .then(() => pc2.setRemoteDescription(pc1.localDescription))
-      .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(d))
-      .then(() => pc1.setRemoteDescription(pc2.localDescription))
-      .catch(generateErrorCallback());
+    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();
+    if (codec == "VP8") {
+      offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126);
+    }
+    await pc1.setLocalDescription(offer);
+    await pc2.setRemoteDescription(pc1.localDescription);
+
+    let answer = await pc2.createAnswer();
+    await pc2.setLocalDescription(answer);
+    await pc1.setRemoteDescription(pc2.localDescription);
+    let trackevent = await ontrackfired;
+
+    v2.srcObject = trackevent.streams[0];
+
+    await v2loadedmetadata;
+
+    await waitUntil(() => v2.currentTime > 0 && v2.srcObject.currentTime > 0);
+    ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
+
+    ok(v1.videoWidth > 0, "source width is positive");
+    ok(v1.videoHeight > 0, "source height is positive");
+    is(v2.videoWidth, v1.videoWidth / 2, "sink is half the width of source");
+    is(v2.videoHeight, v1.videoHeight / 2, "sink is half the height of source");
+    stream.getTracks().forEach(track => track.stop());
+    v1.srcObject = v2.srcObject = null;
   }
 
   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]));
     };
 
--- 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.getAllUserMediaAndAddStreams([{audio: true}]);
         },
       ]
     );
 
     test.chain.append([
       function CHECK_ASSUMPTIONS2() {
         is(test.pcLocal.localMediaElements.length, 2,
            "pcLocal should have two media elements");
--- a/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
+++ b/dom/media/tests/mochitest/test_peerConnection_verifyVideoAfterRenegotiation.html
@@ -73,17 +73,17 @@ runNetworkTest(() => {
 
   addRenegotiation(test.chain,
     [
       function PC_LOCAL_ADD_SECOND_STREAM(test) {
         canvas2 = h2.createAndAppendElement('canvas', 'source_canvas2');
         h2.drawColor(canvas2, h2.blue);
         stream2 = canvas2.captureStream(0);
 
-        // can't use test.pcLocal.getAllUserMedia([{video: true}]);
+        // can't use test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
         // because it doesn't let us substitute the capture stream
         test.pcLocal.attachLocalStream(stream2);
       }
     ]
   );
 
   test.chain.append([
     function FIND_REMOTE2_VIDEO() {