Bug 1032835 - addTrack/removeTrack on-top of existing implementation. r=smaug, r=jesup
authorJan-Ivar Bruaroey <jib@mozilla.com>
Wed, 13 Aug 2014 21:40:54 -0400
changeset 199991 79525105ef9acc02ced1107f5540ea056b74edf4
parent 199990 880ae230025d343115c09525802d8c6114507ba5
child 199992 1edead1c5b2d630cb94ee4f2e7133c6819b423dc
push id9784
push userryanvm@gmail.com
push dateSat, 16 Aug 2014 21:45:40 +0000
treeherderb2g-inbound@94ba78a42305 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, jesup
bugs1032835
milestone34.0a1
Bug 1032835 - addTrack/removeTrack on-top of existing implementation. r=smaug, r=jesup
content/media/DOMMediaStream.cpp
content/media/DOMMediaStream.h
dom/media/PeerConnection.js
dom/media/tests/mochitest/pc.js
dom/webidl/MediaStream.webidl
dom/webidl/PeerConnectionImpl.webidl
dom/webidl/PeerConnectionObserver.webidl
dom/webidl/RTCPeerConnection.webidl
dom/webidl/RTCRtpReceiver.webidl
dom/webidl/RTCRtpSender.webidl
media/webrtc/signaling/src/media/VcmSIPCCBinding.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
--- a/content/media/DOMMediaStream.cpp
+++ b/content/media/DOMMediaStream.cpp
@@ -191,16 +191,28 @@ DOMMediaStream::GetVideoTracks(nsTArray<
   for (uint32_t i = 0; i < mTracks.Length(); ++i) {
     VideoStreamTrack* t = mTracks[i]->AsVideoStreamTrack();
     if (t) {
       aTracks.AppendElement(t);
     }
   }
 }
 
+void
+DOMMediaStream::GetTracks(nsTArray<nsRefPtr<MediaStreamTrack> >& aTracks)
+{
+  aTracks.AppendElements(mTracks);
+}
+
+bool
+DOMMediaStream::HasTrack(const MediaStreamTrack& aTrack) const
+{
+  return mTracks.Contains(&aTrack);
+}
+
 bool
 DOMMediaStream::IsFinished()
 {
   return !mStream || mStream->IsFinished();
 }
 
 void
 DOMMediaStream::InitSourceStream(nsIDOMWindow* aWindow, TrackTypeHints aHintContents)
--- a/content/media/DOMMediaStream.h
+++ b/content/media/DOMMediaStream.h
@@ -76,16 +76,18 @@ public:
   }
   virtual JSObject* WrapObject(JSContext* aCx) MOZ_OVERRIDE;
 
   // WebIDL
   double CurrentTime();
 
   void GetAudioTracks(nsTArray<nsRefPtr<AudioStreamTrack> >& aTracks);
   void GetVideoTracks(nsTArray<nsRefPtr<VideoStreamTrack> >& aTracks);
