Bug 1290948 - Part 7: Update existing mochitests to track the spec fixes in previous parts. r+drno draft
authorByron Campen [:bwc] <docfaraday@gmail.com>
Wed, 23 Aug 2017 16:16:29 -0500
changeset 704687 605e7742c7b187c254e76af6723ef0fe554ae032
parent 704686 702fce28d73d91156c91003efc4a2c9f5b1ab5bd
child 704688 9f100b623b64ae55b4f33ddfe0601f39678a04b3
push id91202
push userbcampen@mozilla.com
push dateTue, 28 Nov 2017 19:52:00 +0000
bugs1290948
milestone59.0a1
Bug 1290948 - Part 7: Update existing mochitests to track the spec fixes in previous parts. r+drno MozReview-Commit-ID: 95YyFm3cRB6
dom/media/tests/mochitest/head.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/pc.js
dom/media/tests/mochitest/templates.js
dom/media/tests/mochitest/test_peerConnection_addSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_addSecondAudioStreamNoBundle.html
dom/media/tests/mochitest/test_peerConnection_addSecondVideoStream.html
dom/media/tests/mochitest/test_peerConnection_addSecondVideoStreamNoBundle.html
dom/media/tests/mochitest/test_peerConnection_addtrack_removetrack_events.html
dom/media/tests/mochitest/test_peerConnection_answererAddSecondAudioStream.html
dom/media/tests/mochitest/test_peerConnection_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.audiocapture.enabled', true],
       ['media.recorder.audio_node.enabled', true]
     ]
   };
 
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -94,16 +94,17 @@ skip-if = toolkit == 'android' # no scre
 [test_getUserMedia_trackEnded.html]
 [test_getUserMedia_peerIdentity.html]
 [test_peerConnection_addIceCandidate.html]
 [test_peerConnection_addtrack_removetrack_events.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_audioCodecs.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_transceivers.html]
+skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicAudio.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_checkPacketDumpHook.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_basicAudioNATSrflx.html]
 skip-if = toolkit == 'android' || (os == 'linux' && (debug || asan)) # websockets don't work on android (bug 1266217), linux hang (bug 1339568)
 [test_peerConnection_basicAudioNATRelay.html]
 skip-if = toolkit == 'android' || (os == 'linux' && (debug || asan)) # websockets don't work on android (bug 1266217), linux hang (bug 1339568)
--- 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.getRemoteTrackId();
+  },
+
   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() {