Bug 1290948 - Part 3: Implement JS part of RTCRtpTransceiver. r?jib draft
authorByron Campen [:bwc] <docfaraday@gmail.com>
Wed, 23 Aug 2017 15:51:21 -0500
changeset 704683 20716b2399b73fc918bc2336c72b33bdbb03f30e
parent 703685 dfadc9b2fb87ce1bd3922d83b7c16dcc690fdcd1
child 704684 a0eb8835b36d902ca7b5732f7ed2fd8e624cf35c
push id91202
push userbcampen@mozilla.com
push dateTue, 28 Nov 2017 19:52:00 +0000
reviewersjib
bugs1290948
milestone59.0a1
Bug 1290948 - Part 3: Implement JS part of RTCRtpTransceiver. r?jib MozReview-Commit-ID: FOZSqKbqHtC
dom/media/PeerConnection.js
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -20,28 +20,30 @@ const PC_CONTRACT = "@mozilla.org/dom/pe
 const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
 const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1";
 const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
 const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
 const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1";
 const PC_RECEIVER_CONTRACT = "@mozilla.org/dom/rtpreceiver;1";
+const PC_TRANSCEIVER_CONTRACT = "@mozilla.org/dom/rtptransceiver;1";
 const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
 const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1";
 
 const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
 const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
 const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
 const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
 const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
 const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}");
 const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
 const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}");
 const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}");