+  void GetTracks(nsTArray<nsRefPtr<MediaStreamTrack> >& aTracks);
+  bool HasTrack(const MediaStreamTrack& aTrack) const;
 
   MediaStream* GetStream() const { return mStream; }
 
   /**
    * Overridden in DOMLocalMediaStreams to allow getUserMedia to pass
    * data directly to RTCPeerConnection without going through graph queuing.
    * Returns a bool to let us know if direct data will be delivered.
    */
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -205,22 +205,16 @@ RTCSessionDescription.prototype = {
 
   __init: function(dict) {
     this.type = dict.type;
     this.sdp  = dict.sdp;
   }
 };
 
 function RTCStatsReport(win, dict) {
-  function appendStats(stats, report) {
-    stats.forEach(function(stat) {
-        report[stat.id] = stat;
-      });
-  }
-
   this._win = win;
   this._pcid = dict.pcid;
   this._report = convertToRTCStatsReport(dict);
 }
 RTCStatsReport.prototype = {
   classDescription: "RTCStatsReport",
   classID: PC_STATS_CID,
   contractID: PC_STATS_CONTRACT,
@@ -282,16 +276,18 @@ RTCIdentityAssertion.prototype = {
   __init: function(idp, name) {
     this.idp = idp;
     this.name  = name;
   }
 };
 
 function RTCPeerConnection() {
   this._queue = [];
+  this._senders = [];
+  this._receivers = [];
 
   this._pc = null;
   this._observer = null;
   this._closed = false;
 
   this._onCreateOfferSuccess = null;
   this._onCreateOfferFailure = null;
   this._onCreateAnswerSuccess = null;
@@ -336,16 +332,17 @@ RTCPeerConnection.prototype = {
     this._mustValidateRTCConfiguration(rtcConfig,
         "RTCPeerConnection constructor passed invalid RTCConfiguration");
     if (_globalPCList._networkdown) {
       throw new this._win.DOMError("",
           "Can't create RTCPeerConnections when the network is down");
     }
 
     this.makeGetterSetterEH("onaddstream");
+    this.makeGetterSetterEH("onaddtrack");
     this.makeGetterSetterEH("onicecandidate");
     this.makeGetterSetterEH("onnegotiationneeded");
     this.makeGetterSetterEH("onsignalingstatechange");
     this.makeGetterSetterEH("onremovestream");
     this.makeGetterSetterEH("ondatachannel");
     this.makeGetterSetterEH("oniceconnectionstatechange");
     this.makeGetterSetterEH("onidentityresult");
     this.makeGetterSetterEH("onpeeridentity");
@@ -800,16 +797,36 @@ RTCPeerConnection.prototype = {
      // Bug 844295: Not implementing this functionality.
      throw new this._win.DOMError("", "removeStream not yet implemented");
   },
 
   getStreamById: function(id) {
     throw new this._win.DOMError("", "getStreamById not yet implemented");
   },
 
+  addTrack: function(track, stream) {
+    if (stream.currentTime === undefined) {
+      throw new this._win.DOMError("", "invalid stream.");
+    }
+    if (stream.getTracks().indexOf(track) == -1) {
+      throw new this._win.DOMError("", "track is not in stream.");
+    }
+    this._checkClosed();
+    this._impl.addTrack(track, stream);
+    let sender = this._win.RTCRtpSender._create(this._win,
+                                                new RTCRtpSender(this, track));
+    this._senders.push({ sender: sender, stream: stream });
+    return sender;
+  },
+
+  removeTrack: function(sender) {
+     // Bug 844295: Not implementing this functionality.
+     throw new this._win.DOMError("", "removeTrack not yet implemented");
+  },
+
   close: function() {
     if (this._closed) {
       return;
     }
     this.changeIceConnectionState("closed");
     this._queueOrRun({ func: this._close, args: [false], wait: false });
     this._closed = true;
   },
@@ -825,16 +842,46 @@ RTCPeerConnection.prototype = {
     return this._impl.getLocalStreams();
   },
 
   getRemoteStreams: function() {
     this._checkClosed();
     return this._impl.getRemoteStreams();
   },
 
+  getSenders: function() {
+    this._checkClosed();
+    let streams = this._impl.getLocalStreams();
+    let senders = [];
+    // prune senders in case any streams have disappeared down below
+    for (let i = this._senders.length - 1; i >= 0; i--) {
+      if (streams.indexOf(this._senders[i].stream) != -1) {
+        senders.push(this._senders[i].sender);
+      } else {
+        this._senders.splice(i,1);
+      }
+    }
+    return senders;
+  },
+
+  getReceivers: function() {
+    this._checkClosed();
+    let streams = this._impl.getRemoteStreams();
+    let receivers = [];
+    // prune receivers in case any streams have disappeared down below
+    for (let i = this._receivers.length - 1; i >= 0; i--) {
+      if (streams.indexOf(this._receivers[i].stream) != -1) {
+        receivers.push(this._receivers[i].receiver);
+      } else {
+        this._receivers.splice(i,1);
+      }
+    }
+    return receivers;
+  },
+
   get localDescription() {
     this._checkClosed();
     let sdp = this._impl.localDescription;
     if (sdp.length == 0) {
       return null;
     }
 
     sdp = this._localIdp.wrapSdp(sdp);
@@ -1243,16 +1290,27 @@ PeerConnectionObserver.prototype = {
     this._dompc.dispatchEvent(ev);
   },
 
   onRemoveStream: function(stream, type) {
     this.dispatchEvent(new this._dompc._win.MediaStreamEvent("removestream",
                                                              { stream: stream }));
   },
 
+  onAddTrack: function(track) {
+    let ev = new this._dompc._win.MediaStreamTrackEvent("addtrack",
+                                                        { track: track });
+    this._dompc.dispatchEvent(ev);
+  },
+
+  onRemoveTrack: function(track, type) {
+    this.dispatchEvent(new this._dompc._win.MediaStreamTrackEvent("removetrack",
+                                                                  { track: track }));
+  },
+
   foundIceCandidate: function(cand) {
     this.dispatchEvent(new this._dompc._win.RTCPeerConnectionIceEvent("icecandidate",
                                                                       { candidate: cand } ));
   },
 
   notifyDataChannel: function(channel) {
     this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
                                                                 { channel: channel }));
@@ -1274,44 +1332,36 @@ RTCPeerConnectionStatic.prototype = {
       .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
   },
 
   registerPeerConnectionLifecycleCallback: function(cb) {
     _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb);
   },
 };
 
-function RTCRtpSender() {}
+function RTCRtpSender(pc, track) {
+  this.pc = pc;
+  this.track = track;
+}
 RTCRtpSender.prototype = {
   classDescription: "RTCRtpSender",
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
-                                         Ci.nsIDOMGlobalPropertyInitializer]),
-
-  init: function(win) { this._win = win; },
-
-  __init: function(track) {
-    this.track = track;
-  }
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 };
 