+const PC_TRANSCEIVER_CID = Components.ID("{09475754-103a-41f5-a2d0-e1f27eb0b537}");
 const PC_COREQUEST_CID = Components.ID("{74b2122d-65a8-4824-aa9e-3d664cb75dc2}");
 const PC_DTMF_SENDER_CID = Components.ID("{3610C242-654E-11E6-8EC0-6D1BE389A607}");
 
 function logMsg(msg, file, line, flag, winID) {
   let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
   let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
   scriptError.initWithWindowID(msg, file, null, line, 0, flag,
                                "content javascript", winID);
@@ -143,19 +145,18 @@ class GlobalPCList {
       this.handleGMPCrash(data);
     }
   }
 
   observe(subject, topic, data) {
     let cleanupPcRef = function(pcref) {
       let pc = pcref.get();
       if (pc) {
-        pc._pc.close();
-        delete pc._observer;
-        pc._pc = null;
+        pc._suppressEvents = true;
+        pc.close();
       }
     };
 
     let cleanupWinId = function(list, winID) {
       if (list.hasOwnProperty(winID)) {
         list[winID].forEach(cleanupPcRef);
         delete list[winID];
       }
@@ -342,18 +343,18 @@ setupPrototype(RTCStatsReport, {
         "candidate-pair": "candidatepair",
         "local-candidate": "localcandidate",
         "remote-candidate": "remotecandidate"
   }
 });
 
 class RTCPeerConnection {
   constructor() {
-    this._senders = [];
-    this._receivers = [];
+    this._receiveStreams = new Map();
+    this._transceivers = [];
 
     this._pc = null;
     this._closed = false;
 
     this._localType = null;
     this._remoteType = null;
     // http://rtcweb-wg.github.io/jsep/#rfc.section.4.1.9
     // canTrickle == null means unknown; when a remote description is received it
@@ -584,16 +585,28 @@ class RTCPeerConnection {
 
     try {
       wrapCallback(onSucc)(await func());
     } catch (e) {
       wrapCallback(onErr)(e);
     }
   }
 
+  // This implements the fairly common "Queue a task" logic
+  async _queueTaskWithClosedCheck(func) {
+    return new this._win.Promise(resolve => {
+      Services.tm.dispatchToMainThread({ run() {
+        if (!this._closed) {
+          func();
+          resolve();
+        }
+      }});
+    });
+  }
+
   /**
    * An RTCConfiguration may look like this:
    *
    * { "iceServers": [ { urls: "stun:stun.example.org", },
    *                   { url: "stun:stun.example.org", }, // deprecated version
    *                   { urls: ["turn:turn1.x.org", "turn:turn2.x.org"],
    *                     username:"jib", credential:"mypass"} ] }
    *
@@ -685,17 +698,17 @@ class RTCPeerConnection {
       throw new this._win.DOMException("Peer connection is closed",
                                        "InvalidStateError");
     }
   }
 
   dispatchEvent(event) {
     // PC can close while events are firing if there is an async dispatch
     // in c++ land. But let through "closed" signaling and ice connection events.
-    if (!this._closed || this._inClose) {
+    if (!this._suppressEvents) {
       this.__DOM_IMPL__.dispatchEvent(event);
     }
   }
 
   // Log error message to web console and window.onerror, if present.
   logErrorAndCallOnError(e) {
     this.logMsg(e.message, e.fileName, e.lineNumber, Ci.nsIScriptError.exceptionFlag);
 
@@ -752,21 +765,70 @@ class RTCPeerConnection {
                             set(h) {
                               this.logWarning(name + " is deprecated! " + msg);
                               return this.setEH(name, h);
                             }
                           });
   }
 
   createOffer(optionsOrOnSucc, onErr, options) {
-    // This entry-point handles both new and legacy call sig. Decipher which one
+    let onSuccess = null;
     if (typeof optionsOrOnSucc == "function") {
-      return this._legacy(optionsOrOnSucc, onErr, () => this._createOffer(options));
+      onSuccess = optionsOrOnSucc;
+    } else {
+      options = optionsOrOnSucc;
+    }
+
+    // Spec language implies that this needs to happen as if it were called
+    // before createOffer, so we do this as early as possible.
+    this._ensureTransceiversForOfferToReceive(options);
+
+    // This entry-point handles both new and legacy call sig. Decipher which one
+    if (onSuccess) {
+      return this._legacy(onSuccess, onErr, () => this._createOffer(options));
     }
-    return this._async(() => this._createOffer(optionsOrOnSucc));
+
+    return this._async(() => this._createOffer(options));
+  }
+
+  // Ensures that we have at least one transceiver of |kind| that is
+  // configured to receive. It will create one if necessary.
+  _ensureOfferToReceive(kind) {
+    let hasRecv = this._transceivers.some(
+      transceiver =>
+        transceiver.getKind() == kind &&
+        (transceiver.direction == "sendrecv" || transceiver.direction == "recvonly") &&
+        !transceiver.stopped);
+
+    if (!hasRecv) {
+      this._addTransceiverNoEvents(kind, {direction: "recvonly"});
+    }
+  }
+
+  // Handles offerToReceiveAudio/Video
+  _ensureTransceiversForOfferToReceive(options) {
+    if (options.offerToReceiveVideo) {
+      this._ensureOfferToReceive("video");
+    }
+
+    if (options.offerToReceiveVideo === false) {
+      this.logWarning("offerToReceiveVideo: false is ignored now. If you " +
+                      "want to disallow a recv track, use " +
+                      "RTCRtpTransceiver.direction");
+    }
+
+    if (options.offerToReceiveAudio) {
+      this._ensureOfferToReceive("audio");
+    }
+
+    if (options.offerToReceiveAudio === false) {
+      this.logWarning("offerToReceiveAudio: false is ignored now. If you " +
+                      "want to disallow a recv track, use " +
+                      "RTCRtpTransceiver.direction");
+    }
   }
 
   async _createOffer(options) {
     this._checkClosed();
     let origin = Cu.getWebIDLCallerPrincipal().origin;
     return this._chain(async () => {
       let haveAssertion;
       if (this._localIdp.enabled) {
@@ -1061,118 +1123,209 @@ class RTCPeerConnection {
     stream.getTracks().forEach(track => this.addTrack(track, stream));
   }
 
   addTrack(track, stream) {
     if (stream.currentTime === undefined) {
       throw new this._win.DOMException("invalid stream.", "InvalidParameterError");
     }
     this._checkClosed();
-    this._senders.forEach(sender => {
-      if (sender.track == track) {
-        throw new this._win.DOMException("already added.",
-                                         "InvalidParameterError");
-      }
+
+    if (this._transceivers.some(
+          transceiver => transceiver.sender.track == track)) {
+      throw new this._win.DOMException("This track is already set on a sender.",
+                                       "InvalidAccessError");
+    }
+
+    let transceiver = this._transceivers.find(transceiver => {
+      return transceiver.sender.track == null &&
+             transceiver.getKind() == track.kind &&
+             !transceiver.stopped &&
+             !transceiver.hasBeenUsedToSend();
     });
-    this._impl.addTrack(track, stream);
-    let sender = this._win.RTCRtpSender._create(this._win,
-                                                new RTCRtpSender(this, track,
-                                                                 stream));
-    this._senders.push(sender);
-    return sender;
+
+    if (transceiver) {
+      transceiver.sender.setTrack(track);
+      transceiver.sender.setStreams([stream]);
+      if (transceiver.direction == "recvonly") {
+        transceiver.setDirectionInternal("sendrecv");
+      } else if (transceiver.direction == "inactive") {
+        transceiver.setDirectionInternal("sendonly");
+      }
+    } else {
+      transceiver = this._addTransceiverNoEvents(track, {
+        streams: [stream],
+        direction: "sendrecv"
+      });
+    }
+
+    transceiver.setAddTrackMagic();
+    transceiver.sync();
+    this.updateNegotiationNeeded();
+    return transceiver.sender;
   }
 
   removeTrack(sender) {
     this._checkClosed();
-    var i = this._senders.indexOf(sender);
-    if (i >= 0) {
-      this._senders.splice(i, 1);
-      this._impl.removeTrack(sender.track); // fires negotiation needed
+
+    sender.checkWasCreatedByPc(this.__DOM_IMPL__);
+
+    let transceiver =
+      this._transceivers.find(transceiver => transceiver.sender == sender);
+
+    // If the transceiver was removed due to rollback, let it slide.
+    if (!transceiver || !sender.track) {
+      return;
+    }
+
+    // TODO(bug 1401983): Move to TransceiverImpl?
+    this._impl.removeTrack(sender.track);
+
+    sender.setTrack(null);
+    if (transceiver.direction == "sendrecv") {
+      transceiver.setDirectionInternal("recvonly");
+    } else if (transceiver.direction == "sendonly") {
+      transceiver.setDirectionInternal("inactive");
     }
+
+    transceiver.sync();
+    this.updateNegotiationNeeded();
+  }
+
+  _addTransceiverNoEvents(sendTrackOrKind, init) {
+    let sendTrack = null;
+    let kind;
+    if (typeof(sendTrackOrKind) == "string") {
+      kind = sendTrackOrKind;
+      switch (kind) {
+        case "audio":
+        case "video":
+          break;
+        default:
+          throw new this._win.TypeError("Invalid media kind");
+      }
+    } else {
+      sendTrack = sendTrackOrKind;
+      kind = sendTrack.kind;
+    }
+
+    let transceiverImpl = this._impl.createTransceiverImpl(kind, sendTrack);
+    let transceiver = this._win.RTCRtpTransceiver._create(
+        this._win,
+        new RTCRtpTransceiver(this, transceiverImpl, init, kind, sendTrack));
+    transceiver.sync();
+    this._transceivers.push(transceiver);
+    return transceiver;
   }
 
-  _insertDTMF(sender, tones, duration, interToneGap) {
-    return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
+  _onTransceiverNeeded(kind, transceiverImpl) {
+    let init = {direction: "recvonly"};
+    let transceiver = this._win.RTCRtpTransceiver._create(
+        this._win,
+        new RTCRtpTransceiver(this, transceiverImpl, init, kind, null));
+    transceiver.sync();
+    this._transceivers.push(transceiver);
+  }
+
+  addTransceiver(sendTrackOrKind, init) {
+    let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init);
+    this.updateNegotiationNeeded();
+    return transceiver;
+  }
+
+  _syncTransceivers() {
+    this._transceivers.forEach(transceiver => transceiver.sync());
+  }
+
+  updateNegotiationNeeded() {
+    if (this._closed || this.signalingState != "stable") {
+      return;
+    }
+
+    let negotiationNeeded = this._impl.checkNegotiationNeeded();
+    if (!negotiationNeeded) {
+      this._negotiationNeeded = false;
+      return;
+    }
+
+    if (this._negotiationNeeded) {
+      return;
+    }
+
+    this._negotiationNeeded = true;
+
+    this._queueTaskWithClosedCheck(() => {
+      if (this._negotiationNeeded) {
+        this.dispatchEvent(new this._win.Event("negotiationneeded"));
+      }
+    });
+  }
+
+  _getOrCreateStream(id) {
+    if (!this._receiveStreams.has(id)) {
+      let stream = new this._win.MediaStream();
+      stream.assignId(id);
+      // Legacy event, remove eventually
+      let ev = new this._win.MediaStreamEvent("addstream", { stream });
+      this.dispatchEvent(ev);
+      this._receiveStreams.set(id, stream);
+    }
+
+    return this._receiveStreams.get(id);
+  }
+
+  _insertDTMF(transceiverImpl, tones, duration, interToneGap) {
+    return this._impl.insertDTMF(transceiverImpl, tones, duration, interToneGap);
   }
 
   _getDTMFToneBuffer(sender) {
     return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
   }
 
-  async _replaceTrack(sender, withTrack) {
+  _replaceTrack(transceiverImpl, withTrack) {
     this._checkClosed();
-    return this._chain(() => new Promise((resolve, reject) => {
-      this._onReplaceTrackSender = sender;
-      this._onReplaceTrackWithTrack = withTrack;
-      this._onReplaceTrackSuccess = resolve;
-      this._onReplaceTrackFailure = reject;
-      this._impl.replaceTrack(sender.track, withTrack);
-    }));
-  }
-
-  _setParameters({ track }, parameters) {
-    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
-      return;
-    }
-    // validate parameters input
-    var encodings = parameters.encodings || [];
-
-    encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
-      if (scaleResolutionDownBy < 1.0) {
-        throw new this._win.RangeError("scaleResolutionDownBy must be >= 1.0");
-      }
-      if (!rid && encodings.length > 1) {
-        throw new this._win.DOMException("Missing rid", "TypeError");
-      }
-      if (uniqueRids[rid]) {
-        throw new this._win.DOMException("Duplicate rid", "TypeError");
-      }
-      uniqueRids[rid] = true;
-      return uniqueRids;
-    }, {});
-
-    this._impl.setParameters(track, parameters);
-  }
-
-  _getParameters({ track }) {
-    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
-      return null;
-    }
-    return this._impl.getParameters(track);
+    this._impl.replaceTrackNoRenegotiation(transceiverImpl, withTrack);
   }
 
   close() {
     if (this._closed) {
       return;
     }
     this._closed = true;
-    this._inClose = true;
     this.changeIceConnectionState("closed");
     this._localIdp.close();
     this._remoteIdp.close();
     this._impl.close();
-    this._inClose = false;
+    this._suppressEvents = true;
+    delete this._pc;
+    delete this._observer;
   }
 
   getLocalStreams() {
     this._checkClosed();
-    return this._impl.getLocalStreams();
+    let localStreams = new Set();
+    this._transceivers.forEach(transceiver => {
+      transceiver.sender.getStreams().forEach(stream => {
+        localStreams.add(stream);
+      });
+    });
+    return [...localStreams.values()];
   }
 
   getRemoteStreams() {
     this._checkClosed();
-    return this._impl.getRemoteStreams();
+    return [...this._receiveStreams.values()];
   }
 
   getSenders() {
-    return this._senders;
+    return this.getTransceivers().map(transceiver => transceiver.sender);
   }
 
   getReceivers() {
-    return this._receivers;
+    return this.getTransceivers().map(transceiver => transceiver.receiver);
   }
 
   mozAddRIDExtension(receiver, extensionId) {
     this._impl.addRIDExtension(receiver.track, extensionId);
   }
 
   mozAddRIDFilter(receiver, rid) {
     this._impl.addRIDFilter(receiver.track, rid);
@@ -1185,16 +1338,20 @@ class RTCPeerConnection {
   mozEnablePacketDump(level, type, sending) {
     this._impl.enablePacketDump(level, type, sending);
   }
 
   mozDisablePacketDump(level, type, sending) {
     this._impl.disablePacketDump(level, type, sending);
   }
 
+  getTransceivers() {
+    return this._transceivers;
+  }
+
   get localDescription() {
     this._checkClosed();
     let sdp = this._impl.localDescription;
     if (sdp.length == 0) {
       return null;
     }
     return new this._win.RTCSessionDescription({ type: this._localType, sdp });
   }
@@ -1322,19 +1479,27 @@ class RTCPeerConnection {
     if (maxPacketLifeTime) {
       type = Ci.IPeerConnection.kDataChannelPartialReliableTimed;
     } else if (maxRetransmits) {
       type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit;
     } else {
       type = Ci.IPeerConnection.kDataChannelReliable;
     }
     // Synchronous since it doesn't block.
-    return this._impl.createDataChannel(label, protocol, type, ordered,
-                                        maxPacketLifeTime, maxRetransmits,
-                                        negotiated, id);
+    let dataChannel =
+      this._impl.createDataChannel(label, protocol, type, ordered,
+                                   maxPacketLifeTime, maxRetransmits,
+                                   negotiated, id);
+
+    // Spec says to only do this if this is the first DataChannel created,
+    // but the c++ code that does the "is negotiation needed" checking will
+    // only ever return true on the first one.
+    this.updateNegotiationNeeded();
+
+    return dataChannel;
   }
 }
 setupPrototype(RTCPeerConnection, {
   classID: PC_CID,
   contractID: PC_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer]),
   _actions: {
@@ -1390,20 +1555,26 @@ class PeerConnectionObserver {
     this._dompc._onCreateAnswerSuccess(sdp);
   }
 
   onCreateAnswerError(code, message) {
     this._dompc._onCreateAnswerFailure(this.newError(message, code));
   }
 
   onSetLocalDescriptionSuccess() {
+    this._dompc._syncTransceivers();
+    this._negotiationNeeded = false;
+    this._dompc.updateNegotiationNeeded();
     this._dompc._onSetLocalDescriptionSuccess();
   }
 
   onSetRemoteDescriptionSuccess() {
+    this._dompc._syncTransceivers();
+    this._negotiationNeeded = false;
+    this._dompc.updateNegotiationNeeded();
     this._dompc._onSetRemoteDescriptionSuccess();
   }
 
   onSetLocalDescriptionError(code, message) {
     this._localType = null;
     this._dompc._onSetLocalDescriptionFailure(this.newError(message, code));
   }
 
@@ -1430,20 +1601,16 @@ class PeerConnectionObserver {
     } else {
       candidate = null;
 
     }
     this.dispatchEvent(new win.RTCPeerConnectionIceEvent("icecandidate",
                                                          { candidate }));
   }
 
-  onNegotiationNeeded() {
-    this.dispatchEvent(new this._win.Event("negotiationneeded"));
-  }
-
   // This method is primarily responsible for updating iceConnectionState.
   // This state is defined in the WebRTC specification as follows:
   //
   // iceConnectionState:
   // -------------------
   //   new           Any of the RTCIceTransports are in the new state and none
   //                 of them are in the checking, failed or disconnected state.
   //
@@ -1553,81 +1720,85 @@ class PeerConnectionObserver {
                               pc._onGetStatsIsLegacy);
     pc._onGetStatsSuccess(webidlobj);
   }
 
   onGetStatsError(code, message) {
     this._dompc._onGetStatsFailure(this.newError(message, code));
   }
 
-  onAddStream(stream) {
-    let ev = new this._dompc._win.MediaStreamEvent("addstream", { stream });
-    this.dispatchEvent(ev);
-  }
-
   onRemoveStream(stream) {
     this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream",
                                                              { stream }));
   }
 
-  onAddTrack(track, streams) {
+  _getTransceiverWithRecvTrack(webrtcTrackId) {
+    return this._dompc.getTransceivers().find(
+        transceiver => transceiver.remoteTrackIdIs(webrtcTrackId));
+  }
+
+  onTrack(webrtcTrackId, streamIds) {
     let pc = this._dompc;
-    let receiver = pc._win.RTCRtpReceiver._create(pc._win,
-                                                  new RTCRtpReceiver(pc,
-                                                                     track));
-    pc._receivers.push(receiver);
-    let ev = new pc._win.RTCTrackEvent("track", { receiver, track, streams });
+    let matchingTransceiver = this._getTransceiverWithRecvTrack(webrtcTrackId);
+
+    // Get or create MediaStreams, and add the new track to them.
+    let streams = streamIds.map(id => this._dompc._getOrCreateStream(id));
+
+    streams.forEach(stream => {
+      stream.addTrack(matchingTransceiver.receiver.track);
+      // Adding tracks from JS does not result in the stream getting
+      // onaddtrack, so we need to do that here. The mediacapture spec says
+      // this needs to be queued, also.
+      pc._queueTaskWithClosedCheck(() => {
+        stream.dispatchEvent(
+            new pc._win.MediaStreamTrackEvent(
+              "addtrack", { track: matchingTransceiver.receiver.track }));
+      });
+    });
+
+
+    let ev = new pc._win.RTCTrackEvent("track", {
+      receiver: matchingTransceiver.receiver,
+      track: matchingTransceiver.receiver.track,
+      streams,
+      transceiver: matchingTransceiver });
     this.dispatchEvent(ev);
 
     // Fire legacy event as well for a little bit.
-    ev = new pc._win.MediaStreamTrackEvent("addtrack", { track });
+    ev = new pc._win.MediaStreamTrackEvent("addtrack",
+        { track: matchingTransceiver.receiver.track });
     this.dispatchEvent(ev);
   }
 
-  onRemoveTrack(track) {
-    let pc = this._dompc;
-    let i = pc._receivers.findIndex(receiver => receiver.track == track);
-    if (i >= 0) {
-      pc._receivers.splice(i, 1);
-    }
-  }
-
-  onReplaceTrackSuccess() {
-    var pc = this._dompc;
-    pc._onReplaceTrackSender.track = pc._onReplaceTrackWithTrack;
-    pc._onReplaceTrackWithTrack = null;
-    pc._onReplaceTrackSender = null;
-    pc._onReplaceTrackSuccess();
-  }
-
-  onReplaceTrackError(code, message) {
-    var pc = this._dompc;
-    pc._onReplaceTrackWithTrack = null;
-    pc._onReplaceTrackSender = null;
-    pc._onReplaceTrackFailure(this.newError(message, code));
+  onTransceiverNeeded(kind, transceiverImpl) {
+    this._dompc._onTransceiverNeeded(kind, transceiverImpl);
   }
 
   notifyDataChannel(channel) {
     this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
                                                                 { channel }));
   }
 
-  onDTMFToneChange(trackId, tone) {
+  onDTMFToneChange(track, tone) {
     var pc = this._dompc;
-    var sender = pc._senders.find(({track}) => track.id == trackId);
+    var sender = pc.getSenders().find(sender => sender.track == track);
     sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
                                                                  { tone }));
   }
 
   onPacket(level, type, sending, packet) {
     var pc = this._dompc;
     if (pc._onPacket) {
       pc._onPacket(level, type, sending, packet);
     }
   }
+
+  syncTransceivers() {
+    this._dompc._syncTransceivers();
+  }
 }
 setupPrototype(PeerConnectionObserver, {
   classID: PC_OBS_CID,
   contractID: PC_OBS_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
                                          Ci.nsIDOMGlobalPropertyInitializer])
 });
 
@@ -1662,87 +1833,352 @@ class RTCDTMFSender {
   }
 
   set ontonechange(handler) {
     this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
   }
 
   insertDTMF(tones, duration, interToneGap) {
     this._sender._pc._checkClosed();
-
-    if (this._sender._pc._senders.indexOf(this._sender.__DOM_IMPL__) == -1) {
-      throw new this._sender._pc._win.DOMException("RTCRtpSender is stopped",
-                                                   "InvalidStateError");
-    }
-
-    duration = Math.max(40, Math.min(duration, 6000));
-    if (interToneGap < 30) interToneGap = 30;
-
-    tones = tones.toUpperCase();
-
-    if (tones.match(/[^0-9A-D#*,]/)) {
-      throw new this._sender._pc._win.DOMException("Invalid DTMF characters",
-                                                   "InvalidCharacterError");
-    }
-
-    this._sender._pc._insertDTMF(this._sender, tones, duration, interToneGap);
+    this._sender._transceiver.insertDTMF(tones, duration, interToneGap);
   }
 }
 setupPrototype(RTCDTMFSender, {
   classID: PC_DTMF_SENDER_CID,
   contractID: PC_DTMF_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpSender {
-  constructor(pc, track, stream) {
-    let dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
-    Object.assign(this, { _pc: pc, track, _stream: stream, dtmf });
+  constructor(pc, transceiverImpl, transceiver, track, streams) {
+    let dtmf = pc._win.RTCDTMFSender._create(
+        pc._win, new RTCDTMFSender(this));
+
+    Object.assign(this, {
+      _pc: pc,
+      _transceiverImpl: transceiverImpl,
+      _transceiver: transceiver,
+      track,
+      _streams: streams,
+      dtmf });
   }
 
   replaceTrack(withTrack) {
-    return this._pc._async(() => this._pc._replaceTrack(this, withTrack));
+    // async functions in here return a chrome promise, which is not something
+    // content can use. This wraps that promise in something content can use.
+    return this._pc._win.Promise.resolve(this._replaceTrack(withTrack));
+  }
+
+  async _replaceTrack(withTrack) {
+    this._pc._checkClosed();
+
+    if (this._transceiver.stopped) {
+      throw new this._pc._win.DOMException(
+          "Cannot call replaceTrack when transceiver is stopped",
+          "InvalidStateError");
+    }
+
+    if (withTrack && (withTrack.kind != this._transceiver.getKind())) {
+      throw new this._pc._win.DOMException(
+          "Cannot replaceTrack with a different kind!",
+          "TypeError");
+    }
+
+    // Updates the track on the MediaPipeline; this is needed whether or not
+    // we've associated this transceiver, the spec language notwithstanding.
+    // Synchronous, and will throw on failure.
+    this._pc._replaceTrack(this._transceiverImpl, withTrack);
+
+    let setTrack = () => {
+      this.track = withTrack;
+      this._transceiver.sync();
+    };
+
+    // Spec is a little weird here; we only queue if the transceiver was
+    // associated, otherwise we update the track synchronously.
+    if (this._transceiver.mid == null) {
+      setTrack();
+    } else {
+      // We're supposed to queue a task if the transceiver is associated
+      await this._pc._queueTaskWithClosedCheck(setTrack);
+    }
   }
 
   setParameters(parameters) {
-    return this._pc._win.Promise.resolve()
-      .then(() => this._pc._setParameters(this, parameters));
+    return this._pc._win.Promise.resolve(this._setParameters(parameters));
+  }
+
+  async _setParameters(parameters) {
+    this._pc._checkClosed();
+
+    if (this._transceiver.stopped) {
+      throw new this._pc._win.DOMException(
+          "This sender's transceiver is stopped", "InvalidStateError");
+    }
+
+    if (!Services.prefs.getBoolPref("media.peerconnection.simulcast")) {
+      return;
+    }
+
+    parameters.encodings = parameters.encodings || [];
+
+    parameters.encodings.reduce((uniqueRids, { rid, scaleResolutionDownBy }) => {
+      if (scaleResolutionDownBy < 1.0) {
+        throw new this._pc._win.RangeError("scaleResolutionDownBy must be >= 1.0");
+      }
+      if (!rid && parameters.encodings.length > 1) {
+        throw new this._pc._win.DOMException("Missing rid", "TypeError");
+      }
+      if (uniqueRids[rid]) {
+        throw new this._pc._win.DOMException("Duplicate rid", "TypeError");
+      }
+      uniqueRids[rid] = true;
+      return uniqueRids;
+    }, {});
+
+    // TODO(bug 1401592): transaction ids, timing changes
+
+    await this._pc._queueTaskWithClosedCheck(() => {
+      this.parameters = parameters;
+      this._transceiver.sync();
+    });
   }
 
   getParameters() {
-    return this._pc._getParameters(this);
+    // TODO(bug 1401592): transaction ids
+
+    // All the other stuff that the spec says to update is handled when
+    // transceivers are synced.
+    return this.parameters;
+  }
+
+  setStreams(streams) {
+    this._streams = streams;
+  }
+
+  getStreams() {
+    return this._streams;
+  }
+
+  setTrack(track) {
+    this.track = track;
   }
 
   getStats() {
     return this._pc._async(
       async () => this._pc._getStats(this.track));
   }
+
+  checkWasCreatedByPc(pc) {
+    if (pc != this._pc.__DOM_IMPL__) {
+      throw new this._pc._win.DOMException(
+          "This sender was not created by this PeerConnection",
+          "InvalidAccessError");
+    }
+  }
 }
 setupPrototype(RTCRtpSender, {
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
 class RTCRtpReceiver {
-  constructor(pc, track) {
-    Object.assign(this, { _pc: pc, track });
+  constructor(pc, transceiverImpl) {
+    // We do not set the track here; that is done when _transceiverImpl is set
+    Object.assign(this,
+        {
+          _pc: pc,
+          _transceiverImpl: transceiverImpl,
+          track: transceiverImpl.getReceiveTrack()
+        });
   }
 
+  // TODO(bug 1401983): Create a getStats binding on TransceiverImpl, and use
+  // that here.
   getStats() {
     return this._pc._async(
       async () => this._pc.getStats(this.track));
   }
 }
 setupPrototype(RTCRtpReceiver, {
   classID: PC_RECEIVER_CID,
   contractID: PC_RECEIVER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
 });
 
+class RTCRtpTransceiver {
+  constructor(pc, transceiverImpl, init, kind, sendTrack) {
+    let receiver = pc._win.RTCRtpReceiver._create(
+        pc._win, new RTCRtpReceiver(pc, transceiverImpl, kind));
+    let streams = (init && init.streams) || [];
+    let sender = pc._win.RTCRtpSender._create(
+        pc._win, new RTCRtpSender(pc, transceiverImpl, this, sendTrack, streams));
+
+    let direction = (init && init.direction) || "sendrecv";
+    Object.assign(this,
+        {
+          _pc: pc,
+          mid: null,
+          sender,
+          receiver,
+          stopped: false,
+          _direction: direction,
+          currentDirection: null,
+          _remoteTrackId: null,
+          addTrackMagic: false,
+          _hasBeenUsedToSend: false,
+          // the receiver starts out without a track, so record this here
+          _kind: kind,
+          _transceiverImpl: transceiverImpl
+        });
+  }
+
+  set direction(direction) {
+    this._pc._checkClosed();
+
+    if (this.stopped) {
+      throw new this._pc._win.DOMException("Transceiver is stopped!",
+                                           "InvalidStateError");
+    }
+
+    if (this._direction == direction) {
+      return;
+    }
+
+    this._direction = direction;
+    this.sync();
+    this._pc.updateNegotiationNeeded();
+  }
+
+  get direction() {
+    return this._direction;
+  }
+
+  setDirectionInternal(direction) {
+    this._direction = direction;
+  }
+
+  stop() {
+    if (this.stopped) {
+      return;
+    }
+
+    this._pc._checkClosed();
+
+    this.setStopped();
+    this.sync();
+    this._pc.updateNegotiationNeeded();
+  }
+
+  setStopped() {
+    this.stopped = true;
+    this.currentDirection = null;
+  }
+
+  remove() {
+    var index = this._pc._transceivers.indexOf(this.__DOM_IMPL__);
+    if (index != -1) {
+      this._pc._transceivers.splice(index, 1);
+    }
+  }
+
+  getKind() {
+    return this._kind;
+  }
+
+  hasBeenUsedToSend() {
+    return this._hasBeenUsedToSend;
+  }
+
+  setRemoteTrackId(webrtcTrackId) {
+    this._remoteTrackId = webrtcTrackId;
+  }
+
+  remoteTrackIdIs(webrtcTrackId) {
+    return this._remoteTrackId == webrtcTrackId;
+  }
+
+  getRemoteTrackId() {
+    return this._remoteTrackId;
+  }
+
+  setAddTrackMagic() {
+    this.addTrackMagic = true;
+  }
+
+  sync() {
+    if (this._syncing) {
+      throw new DOMException("Reentrant sync! This is a bug!", "InternalError");
+    }
+    this._syncing = true;
+    this._transceiverImpl.syncWithJS(this.__DOM_IMPL__);
+    this._syncing = false;
+  }
+
+  // Used by _transceiverImpl.syncWithJS, don't call sync again!
+  setCurrentDirection(direction) {
+    if (this.stopped) {
+      return;
+    }
+
+    switch (direction) {
+      case "sendrecv":
+      case "sendonly":
+        this._hasBeenUsedToSend = true;
+        break;
+      default:
+    }
+
+    this.currentDirection = direction;
+  }
+
+  // Used by _transceiverImpl.syncWithJS, don't call sync again!
+  setMid(mid) {
+    this.mid = mid;
+  }
+
+  // Used by _transceiverImpl.syncWithJS, don't call sync again!
+  unsetMid() {
+    this.mid = null;
+  }
+
+  insertDTMF(tones, duration, interToneGap) {
+    if (this.stopped) {
+      throw new this._pc._win.DOMException("Transceiver is stopped!",
+                                           "InvalidStateError");
+    }
+
+    if (!this.sender.track) {
+      throw new this._pc._win.DOMException("RTCRtpSender has no track",
+                                           "InvalidStateError");
+    }
+
+    duration = Math.max(40, Math.min(duration, 6000));
+    if (interToneGap < 30) interToneGap = 30;
+
+    tones = tones.toUpperCase();
+
+    if (tones.match(/[^0-9A-D#*,]/)) {
+      throw new this._pc._win.DOMException("Invalid DTMF characters",
+                                           "InvalidCharacterError");
+    }
+
+    // TODO (bug 1401983): Move this API to TransceiverImpl so we don't need the
+    // extra hops through RTCPeerConnection and PeerConnectionImpl
+    this._pc._insertDTMF(this._transceiverImpl, tones, duration, interToneGap);
+  }
+}
+
+setupPrototype(RTCRtpTransceiver, {
+  classID: PC_TRANSCEIVER_CID,
+  contractID: PC_TRANSCEIVER_CONTRACT,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports])
+});
+
 class CreateOfferRequest {
   constructor(windowID, innerWindowID, callID, isSecure) {
     Object.assign(this, { windowID, innerWindowID, callID, isSecure });
   }
 }
 setupPrototype(CreateOfferRequest, {
   classID: PC_COREQUEST_CID,
   contractID: PC_COREQUEST_CONTRACT,
@@ -1753,12 +2189,13 @@ this.NSGetFactory = XPCOMUtils.generateN
   [GlobalPCList,
    RTCDTMFSender,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCPeerConnectionStatic,
    RTCRtpReceiver,
    RTCRtpSender,
+   RTCRtpTransceiver,
    RTCStatsReport,
    PeerConnectionObserver,
    CreateOfferRequest]
 );