-function RTCRtpReceiver() {}
+function RTCRtpReceiver(pc, track) {
+  this.pc = pc;
+  this.track = track;
+}
 RTCRtpReceiver.prototype = {
   classDescription: "RTCRtpReceiver",
   classID: PC_RECEIVER_CID,
   contractID: PC_RECEIVER_CONTRACT,
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
-                                         Ci.nsIDOMGlobalPropertyInitializer]),
-
-  init: function(win) { this._win = win; },
-
-  __init: function(track) {
-    this.track = track;
-  }
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
   [GlobalPCList,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCPeerConnectionStatic,
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -1538,17 +1538,19 @@ PeerConnectionWrapper.prototype = {
    * @param {string} side
    *        The location the stream is coming from ('local' or 'remote')
    */
   attachMedia : function PCW_attachMedia(stream, type, side) {
     info("Got media stream: " + type + " (" + side + ")");
     this.streams.push(stream);
 
     if (side === 'local') {
-      this._pc.addStream(stream);
+      stream.getTracks().forEach(function(track) {
+        this._pc.addTrack(track, stream);
+      }.bind(this));
     }
 
     var element = createMediaElement(type, this.label + '_' + side);
     this.mediaCheckers.push(new MediaElementChecker(element));
     element.mozSrcObject = stream;
     element.play();
   },
 
--- a/dom/webidl/MediaStream.webidl
+++ b/dom/webidl/MediaStream.webidl
@@ -18,18 +18,19 @@ dictionary MediaStreamConstraints {
     (boolean or MediaTrackConstraints) video = false;
     boolean picture = false; // Mozilla legacy
     boolean fake = false;    // for testing
     DOMString? peerIdentity = null;
 };
 
 interface MediaStream {
     // readonly attribute DOMString    id;
-    sequence<AudioStreamTrack> getAudioTracks ();
-    sequence<VideoStreamTrack> getVideoTracks ();
+    sequence<AudioStreamTrack> getAudioTracks();
+    sequence<VideoStreamTrack> getVideoTracks();
+    sequence<MediaStreamTrack> getTracks();
     // MediaStreamTrack           getTrackById (DOMString trackId);
     // void                       addTrack (MediaStreamTrack track);
     // void                       removeTrack (MediaStreamTrack track);
     //         attribute boolean      ended;
     //         attribute EventHandler onended;
     //         attribute EventHandler onaddtrack;
     //         attribute EventHandler onremovetrack;
 	readonly attribute double currentTime;
--- a/dom/webidl/PeerConnectionImpl.webidl
+++ b/dom/webidl/PeerConnectionImpl.webidl
@@ -33,17 +33,21 @@ interface PeerConnectionImpl  {
   [Throws]
   void setRemoteDescription(long action, DOMString sdp);
 
   /* Stats call, calls either |onGetStatsSuccess| or |onGetStatsError| on our
      observer. (see the |PeerConnectionObserver| interface) */
   [Throws]
   void getStats(MediaStreamTrack? selector);
 
-  /* Adds the stream created by GetUserMedia */
+  /* Adds the tracks created by GetUserMedia */
+  [Throws]
+  void addTrack(MediaStreamTrack track, MediaStream... streams);
+  [Throws]
+  void removeTrack(MediaStreamTrack track);
   [Throws]
   void addStream(MediaStream stream);
   [Throws]
   void removeStream(MediaStream stream);
   [Throws]
   void closeStreams();
 
   sequence<MediaStream> getLocalStreams();
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -29,14 +29,14 @@ interface PeerConnectionObserver
   void onGetStatsError(unsigned long name, DOMString message);
 
   /* Data channel callbacks */
   void notifyDataChannel(DataChannel channel);
 
   /* Notification of one of several types of state changed */
   void onStateChange(PCObserverStateType state);
 
-  /* Changes to MediaStreams */
+  /* Changes to MediaStreamTracks */
   void onAddStream(MediaStream stream);
   void onRemoveStream();
-  void onAddTrack();
+  void onAddTrack(MediaStreamTrack track);
   void onRemoveTrack();
 };
--- a/dom/webidl/RTCPeerConnection.webidl
+++ b/dom/webidl/RTCPeerConnection.webidl
@@ -102,20 +102,22 @@ interface mozRTCPeerConnection : EventTa
   RTCConfiguration      getConfiguration ();
   sequence<MediaStream> getLocalStreams ();
   sequence<MediaStream> getRemoteStreams ();
   MediaStream? getStreamById (DOMString streamId);
   void addStream (MediaStream stream);
   void removeStream (MediaStream stream);
 
   // replaces addStream; fails if already added
-  // because a track can be part of multiple streams, the id parameter
-  // indicates which particular stream should be referenced in signaling
+  // because a track can be part of multiple streams, stream parameters
+  // indicate which particular streams should be referenced in signaling
 
-  RTCRtpSender addTrack(MediaStreamTrack track, DOMString streamId);
+  RTCRtpSender addTrack(MediaStreamTrack track,
+                        MediaStream stream,
+                        MediaStream... moreStreams);
   void removeTrack(RTCRtpSender sender);
 
   sequence<RTCRtpSender> getSenders();
   sequence<RTCRtpReceiver> getReceivers();
 
   void close ();
   attribute EventHandler onnegotiationneeded;
   attribute EventHandler onicecandidate;
--- a/dom/webidl/RTCRtpReceiver.webidl
+++ b/dom/webidl/RTCRtpReceiver.webidl
@@ -3,13 +3,12 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  *
  * The origin of this IDL file is
  * http://lists.w3.org/Archives/Public/public-webrtc/2014May/0067.html
  */
 
 [Pref="media.peerconnection.enabled",
- JSImplementation="@mozilla.org/dom/rtpreceiver;1",
- Constructor (MediaStreamTrack track)]
+ JSImplementation="@mozilla.org/dom/rtpreceiver;1"]
 interface RTCRtpReceiver {
   readonly attribute MediaStreamTrack track;
 };
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -3,13 +3,12 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/.
  *
  * The origin of this IDL file is
  * http://lists.w3.org/Archives/Public/public-webrtc/2014May/0067.html
  */
 
 [Pref="media.peerconnection.enabled",
- JSImplementation="@mozilla.org/dom/rtpsender;1",
- Constructor (MediaStreamTrack track)]
+ JSImplementation="@mozilla.org/dom/rtpsender;1"]
 interface RTCRtpSender {
   readonly attribute MediaStreamTrack track;
 };
--- a/media/webrtc/signaling/src/media/VcmSIPCCBinding.cpp
+++ b/media/webrtc/signaling/src/media/VcmSIPCCBinding.cpp
@@ -2530,16 +2530,20 @@ static int vcmTxStartICE_m(cc_mcapid_t m
               pc_stream_id,
               level);
 
   // Find the PC and get the stream
   sipcc::PeerConnectionWrapper pc(peerconnection);
   ENSURE_PC(pc, VCM_ERROR);
   nsRefPtr<sipcc::LocalSourceStreamInfo> stream =
     pc.impl()->media()->GetLocalStream(pc_stream_id);
+  if (!stream) {
+    CSFLogError(logTag, "Stream not found");
+    return VCM_ERROR;
+  }
 
   // Create the transport flows
   mozilla::RefPtr<TransportFlow> rtp_flow =
     vcmCreateTransportFlow(pc.impl(), level, false, setup_type,
                            fingerprint_alg, fingerprint);
   if (!rtp_flow) {
     CSFLogError(logTag, "Could not create RTP flow");
     return VCM_ERROR;
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -280,16 +280,27 @@ public:
 
       // Start currentTime from the point where this stream was successfully
       // returned.
       aStream->SetLogicalStreamStartTime(aStream->GetStream()->GetCurrentTime());
 
       CSFLogInfo(logTag, "Returning success for OnAddStream()");
       // We are running on main thread here so we shouldn't have a race
       // on this callback
+
+      nsTArray<nsRefPtr<MediaStreamTrack>> tracks;
+      aStream->GetTracks(tracks);
+      for (uint32_t i = 0; i < tracks.Length(); i++) {
+        JSErrorResult rv;
+        mObserver->OnAddTrack(*tracks[i], rv);
+        if (rv.Failed()) {
+          CSFLogError(logTag, ": OnAddTrack(%d) failed! Error: %d", i,
+                      rv.ErrorCode());
+        }
+      }
       JSErrorResult rv;
       mObserver->OnAddStream(*aStream, rv);
       if (rv.Failed()) {
         CSFLogError(logTag, ": OnAddStream() failed! Error: %d", rv.ErrorCode());
       }
     }
   private:
     nsRefPtr<PeerConnectionObserver> mObserver;
@@ -1460,17 +1471,17 @@ PeerConnectionImpl::AddStream(DOMMediaSt
   if ((hints & DOMMediaStream::HINT_CONTENTS_VIDEO) &&
       mNumVideoStreams > 0) {
     CSFLogError(logTag, "%s: Only one local video stream is supported for now",
                 __FUNCTION__);
     return NS_ERROR_FAILURE;
   }
 
   uint32_t stream_id;
-  nsresult res = mMedia->AddStream(&aMediaStream, &stream_id);
+  nsresult res = mMedia->AddStream(&aMediaStream, hints, &stream_id);
   if (NS_FAILED(res)) {
     return res;
   }
 
   aMediaStream.AddPrincipalChangeObserver(this);
 
   // TODO(ekr@rtfm.com): these integers should be the track IDs
   if (hints & DOMMediaStream::HINT_CONTENTS_AUDIO) {
@@ -1485,25 +1496,151 @@ PeerConnectionImpl::AddStream(DOMMediaSt
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PeerConnectionImpl::RemoveStream(DOMMediaStream& aMediaStream) {
   PC_AUTO_ENTER_API_CALL(true);
 
+  uint32_t hints = aMediaStream.GetHintContents();
+
   uint32_t stream_id;
-  nsresult res = mMedia->RemoveStream(&aMediaStream, &stream_id);
+  nsresult res = mMedia->RemoveStream(&aMediaStream, hints, &stream_id);
 
   if (NS_FAILED(res))
     return res;
 
   aMediaStream.RemovePrincipalChangeObserver(this);
 
+  if (hints & DOMMediaStream::HINT_CONTENTS_AUDIO) {
+    mInternal->mCall->removeStream(stream_id, 0, AUDIO);
+    MOZ_ASSERT(mNumAudioStreams > 0);
+    mNumAudioStreams--;
+  }
+
+  if (hints & DOMMediaStream::HINT_CONTENTS_VIDEO) {
+    mInternal->mCall->removeStream(stream_id, 1, VIDEO);
+    MOZ_ASSERT(mNumVideoStreams > 0);
+    mNumVideoStreams--;
+  }
+
+  return NS_OK;
+}
+
+nsresult
+PeerConnectionImpl::AddTrack(MediaStreamTrack& aTrack,
+                             const Sequence<OwningNonNull<DOMMediaStream>>& aStreams)
+{
+  PC_AUTO_ENTER_API_CALL(true);
+
+  if (!aStreams.Length()) {
+    CSFLogError(logTag, "%s: At least one stream arg required", __FUNCTION__);
+    return NS_ERROR_FAILURE;
+  }
+  DOMMediaStream& aMediaStream = aStreams[0];
+#ifdef MOZILLA_INTERNAL_API
+  if (!aMediaStream.HasTrack(aTrack)) {
+    CSFLogError(logTag, "%s: Track is not in stream", __FUNCTION__);
+    return NS_ERROR_FAILURE;
+  }
+  uint32_t hints = aMediaStream.GetHintContents() &
+      ((aTrack.AsAudioStreamTrack()? DOMMediaStream::HINT_CONTENTS_AUDIO : 0) |
+       (aTrack.AsVideoStreamTrack()? DOMMediaStream::HINT_CONTENTS_VIDEO : 0));
+#else
   uint32_t hints = aMediaStream.GetHintContents();
+#endif
+
+  // XXX Remove this check once addStream has an error callback
+  // available and/or we have plumbing to handle multiple
+  // local audio streams.
+  if ((hints & DOMMediaStream::HINT_CONTENTS_AUDIO) &&
+      mNumAudioStreams > 0) {
+    CSFLogError(logTag, "%s: Only one local audio stream is supported for now",
+                __FUNCTION__);
+    return NS_ERROR_FAILURE;
+  }
+
+  // XXX Remove this check once addStream has an error callback
+  // available and/or we have plumbing to handle multiple
+  // local video streams.
+  if ((hints & DOMMediaStream::HINT_CONTENTS_VIDEO) &&
+      mNumVideoStreams > 0) {
+    CSFLogError(logTag, "%s: Only one local video stream is supported for now",
+                __FUNCTION__);
+    return NS_ERROR_FAILURE;
+  }
+
+  uint32_t num = mMedia->LocalStreamsLength();
+
+  uint32_t stream_id;
+  nsresult res = mMedia->AddStream(&aMediaStream, hints, &stream_id);
+  if (NS_FAILED(res)) {
+    return res;
+  }
+
+  if (num != mMedia->LocalStreamsLength()) {
+    aMediaStream.AddPrincipalChangeObserver(this);
+  }
+
+  // TODO(ekr@rtfm.com): these integers should be the track IDs
+  if (hints & DOMMediaStream::HINT_CONTENTS_AUDIO) {
+    mInternal->mCall->addStream(stream_id, 0, AUDIO);
+    mNumAudioStreams++;
+  }
+
+  if (hints & DOMMediaStream::HINT_CONTENTS_VIDEO) {
+    mInternal->mCall->addStream(stream_id, 1, VIDEO);
+    mNumVideoStreams++;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::RemoveTrack(MediaStreamTrack& aTrack) {
+  PC_AUTO_ENTER_API_CALL(true);
+
+  DOMMediaStream *stream = nullptr;
+#ifdef MOZILLA_INTERNAL_API
+  for (uint32_t i = 0; i < mMedia->LocalStreamsLength(); ++i) {
+    auto* candidate = mMedia->GetLocalStream(i)->GetMediaStream();
+    if (candidate->HasTrack(aTrack)) {
+      stream = candidate;
+      break;
+    }
+  }
+#endif
+  if (!stream) {
+    CSFLogError(logTag, "%s: Track not found", __FUNCTION__);
+    return NS_OK;
+  }
+
+  DOMMediaStream& aMediaStream = *stream;
+
+#ifdef MOZILLA_INTERNAL_API
+  uint32_t hints = aMediaStream.GetHintContents() &
+      ((aTrack.AsAudioStreamTrack()? DOMMediaStream::HINT_CONTENTS_AUDIO : 0) |
+       (aTrack.AsVideoStreamTrack()? DOMMediaStream::HINT_CONTENTS_VIDEO : 0));
+#else
+  uint32_t hints = aMediaStream.GetHintContents();
+#endif
+
+  uint32_t num = mMedia->LocalStreamsLength();
+
+  uint32_t stream_id;
+  nsresult res = mMedia->RemoveStream(&aMediaStream, hints, &stream_id);
+
+  if (NS_FAILED(res)) {
+    return res;
+  }
+
+  if (num != mMedia->LocalStreamsLength()) {
+    aMediaStream.RemovePrincipalChangeObserver(this);
+  }
 
   if (hints & DOMMediaStream::HINT_CONTENTS_AUDIO) {
     mInternal->mCall->removeStream(stream_id, 0, AUDIO);
     MOZ_ASSERT(mNumAudioStreams > 0);
     mNumAudioStreams--;
   }
 
   if (hints & DOMMediaStream::HINT_CONTENTS_VIDEO) {
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -370,16 +370,28 @@ public:
   }
 
   NS_IMETHODIMP_TO_ERRORRESULT(RemoveStream, ErrorResult &rv,
                                DOMMediaStream& aMediaStream)
   {
     rv = RemoveStream(aMediaStream);
   }
 
+  NS_IMETHODIMP_TO_ERRORRESULT(AddTrack, ErrorResult &rv,
+      mozilla::dom::MediaStreamTrack& aTrack,
+      const mozilla::dom::Sequence<mozilla::dom::OwningNonNull<DOMMediaStream>>& aStreams)
+  {
+    rv = AddTrack(aTrack, aStreams);
+  }
+
+  NS_IMETHODIMP_TO_ERRORRESULT(RemoveTrack, ErrorResult &rv,
+                               mozilla::dom::MediaStreamTrack& aTrack)
+  {
+    rv = RemoveTrack(aTrack);
+  }
 
   nsresult GetPeerIdentity(nsAString& peerIdentity)
   {
 #ifdef MOZILLA_INTERNAL_API
     if (mPeerIdentity) {
       peerIdentity = mPeerIdentity->ToString();
       return NS_OK;
     }
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.cpp
@@ -38,24 +38,36 @@ static const mozilla::TrackID TRACK_VIDE
  * FIX - Do we need to iterate over the tracks instead of taking these hints?
  */
 void
 LocalSourceStreamInfo::ExpectAudio(const mozilla::TrackID aID)
 {
   mAudioTracks.AppendElement(aID);
 }
 
+void
+LocalSourceStreamInfo::RemoveAudio(const mozilla::TrackID aID)
+{
+  mAudioTracks.RemoveElement(aID);
+}
+
 // If the ExpectVideo hint is on we will add a track at the default first
 // video track ID (1).
 void
 LocalSourceStreamInfo::ExpectVideo(const mozilla::TrackID aID)
 {
   mVideoTracks.AppendElement(aID);
 }
 
+void
+LocalSourceStreamInfo::RemoveVideo(const mozilla::TrackID aID)
+{
+  mVideoTracks.RemoveElement(aID);
+}
+
 unsigned
 LocalSourceStreamInfo::AudioTrackCount()
 {
   return mAudioTracks.Length();
 }
 
 unsigned
 LocalSourceStreamInfo::VideoTrackCount()
@@ -229,90 +241,107 @@ nsresult PeerConnectionMedia::Init(const
   // figure out how to report that StartGathering failed. Bug 827982.
   RUN_ON_THREAD(mIceCtx->thread(),
                 WrapRunnable(mIceCtx, &NrIceCtx::StartGathering), NS_DISPATCH_NORMAL);
 
   return NS_OK;
 }
 
 nsresult
-PeerConnectionMedia::AddStream(nsIDOMMediaStream* aMediaStream, uint32_t *stream_id)
+PeerConnectionMedia::AddStream(nsIDOMMediaStream* aMediaStream,
+                               uint32_t hints,
+                               uint32_t *stream_id)
 {
   ASSERT_ON_THREAD(mMainThread);
 
   if (!aMediaStream) {
     CSFLogError(logTag, "%s - aMediaStream is NULL", __FUNCTION__);
     return NS_ERROR_FAILURE;
   }
 
   DOMMediaStream* stream = static_cast<DOMMediaStream*>(aMediaStream);
 
-  CSFLogDebug(logTag, "%s: MediaStream: %p",
-    __FUNCTION__, aMediaStream);
+  CSFLogDebug(logTag, "%s: MediaStream: %p", __FUNCTION__, aMediaStream);
 
   // Adding tracks here based on nsDOMMediaStream expectation settings
-  uint32_t hints = stream->GetHintContents();
 #ifdef MOZILLA_INTERNAL_API
   if (!Preferences::GetBool("media.peerconnection.video.enabled", true)) {
     hints &= ~(DOMMediaStream::HINT_CONTENTS_VIDEO);
   }
 #endif
 
   if (!(hints & (DOMMediaStream::HINT_CONTENTS_AUDIO |
         DOMMediaStream::HINT_CONTENTS_VIDEO))) {
     CSFLogDebug(logTag, "Empty Stream !!");
     return NS_OK;
   }
 
-  // Now see if we already have a stream of this type, since we only
-  // allow one of each.
+  // Now see if we already have this stream or another stream with
+  // tracks of the same type, since we only allow one track of each type.
   // TODO(ekr@rtfm.com): remove this when multiple of each stream
   // is allowed
+  nsRefPtr<LocalSourceStreamInfo> localSourceStream = nullptr;
+
   for (uint32_t u = 0; u < mLocalSourceStreams.Length(); u++) {
-    nsRefPtr<LocalSourceStreamInfo> localSourceStream = mLocalSourceStreams[u];
-
-    if (localSourceStream->GetMediaStream()->GetHintContents() & hints) {
+    auto& lss = mLocalSourceStreams[u];
+    if (((hints & DOMMediaStream::HINT_CONTENTS_AUDIO) && lss->AudioTrackCount()) ||
+        ((hints & DOMMediaStream::HINT_CONTENTS_VIDEO) && lss->VideoTrackCount())) {
       CSFLogError(logTag, "Only one stream of any given type allowed");
       return NS_ERROR_FAILURE;
     }
+    if (stream == lss->GetMediaStream()) {
+      localSourceStream = lss;
+      *stream_id = u;
+      break;
+    }
   }
-
-  // OK, we're good to add
-  nsRefPtr<LocalSourceStreamInfo> localSourceStream =
-      new LocalSourceStreamInfo(stream, this);
-  *stream_id = mLocalSourceStreams.Length();
+  if (!localSourceStream) {
+    localSourceStream = new LocalSourceStreamInfo(stream, this);
+    mLocalSourceStreams.AppendElement(localSourceStream);
+    *stream_id = mLocalSourceStreams.Length() - 1;
+  }
 
   if (hints & DOMMediaStream::HINT_CONTENTS_AUDIO) {
     localSourceStream->ExpectAudio(TRACK_AUDIO);
   }
 
   if (hints & DOMMediaStream::HINT_CONTENTS_VIDEO) {
     localSourceStream->ExpectVideo(TRACK_VIDEO);
   }
-
-  mLocalSourceStreams.AppendElement(localSourceStream);
-
   return NS_OK;
 }
 
 nsresult
-PeerConnectionMedia::RemoveStream(nsIDOMMediaStream* aMediaStream, uint32_t *stream_id)
+PeerConnectionMedia::RemoveStream(nsIDOMMediaStream* aMediaStream,
+                                  uint32_t hints,
+                                  uint32_t *stream_id)
 {
   MOZ_ASSERT(aMediaStream);
   ASSERT_ON_THREAD(mMainThread);
 
   DOMMediaStream* stream = static_cast<DOMMediaStream*>(aMediaStream);
 
   CSFLogDebug(logTag, "%s: MediaStream: %p",
     __FUNCTION__, aMediaStream);
 
   for (uint32_t u = 0; u < mLocalSourceStreams.Length(); u++) {
     nsRefPtr<LocalSourceStreamInfo> localSourceStream = mLocalSourceStreams[u];
     if (localSourceStream->GetMediaStream() == stream) {
       *stream_id = u;
+
+      if (hints & DOMMediaStream::HINT_CONTENTS_AUDIO) {
+        localSourceStream->RemoveAudio(TRACK_AUDIO);
+      }
+      if (hints & DOMMediaStream::HINT_CONTENTS_VIDEO) {
+        localSourceStream->RemoveAudio(TRACK_VIDEO);
+      }
+      if (!(localSourceStream->AudioTrackCount() +
+            localSourceStream->VideoTrackCount())) {
+        mLocalSourceStreams.RemoveElementAt(u);
+      }
       return NS_OK;
     }
   }
 
   return NS_ERROR_ILLEGAL_VALUE;
 }
 
 void
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionMedia.h
@@ -221,16 +221,18 @@ public:
                      mozilla::RefPtr<mozilla::MediaPipelineTransmit> aPipeline);
 
 #ifdef MOZILLA_INTERNAL_API
   void UpdateSinkIdentity_m(nsIPrincipal* aPrincipal, const PeerIdentity* aSinkIdentity);
 #endif
 
   void ExpectAudio(const mozilla::TrackID);
   void ExpectVideo(const mozilla::TrackID);
+  void RemoveAudio(const mozilla::TrackID);
+  void RemoveVideo(const mozilla::TrackID);
   unsigned AudioTrackCount();
   unsigned VideoTrackCount();
   void DetachTransport_s();
   void DetachMedia_m();
 
   bool AnyCodecHasPluginID(uint64_t aPluginID);
 
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(LocalSourceStreamInfo)
@@ -296,20 +298,23 @@ class PeerConnectionMedia : public sigsl
     return mIceStreams[i];
   }
 
   size_t num_ice_media_streams() const {
     return mIceStreams.size();
   }
 
   // Add a stream (main thread only)
-  nsresult AddStream(nsIDOMMediaStream* aMediaStream, uint32_t *stream_id);
+  nsresult AddStream(nsIDOMMediaStream* aMediaStream, uint32_t hints,
+                     uint32_t *stream_id);
 
   // Remove a stream (main thread only)
-  nsresult RemoveStream(nsIDOMMediaStream* aMediaStream, uint32_t *stream_id);
+  nsresult RemoveStream(nsIDOMMediaStream* aMediaStream,
+                        uint32_t hints,
+                        uint32_t *stream_id);
 
   // Get a specific local stream
   uint32_t LocalStreamsLength()
   {
     return mLocalSourceStreams.Length();
   }
   LocalSourceStreamInfo* GetLocalStream(int index);