Backed out 2 changesets (bug 1258143) for dom/media/tests/crashtests/791330.html failures CLOSED TREE
authorBogdan Tara <btara@mozilla.com>
Fri, 12 Oct 2018 17:42:26 +0300
changeset 489323 2fe9ae64504697968b89d5ec4ef0d027a0dc46fa
parent 489322 94ccc53c61a75edd4c218d70ab37b69e25b452df
child 489324 defaf4df79d35b6377589bfa8be43420880eafd1
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
bugs1258143, 791330
milestone64.0a1
backs out514420f15a67df92c218191b23a3b4bcd4b7ea95
905c871bcf0366ad037e4409ce21afe2a2fba440
Backed out 2 changesets (bug 1258143) for dom/media/tests/crashtests/791330.html failures CLOSED TREE Backed out changeset 514420f15a67 (bug 1258143) Backed out changeset 905c871bcf03 (bug 1258143)
dom/bindings/Bindings.conf
dom/media/DOMMediaStream.cpp
dom/media/DOMMediaStream.h
dom/media/MediaManager.cpp
dom/media/MediaStreamGraph.h
dom/media/encoder/ContainerWriter.h
dom/media/nsIDOMNavigatorUserMedia.idl
dom/media/test/test_mediarecorder_avoid_recursion.html
dom/media/test/test_mediatrack_consuming_mediastream.html
dom/media/test/test_mediatrack_events.html
dom/media/tests/mochitest/mediaStreamPlayback.js
dom/media/tests/mochitest/mochitest.ini
dom/media/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html
dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentIframes.html
dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentStreams.html
dom/media/tests/mochitest/test_getUserMedia_basicAudio.html
dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
dom/media/tests/mochitest/test_getUserMedia_basicVideo.html
dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html
dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html
dom/media/tests/mochitest/test_getUserMedia_callbacks.html
dom/media/tests/mochitest/test_getUserMedia_cubebDisabledFakeStreams.html
dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html
dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html
dom/media/tests/mochitest/test_getUserMedia_mediaStreamClone.html
dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html
dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html
dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html
dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html
dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html
dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html
dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html
dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html
dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html
dom/tests/mochitest/general/test_interfaces.js
dom/webidl/LocalMediaStream.webidl
dom/webidl/moz.build
media/webrtc/signaling/test/FakeMediaStreams.h
mobile/android/tests/browser/robocop/robocop_getusermedia.html
mobile/android/tests/browser/robocop/robocop_getusermedia2.html
testing/web-platform/tests/html/dom/interfaces.https.html
testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -502,16 +502,21 @@ DOMInterfaces = {
     'concrete': False
 },
 
 'LegacyMozTCPSocket': {
     'headerFile': 'TCPSocket.h',
     'wrapperCache': False,
 },
 
+'LocalMediaStream': {
+    'headerFile': 'DOMMediaStream.h',
+    'nativeType': 'mozilla::DOMLocalMediaStream'
+},
+
 'MatchGlob': {
     'nativeType': 'mozilla::extensions::MatchGlob',
 },
 
 'MatchPattern': {
     'nativeType': 'mozilla::extensions::MatchPattern',
 },
 
--- a/dom/media/DOMMediaStream.cpp
+++ b/dom/media/DOMMediaStream.cpp
@@ -13,16 +13,17 @@
 #include "MediaStreamGraphImpl.h"
 #include "MediaStreamListener.h"
 #include "VideoStreamTrack.h"
 #include "mozilla/dom/AudioNode.h"
 #include "mozilla/dom/AudioTrack.h"
 #include "mozilla/dom/AudioTrackList.h"
 #include "mozilla/dom/DocGroup.h"
 #include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/LocalMediaStreamBinding.h"
 #include "mozilla/dom/MediaStreamBinding.h"
 #include "mozilla/dom/MediaStreamTrackEvent.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/VideoTrack.h"
 #include "mozilla/dom/VideoTrackList.h"
 #include "mozilla/media/MediaUtils.h"
 #include "nsContentUtils.h"
 #include "nsIScriptError.h"
@@ -398,16 +399,23 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_ADDREF_INHERITED(DOMMediaStream, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(DOMMediaStream, DOMEventTargetHelper)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMMediaStream)
   NS_INTERFACE_MAP_ENTRY(DOMMediaStream)
 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
 
+NS_IMPL_ADDREF_INHERITED(DOMLocalMediaStream, DOMMediaStream)
+NS_IMPL_RELEASE_INHERITED(DOMLocalMediaStream, DOMMediaStream)
+
+NS_INTERFACE_MAP_BEGIN(DOMLocalMediaStream)
+  NS_INTERFACE_MAP_ENTRY(DOMLocalMediaStream)
+NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream)
+
 NS_IMPL_CYCLE_COLLECTION_INHERITED(DOMAudioNodeMediaStream, DOMMediaStream,
                                    mStreamNode)
 
 NS_IMPL_ADDREF_INHERITED(DOMAudioNodeMediaStream, DOMMediaStream)
 NS_IMPL_RELEASE_INHERITED(DOMAudioNodeMediaStream, DOMMediaStream)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMAudioNodeMediaStream)
 NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream)
@@ -1520,16 +1528,75 @@ DOMMediaStream::NotifyPlaybackTrackBlock
     // waiting for any further tracks to get blocked. It is now safe to
     // recompute the principal based on our main thread track set state.
     LOG(LogLevel::Debug, ("DOMMediaStream %p saw all tracks pending removal "
                           "finish. Recomputing principal.", this));
     RecomputePrincipal();
   }
 }
 
+DOMLocalMediaStream::~DOMLocalMediaStream()
+{
+  if (mInputStream) {
+    // Make sure Listeners of this stream know it's going away
+    StopImpl();
+  }
+}
+
+JSObject*
+DOMLocalMediaStream::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+  return dom::LocalMediaStream_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+DOMLocalMediaStream::Stop()
+{
+  LOG(LogLevel::Debug, ("DOMMediaStream %p Stop()", this));
+  nsCOMPtr<nsPIDOMWindowInner> pWindow = GetParentObject();
+  nsIDocument* document = pWindow ? pWindow->GetExtantDoc() : nullptr;
+  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
+                                  NS_LITERAL_CSTRING("Media"),
+                                  document,
+                                  nsContentUtils::eDOM_PROPERTIES,
+                                  "MediaStreamStopDeprecatedWarning");
+
+  StopImpl();
+}
+
+void
+DOMLocalMediaStream::StopImpl()
+{
+  if (mInputStream && mInputStream->AsSourceStream()) {
+    mInputStream->AsSourceStream()->EndAllTrackAndFinish();
+  }
+}
+
+already_AddRefed<DOMLocalMediaStream>
+DOMLocalMediaStream::CreateSourceStreamAsInput(nsPIDOMWindowInner* aWindow,
+                                               MediaStreamGraph* aGraph,
+                                               MediaStreamTrackSourceGetter* aTrackSourceGetter)
+{
+  RefPtr<DOMLocalMediaStream> stream =
+    new DOMLocalMediaStream(aWindow, aTrackSourceGetter);
+  stream->InitSourceStream(aGraph);
+  return stream.forget();
+}
+
+already_AddRefed<DOMLocalMediaStream>
+DOMLocalMediaStream::CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
+                                                   MediaStreamGraph* aGraph,
+                                                   MediaStreamTrackSourceGetter* aTrackSourceGetter)
+{
+  RefPtr<DOMLocalMediaStream> stream =
+    new DOMLocalMediaStream(aWindow, aTrackSourceGetter);
+  stream->InitTrackUnionStream(aGraph);
+  return stream.forget();
+}
+
 DOMAudioNodeMediaStream::DOMAudioNodeMediaStream(nsPIDOMWindowInner* aWindow, AudioNode* aNode)
   : DOMMediaStream(aWindow, nullptr),
     mStreamNode(aNode)
 {
 }
 
 DOMAudioNodeMediaStream::~DOMAudioNodeMediaStream()
 {
--- a/dom/media/DOMMediaStream.h
+++ b/dom/media/DOMMediaStream.h
@@ -23,16 +23,17 @@
 // See dom/media/webaudio/AudioContext.h for more fun!
 #ifdef CurrentTime
 #undef CurrentTime
 #endif
 
 namespace mozilla {
 
 class AbstractThread;
+class DOMLocalMediaStream;
 class DOMMediaStream;
 class MediaStream;
 class MediaInputPort;
 class MediaStreamGraph;
 class ProcessedMediaStream;
 
 enum class BlockingMode;
 
@@ -200,16 +201,17 @@ protected:
  *                   \                                 (pointing to t1 in A')
  *                    ----> t2 ------------> t2     <- MediaStreamTrack Z'
  *                                                     (pointing to t2 in A')
  */
 class DOMMediaStream : public DOMEventTargetHelper,
                        public dom::PrincipalChangeObserver<dom::MediaStreamTrack>,
                        public RelativeTimeline
 {
+  friend class DOMLocalMediaStream;
   friend class dom::MediaStreamTrack;
   typedef dom::MediaStreamTrack MediaStreamTrack;
   typedef dom::AudioStreamTrack AudioStreamTrack;
   typedef dom::VideoStreamTrack VideoStreamTrack;
   typedef dom::MediaStreamTrackSource MediaStreamTrackSource;
   typedef dom::AudioTrack AudioTrack;
   typedef dom::VideoTrack VideoTrack;
   typedef dom::AudioTrackList AudioTrackList;
@@ -749,16 +751,59 @@ private:
   nsCOMPtr<nsIPrincipal> mVideoPrincipal;
   nsTArray<dom::PrincipalChangeObserver<DOMMediaStream>*> mPrincipalChangeObservers;
   CORSMode mCORSMode;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(DOMMediaStream,
                               NS_DOMMEDIASTREAM_IID)
 
+#define NS_DOMLOCALMEDIASTREAM_IID \
+{ 0xb1437260, 0xec61, 0x4dfa, \
+  { 0x92, 0x54, 0x04, 0x44, 0xe2, 0xb5, 0x94, 0x9c } }
+
+class DOMLocalMediaStream : public DOMMediaStream
+{
+public:
+  explicit DOMLocalMediaStream(nsPIDOMWindowInner* aWindow,
+                               MediaStreamTrackSourceGetter* aTrackSourceGetter)
+    : DOMMediaStream(aWindow, aTrackSourceGetter) {}
+
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOMLOCALMEDIASTREAM_IID)
+
+  virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+  void Stop();
+
+  /**
+   * Create an nsDOMLocalMediaStream whose underlying stream is a SourceMediaStream.
+   */
+  static already_AddRefed<DOMLocalMediaStream>
+  CreateSourceStreamAsInput(nsPIDOMWindowInner* aWindow,
+                            MediaStreamGraph* aGraph,
+                            MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
+
+  /**
+   * Create an nsDOMLocalMediaStream whose underlying stream is a TrackUnionStream.
+   */
+  static already_AddRefed<DOMLocalMediaStream>
+  CreateTrackUnionStreamAsInput(nsPIDOMWindowInner* aWindow,
+                                MediaStreamGraph* aGraph,
+                                MediaStreamTrackSourceGetter* aTrackSourceGetter = nullptr);
+
+protected:
+  virtual ~DOMLocalMediaStream();
+
+  void StopImpl();
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(DOMLocalMediaStream,
+                              NS_DOMLOCALMEDIASTREAM_IID)
+
 class DOMAudioNodeMediaStream : public DOMMediaStream
 {
   typedef dom::AudioNode AudioNode;
 public:
   DOMAudioNodeMediaStream(nsPIDOMWindowInner* aWindow, AudioNode* aNode);
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DOMAudioNodeMediaStream, DOMMediaStream)
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -1515,18 +1515,18 @@ public:
         principal = window->GetExtantDoc()->NodePrincipal();
       }
 
       // Normal case, connect the source stream to the track union stream to
       // avoid us blocking. Pass a simple TrackSourceGetter for potential
       // fake tracks. Apart from them gUM never adds tracks dynamically.
       domStream = new nsMainThreadPtrHolder<DOMMediaStream>(
         "GetUserMediaStreamRunnable::DOMMediaStreamMainThreadHolder",
-        DOMMediaStream::CreateSourceStreamAsInput(window, msg,
-                                                  new FakeTrackSourceGetter(principal)));
+        DOMLocalMediaStream::CreateSourceStreamAsInput(window, msg,
+                                                       new FakeTrackSourceGetter(principal)));
       stream = domStream->GetInputStream()->AsSourceStream();
 
       if (mAudioDevice) {
         nsString audioDeviceName;
         mAudioDevice->GetName(audioDeviceName);
         const MediaSourceEnum source = mAudioDevice->GetMediaSource();
         RefPtr<MediaStreamTrackSource> audioSource =
           new LocalTrackSource(principal, audioDeviceName, mSourceListener,
--- a/dom/media/MediaStreamGraph.h
+++ b/dom/media/MediaStreamGraph.h
@@ -820,18 +820,18 @@ public:
                       MediaSegment* aRawSegment = nullptr) override {
     mMutex.AssertCurrentThreadOwns();
     MediaStream::ApplyTrackDisabling(aTrackID, aSegment, aRawSegment);
   }
 
   void RemoveAllDirectListenersImpl() override;
 
   /**
-   * End all tracks and Finish() this stream. Used to voluntarily revoke access
-   * to a MediaStream.
+   * End all tracks and Finish() this stream.  Used to voluntarily revoke access
+   * to a LocalMediaStream.
    */
   void EndAllTrackAndFinish();
 
   void RegisterForAudioMixing();
 
   /**
    * Returns true if this SourceMediaStream contains at least one audio track
    * that is in pending state.
--- a/dom/media/encoder/ContainerWriter.h
+++ b/dom/media/encoder/ContainerWriter.h
@@ -16,17 +16,17 @@ namespace mozilla {
  */
 class ContainerWriter {
 public:
   ContainerWriter()
     : mInitialized(false)
     , mIsWritingComplete(false)
   {}
   virtual ~ContainerWriter() {}
-  // Mapping to DOMMediaStream::TrackTypeHints
+  // Mapping to DOMLocalMediaStream::TrackTypeHints
   enum {
     CREATE_AUDIO_TRACK = 1 << 0,
     CREATE_VIDEO_TRACK = 1 << 1,
   };
   enum {
     END_OF_STREAM = 1 << 0
   };
 
--- a/dom/media/nsIDOMNavigatorUserMedia.idl
+++ b/dom/media/nsIDOMNavigatorUserMedia.idl
@@ -22,17 +22,17 @@ interface nsIGetUserMediaDevicesSuccessC
   void onSuccess(in nsIVariant devices);
 };
 
 [scriptable, function, uuid(f2a144fc-3534-4761-8c5d-989ae720f89a)]
 interface nsIDOMGetUserMediaSuccessCallback : nsISupports
 {
   /*
    * value must be a Blob if picture is true and a
-   * DOMMediaStream if either audio or video are true.
+   * DOMLocalMediaStream if either audio or video are true.
    */
   void onSuccess(in nsISupports value);
 };
 
 [scriptable, function, uuid(39e96c61-2636-4f0e-918e-9bb64276492a)]
 interface nsIDOMGetUserMediaErrorCallback : nsISupports
 {
   void onError(in nsISupports error);
--- a/dom/media/test/test_mediarecorder_avoid_recursion.html
+++ b/dom/media/test/test_mediarecorder_avoid_recursion.html
@@ -25,18 +25,18 @@ async function startTest() {
       ok(false, 'Unexpected onwarning callback fired');
     };
     mediaRecorder.ondataavailable = function (e) {
       ++count;
       info("got ondataavailable data size = " + e.data.size);
       // no more requestData() to prevent busy main thread from starving
       // the encoding thread
       if (count == 30) {
-        info("track.stop");
-        stream.getTracks()[0].stop();
+        info("stream.stop");
+        stream.stop();
       } else if (count < 30 && mediaRecorder.state == 'recording') {
         info("requestData again");
         mediaRecorder.requestData();
       }
     };
     mediaRecorder.onstop = function () {
       ok(true, "requestData within ondataavailable successfully avoided infinite recursion");
       SimpleTest.finish();
--- a/dom/media/test/test_mediatrack_consuming_mediastream.html
+++ b/dom/media/test/test_mediatrack_consuming_mediastream.html
@@ -126,17 +126,17 @@ async function startTest() {
       element.pause();
       checkTrackAdded();
     } else if (steps == 2) {
       element.onpause = onpause;
       element.pause();
       checkTrackChanged(1, false);
     } else if (steps == 3) {
       checkTrackChanged(2, true);
-      stream.getTracks().forEach(t => t.stop());
+      stream.stop();
     }
   }
 
   element.onplaying = onplaying;
   element.srcObject = stream;
 
   steps++;
   await element.play();
--- a/dom/media/test/test_mediatrack_events.html
+++ b/dom/media/test/test_mediatrack_events.html
@@ -100,17 +100,17 @@ async function startTest() {
     element.onplaying = null;
     if (element.ended) {
       return;
     }
     if (steps == 1) {
       element.onpause = onpause;
       element.pause();
     } else if (steps == 2) {
-      stream.getTracks().forEach(t => t.stop());
+      stream.stop();
     }
   }
 
   element.onplaying = onplaying;
   element.srcObject = stream;
 
   isnot(element.audioTracks, undefined,
         'HTMLMediaElement::AudioTracks() property should be available.');
--- a/dom/media/tests/mochitest/mediaStreamPlayback.js
+++ b/dom/media/tests/mochitest/mediaStreamPlayback.js
@@ -138,16 +138,86 @@ MediaStreamPlayback.prototype = {
    *               being played.
    */
   detachFromMediaElement : function() {
     this.mediaElement.pause();
     this.mediaElement.srcObject = null;
   }
 }
 
+
+/**
+ * This class is basically the same as MediaStreamPlayback except
+ * ensures that the instance provided startMedia is a MediaStream.
+ *
+ * @param {HTMLMediaElement} mediaElement the media element for playback
+ * @param {LocalMediaStream} mediaStream the media stream used in
+ *                                       the mediaElement for playback
+ */
+function LocalMediaStreamPlayback(mediaElement, mediaStream) {
+  ok(mediaStream instanceof LocalMediaStream,
+     "Stream should be a LocalMediaStream");
+  MediaStreamPlayback.call(this, mediaElement, mediaStream);
+}
+
+LocalMediaStreamPlayback.prototype = Object.create(MediaStreamPlayback.prototype, {
+
+  /**
+   * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()!
+   *
+   * Starts media with a media stream, runs it until a canplaythrough and
+   * timeupdate event fires, and calls stop() on the stream.
+   *
+   * @param {Boolean} isResume specifies if this media element is being resumed
+   *                           from a previous run
+   */
+  playMediaWithDeprecatedStreamStop : {
+    value: function(isResume) {
+      this.startMedia(isResume);
+      return this.verifyPlaying()
+        .then(() => this.deprecatedStopStreamInMediaPlayback())
+        .then(() => this.detachFromMediaElement());
+    }
+  },
+
+  /**
+   * DEPRECATED - MediaStream.stop() is going away. Use MediaStreamTrack.stop()!
+   *
+   * Stops the local media stream while it's currently in playback in
+   * a media element.
+   *
+   * Precondition: The media stream and element should both be actively
+   *               being played.
+   *
+   */
+  deprecatedStopStreamInMediaPlayback : {
+    value: function () {
+      return new Promise((resolve, reject) => {
+        /**
+         * Callback fired when the ended event fires when stop() is called on the
+         * stream.
+         */
+        var endedCallback = () => {
+          this.mediaElement.removeEventListener('ended', endedCallback);
+          ok(true, "ended event successfully fired");
+          resolve();
+        };
+
+        this.mediaElement.addEventListener('ended', endedCallback);
+        this.mediaStream.stop();
+
+        // If ended doesn't fire in enough time, then we fail the test
+        setTimeout(() => {
+          reject(new Error("ended event never fired"));
+        }, ENDED_TIMEOUT_LENGTH);
+      });
+    }
+  }
+});
+
 // haxx to prevent SimpleTest from failing at window.onload
 function addLoadEvent() {}
 
 var scriptsReady = Promise.all([
   "/tests/SimpleTest/SimpleTest.js",
   "head.js"
 ].map(script  => {
   var el = document.createElement("script");
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -94,16 +94,22 @@ skip-if = android_version == '18' # andr
 [test_getUserMedia_mediaStreamTrackClone.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_getUserMedia_playAudioTwice.html]
 [test_getUserMedia_playVideoAudioTwice.html]
 [test_getUserMedia_playVideoTwice.html]
 [test_getUserMedia_scarySources.html]
 skip-if = toolkit == 'android' # no screenshare or windowshare on android
 [test_getUserMedia_spinEventLoop.html]
+[test_getUserMedia_stopAudioStream.html]
+[test_getUserMedia_stopAudioStreamWithFollowupAudio.html]
+[test_getUserMedia_stopVideoAudioStream.html]
+[test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html]
+[test_getUserMedia_stopVideoStream.html]
+[test_getUserMedia_stopVideoStreamWithFollowupVideo.html]
 [test_getUserMedia_trackCloneCleanup.html]
 [test_getUserMedia_trackEnded.html]
 [test_getUserMedia_peerIdentity.html]
 [test_peerConnection_addIceCandidate.html]
 [test_peerConnection_addtrack_removetrack_events.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_audioCodecs.html]
 skip-if = (android_version == '18') # android(Bug 1189784, timeouts on 4.3 emulator)
--- a/dom/media/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_addTrackRemoveTrack.html
@@ -22,33 +22,33 @@
 
         stream.addTrack(track);
         checkMediaStreamContains(stream, [track], "Re-added audio");
 
         stream.addTrack(otherTrack);
         checkMediaStreamContains(stream, [track, otherTrack], "Added video");
 
         var testElem = createMediaElement('video', 'testAddTrackAudioVideo');
-        var playback = new MediaStreamPlayback(testElem, stream);
+        var playback = new LocalMediaStreamPlayback(testElem, stream);
         return playback.playMedia(false);
     }))
     .then(() => getUserMedia({video: true})).then(stream =>
       getUserMedia({video: true}).then(otherStream => {
         info("Test addTrack()ing a video track to a video-only gUM stream");
         var track = stream.getTracks()[0];
         var otherTrack = otherStream.getTracks()[0];
 
         stream.addTrack(track);
         checkMediaStreamContains(stream, [track], "Re-added video");
 
         stream.addTrack(otherTrack);
         checkMediaStreamContains(stream, [track, otherTrack], "Added video");
 
         var test = createMediaElement('video', 'testAddTrackDoubleVideo');
-        var playback = new MediaStreamPlayback(test, stream);
+        var playback = new LocalMediaStreamPlayback(test, stream);
         return playback.playMedia(false);
     }))
     .then(() => getUserMedia({video: true})).then(stream =>
       getUserMedia({video: true}).then(otherStream => {
         info("Test removeTrack() existing and added video tracks from a video-only gUM stream");
         var track = stream.getTracks()[0];
         var otherTrack = otherStream.getTracks()[0];
 
@@ -75,17 +75,17 @@
         return wait(500).then(() => {
           ok(!loadeddata, "Stream without tracks shall not raise 'loadeddata' on media element");
           elem.pause();
           elem.srcObject = null;
         })
         .then(() => {
           stream.addTrack(track);
           checkMediaStreamContains(stream, [track], "Re-added added-then-removed track");
-          var playback = new MediaStreamPlayback(elem, stream);
+          var playback = new LocalMediaStreamPlayback(elem, stream);
           return playback.playMedia(false);
         })
         .then(() => otherTrack.stop());
     }))
     .then(() => getUserMedia({ audio: true })).then(audioStream =>
       getUserMedia({ video: true }).then(videoStream => {
         info("Test adding track and removing the original");
         var audioTrack = audioStream.getTracks()[0];
@@ -93,17 +93,17 @@
         videoStream.removeTrack(videoTrack);
         audioStream.addTrack(videoTrack);
 
         checkMediaStreamContains(videoStream, [], "1, Removed original track");
         checkMediaStreamContains(audioStream, [audioTrack, videoTrack],
                                  "2, Added external track");
 
         var elem = createMediaElement('video', 'testAddRemoveOriginalTrackVideo');
-        var playback = new MediaStreamPlayback(elem, audioStream);
+        var playback = new LocalMediaStreamPlayback(elem, audioStream);
         return playback.playMedia(false);
       }))
     .then(() => getUserMedia({ audio: true, video: true })).then(stream => {
       info("Test removing stopped tracks");
       stream.getTracks().forEach(t => {
         t.stop();
         stream.removeTrack(t);
       });
--- a/dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentIframes.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentIframes.html
@@ -129,17 +129,17 @@ runTest(async function() {
     }
 
     // We do not currently have tests to verify the behaviour of the different
     // constraints. Once we do we should do further verification here. See
     // bug 1406372, bug 1406376, and bug 1406377.
 
     for (let testCase of testCases) {
       let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
-      let playback = new MediaStreamPlayback(testAudio, testCase.gumStream);
+      let playback = new LocalMediaStreamPlayback(testAudio, testCase.gumStream);
       await playback.playMediaWithoutStoppingTracks(false);
     }
 
     // Stop the tracks for each stream, we left them running above via
     // playMediaWithoutStoppingTracks to make sure they can play concurrently.
     for (let testCase of testCases) {
       testCase.gumStream.getTracks().map(t => t.stop());
     }
--- a/dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentStreams.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_audioConstraints_concurrentStreams.html
@@ -102,17 +102,17 @@ runTest(async function() {
   }
 
   // We do not currently have tests to verify the behaviour of the different
   // constraints. Once we do we should do further verificaiton here. See
   // bug 1406372, bug 1406376, and bug 1406377.
 
   for (let testCase of testCases) {
     let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
-    let playback = new MediaStreamPlayback(testAudio, testCase.gumStream);
+    let playback = new LocalMediaStreamPlayback(testAudio, testCase.gumStream);
     await playback.playMediaWithoutStoppingTracks(false);
   }
 
   // Stop the tracks for each stream, we left them running above via
   // playMediaWithoutStoppingTracks to make sure they can play concurrently.
   for (let testCase of testCases) {
     testCase.gumStream.getTracks().map(t => t.stop());
   }
--- a/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicAudio.html
@@ -4,24 +4,24 @@
   <script type="application/javascript" src="mediaStreamPlayback.js"></script>
 </head>
 <body>
 <pre id="test">
 <script type="application/javascript">
   createHTML({ title: "getUserMedia Basic Audio Test", bug: "781534" });
   /**
    * Run a test to verify that we can complete a start and stop media playback
-   * cycle for an audio MediaStream on an audio HTMLMediaElement.
+   * cycle for an audio LocalMediaStream on an audio HTMLMediaElement.
    */
   runTest(function () {
     var testAudio = createMediaElement('audio', 'testAudio');
     var constraints = {audio: true};
 
     return getUserMedia(constraints).then(stream => {
-      var playback = new MediaStreamPlayback(testAudio, stream);
+      var playback = new LocalMediaStreamPlayback(testAudio, stream);
       return playback.playMedia(false);
     });
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicScreenshare.html
@@ -101,17 +101,17 @@
     let stream = await getUserMedia({video: {mediaSource: "screen"}});
     let settings = stream.getTracks()[0].getSettings();
     ok(settings.width <= 8192,
        `Width setting ${settings.width} should be set after gUM (or 0 per bug 1453247)`);
     ok(settings.height <= 8192,
        `Height setting ${settings.height} should be set after gUM (or 0 per bug 1453247)`);
     draw(helper.red, helper.blue,
          helper.green, helper.grey);
-    let playback = new MediaStreamPlayback(testVideo, stream);
+    let playback = new LocalMediaStreamPlayback(testVideo, stream);
     playback.startMedia();
     await playback.verifyPlaying();
     settings = stream.getTracks()[0].getSettings();
     is(settings.width, testVideo.videoWidth,
        "Width setting should match video width");
     is(settings.height, testVideo.videoHeight,
        "Height setting should match video height");
     let screenWidth = testVideo.videoWidth;
@@ -144,17 +144,17 @@
     });
     settings = stream.getTracks()[0].getSettings();
     ok(settings.width == 0 || (settings.width >= 10 && settings.width <= 100),
        `Width setting ${settings.width} should be correct after gUM (or 0 per bug 1453247)`);
     ok(settings.height == 0 || (settings.height >= 10 && settings.height <= 100),
        `Height setting ${settings.height} should be correct after gUM (or 0 per bug 1453247)`);
     draw(helper.green, helper.red,
          helper.grey, helper.blue);
-    playback = new MediaStreamPlayback(testVideo, stream);
+    playback = new LocalMediaStreamPlayback(testVideo, stream);
     playback.startMedia();
     await playback.verifyPlaying();
     settings = stream.getTracks()[0].getSettings();
     ok(settings.width >= 10 && settings.width <= 100,
        `Width setting ${settings.width} should be within constraints`);
     ok(settings.height >= 10 && settings.height <= 100,
        `Height setting ${settings.height} should be within constraints`);
     is(settings.width, testVideo.videoWidth,
@@ -201,17 +201,17 @@
     ok(Math.abs(expectedHeight - settings.height) <= 1,
        "Aspect ratio after applying constraints should be close enough");
     draw(helper.grey, helper.green,
          helper.blue, helper.red);
     await playback.verifyPlaying(); // still playing
     await verifyScreenshare(testVideo, helper,
                             helper.grey, helper.green,
                             helper.blue, helper.red);
-    await playback.stopTracksForStreamInMediaPlayback();
+    await playback.deprecatedStopStreamInMediaPlayback();
     playback.detachFromMediaElement();
 
     SpecialPowers.wrap(document).exitFullscreen();
   });
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicTabshare.html
@@ -7,60 +7,60 @@
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     title: "getUserMedia Basic Tabshare Test",
     bug: "1193075"
   });
   /**
    * Run a test to verify that we can complete a start and stop media playback
-   * cycle for a tabshare MediaStream on a video HTMLMediaElement.
+   * cycle for a tabshare LocalMediaStream on a video HTMLMediaElement.
    *
    * Additionally, exercise applyConstraints code for tabshare viewport offset.
    */
   runTest(function () {
     var testVideo = createMediaElement('video', 'testVideo');
 
     return Promise.resolve()
       .then(() => pushPrefs(["media.getusermedia.browser.enabled", true]))
       .then(() => getUserMedia({
         video: { mediaSource: "browser",
                  scrollWithPage: true },
         fake: false
       }))
       .then(stream => {
-        var playback = new MediaStreamPlayback(testVideo, stream);
-        return playback.playMedia(false);
+        var playback = new LocalMediaStreamPlayback(testVideo, stream);
+        return playback.playMediaWithDeprecatedStreamStop(false);
       })
       .then(() => getUserMedia({
         video: {
           mediaSource: "browser",
           viewportOffsetX: 0,
           viewportOffsetY: 0,
           viewportWidth: 100,
           viewportHeight: 100
         },
         fake: false
       }))
       .then(stream => {
-        var playback = new MediaStreamPlayback(testVideo, stream);
+        var playback = new LocalMediaStreamPlayback(testVideo, stream);
         playback.startMedia(false);
         return playback.verifyPlaying()
           .then(() => Promise.all([
             () => testVideo.srcObject.getVideoTracks()[0].applyConstraints({
               mediaSource: "browser",
               viewportOffsetX: 10,
               viewportOffsetY: 50,
               viewportWidth: 90,
               viewportHeight: 50
             }),
             () => listenUntil(testVideo, "resize", () => true)
           ]))
           .then(() => playback.verifyPlaying()) // still playing
-          .then(() => playback.stopTracksForStreamInMediaPlayback())
+          .then(() => playback.deprecatedStopStreamInMediaPlayback())
           .then(() => playback.detachFromMediaElement());
       });
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicVideo.html
@@ -7,24 +7,24 @@
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     title: "getUserMedia Basic Video Test",
     bug: "781534"
   });
   /**
    * Run a test to verify that we can complete a start and stop media playback
-   * cycle for an video MediaStream on a video HTMLMediaElement.
+   * cycle for an video LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
     var testVideo = createMediaElement('video', 'testVideo');
     var constraints = {video: true};
 
     return getUserMedia(constraints).then(stream => {
-      var playback = new MediaStreamPlayback(testVideo, stream);
+      var playback = new LocalMediaStreamPlayback(testVideo, stream);
       return playback.playMedia(false);
     });
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicVideoAudio.html
@@ -7,24 +7,24 @@
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     title: "getUserMedia Basic Video & Audio Test",
     bug: "781534"
   });
   /**
    * Run a test to verify that we can complete a start and stop media playback
-   * cycle for a video and audio MediaStream on a video HTMLMediaElement.
+   * cycle for a video and audio LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
     var testVideoAudio = createMediaElement('video', 'testVideoAudio');
     var constraints = {video: true, audio: true};
 
     return getUserMedia(constraints).then(stream => {
-      var playback = new MediaStreamPlayback(testVideoAudio, stream);
+      var playback = new LocalMediaStreamPlayback(testVideoAudio, stream);
       return playback.playMedia(false);
     });
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
@@ -14,17 +14,17 @@
    * Run a test to verify that we will always get 'loadedmetadata' from a video
    * HTMLMediaElement playing a gUM MediaStream.
    */
   runTest(() => {
     var testVideo = createMediaElement('video', 'testVideo');
     var constraints = {video: true};
 
     return getUserMedia(constraints).then(stream => {
-      var playback = new MediaStreamPlayback(testVideo, stream);
+      var playback = new LocalMediaStreamPlayback(testVideo, stream);
       var video = playback.mediaElement;
 
       video.srcObject = stream;
       return new Promise(resolve => {
         ok(playback.mediaElement.paused,
            "Media element should be paused before play()ing");
         video.addEventListener('loadedmetadata', function() {
           ok(video.videoWidth > 0, "Expected nonzero video width");
--- a/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_basicWindowshare.html
@@ -7,31 +7,31 @@
 <pre id="test">
 <script type="application/javascript">
   createHTML({
     title: "getUserMedia Basic Windowshare Test",
     bug: "1038926"
   });
   /**
    * Run a test to verify that we can complete a start and stop media playback
-   * cycle for an screenshare MediaStream on a video HTMLMediaElement.
+   * cycle for an screenshare LocalMediaStream on a video HTMLMediaElement.
    */
   runTest(function () {
     var testVideo = createMediaElement('video', 'testVideo');
     var constraints = {
       video: {
          mozMediaSource: "window",
          mediaSource: "window"
       },
       fake: false
     };
 
     return getUserMedia(constraints).then(stream => {
-      var playback = new MediaStreamPlayback(testVideo, stream);
-      return playback.playMedia(false);
+      var playback = new LocalMediaStreamPlayback(testVideo, stream);
+      return playback.playMediaWithDeprecatedStreamStop(false);
     });
 
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_callbacks.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_callbacks.html
@@ -17,17 +17,17 @@
     var testAudio = createMediaElement('audio', 'testAudio');
     var constraints = {audio: true};
 
     SimpleTest.waitForExplicitFinish();
     return new Promise(resolve =>
       navigator.mozGetUserMedia(constraints, stream => {
         checkMediaStreamTracks(constraints, stream);
 
-        var playback = new MediaStreamPlayback(testAudio, stream);
+        var playback = new LocalMediaStreamPlayback(testAudio, stream);
         return playback.playMedia(false)
           .then(() => resolve(), generateErrorCallback());
       }, generateErrorCallback())
     );
   });
 
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_getUserMedia_cubebDisabledFakeStreams.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_cubebDisabledFakeStreams.html
@@ -27,17 +27,17 @@
     try {
       stream = await getUserMedia(constraints);
     } catch (e) {
       // We've got no audio backend, so we expect gUM to fail
       ok(false, `Did not expect to fail, but got ${e}`);
       return;
     }
     ok(stream, "getUserMedia should get a stream!");
-    let playback = new MediaStreamPlayback(testAudio, stream);
+    let playback = new LocalMediaStreamPlayback(testAudio, stream);
     return playback.playMedia(false);
   });
 
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_gumWithinGum.html
@@ -11,28 +11,29 @@
    * Run a test that we can complete a playback cycle for a video,
    * then upon completion, do a playback cycle with audio, such that
    * the audio gum call happens within the video gum call.
    */
   runTest(function () {
     return getUserMedia({video: true})
       .then(videoStream => {
         var testVideo = createMediaElement('video', 'testVideo');
-        var videoPlayback = new MediaStreamPlayback(testVideo,
-                                                    videoStream);
+        var videoPlayback = new LocalMediaStreamPlayback(testVideo,
+                                                         videoStream);
 
-        return videoPlayback.playMediaWithoutStoppingTracks(false)
+        return videoPlayback.playMedia(false)
           .then(() => getUserMedia({audio: true}))
           .then(audioStream => {
             var testAudio = createMediaElement('audio', 'testAudio');
-            var audioPlayback = new MediaStreamPlayback(testAudio,
-                                                        audioStream);
+            var audioPlayback = new LocalMediaStreamPlayback(testAudio,
+                                                             audioStream);
 
-            return audioPlayback.playMedia(false);
+            return audioPlayback.playMedia(false)
+              .then(() => audioStream.stop());
           })
-          .then(() => videoStream.getTracks().forEach(t => t.stop()));
+          .then(() => videoStream.stop());
       });
   });
 
 </script>
 </pre>
 </body>
 </html>
--- a/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_mediaElementCapture_tracks.html
@@ -152,16 +152,18 @@ runTest(() => getUserMedia({audio: true,
     streamUntilEnded = untilEndedElement.mozCaptureStreamUntilEnded();
 
     is(streamUntilEnded.getAudioTracks().length, 3,
        "video element should capture all 3 audio tracks until ended");
     is(streamUntilEnded.getVideoTracks().length, 1,
        "video element should capture only 1 video track until ended");
 
     untilEndedElement.srcObject.getTracks().forEach(t => t.stop());
+    // TODO(1208316) We stop the stream to make the media element end.
+    untilEndedElement.srcObject.stop();
 
     return Promise.all([
       haveEvent(untilEndedElement, "ended", wait(50000, new Error("Timeout"))),
       ...streamUntilEnded.getTracks()
            .map(t => haveEvent(t, "ended", wait(50000, new Error("Timeout"))))
     ]);
   })
   .then(() => {
--- a/dom/media/tests/mochitest/test_getUserMedia_mediaStreamClone.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_mediaStreamClone.html
@@ -64,16 +64,17 @@ runTest(async () => {
     streamClone.addTrack(track);
     checkMediaStreamContains(streamClone, [trackClone, track],
                              "Added video to clone");
     checkMediaStreamContains(stream, [otherTrack],
                              "Original not affected");
 
     // Not part of streamClone. Does not get stopped by the playback test.
     otherTrack.stop();
+    otherStream.stop();
 
     let test = createMediaElement('video', 'testClonePlayback');
     let playback = new MediaStreamPlayback(test, streamClone);
     await playback.playMedia(false);
   }
 
   {
     info("Test cloning a stream into inception");
--- a/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_playAudioTwice.html
@@ -8,17 +8,17 @@
 <script type="application/javascript">
   createHTML({title: "getUserMedia Play Audio Twice", bug: "822109" });
   /**
    * Run a test that we can complete an audio playback cycle twice in a row.
    */
   runTest(function () {
     return getUserMedia({audio: true}).then(audioStream => {
       var testAudio = createMediaElement('audio', 'testAudio');
-      var playback = new MediaStreamPlayback(testAudio, audioStream);
+      var playback = new LocalMediaStreamPlayback(testAudio, audioStream);
 
       return playback.playMediaWithoutStoppingTracks(false)
         .then(() => playback.playMedia(true));
     });
   });
 </script>
 </pre>
 </body>
--- a/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_playVideoAudioTwice.html
@@ -8,17 +8,17 @@
 <script type="application/javascript">
   createHTML({title: "getUserMedia Play Video and Audio Twice", bug: "822109" });
   /**
    * Run a test that we can complete a video playback cycle twice in a row.
    */
   runTest(function () {
     return getUserMedia({video: true, audio: true}).then(stream => {
       var testVideo = createMediaElement('video', 'testVideo');
-      var playback = new MediaStreamPlayback(testVideo, stream);
+      var playback = new LocalMediaStreamPlayback(testVideo, stream);
 
       return playback.playMediaWithoutStoppingTracks(false)
         .then(() => playback.playMedia(true));
     });
   });
 
 </script>
 </pre>
--- a/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html
+++ b/dom/media/tests/mochitest/test_getUserMedia_playVideoTwice.html
@@ -8,17 +8,17 @@
 <script type="application/javascript">
   createHTML({ title: "getUserMedia Play Video Twice", bug: "822109" });
   /**
    * Run a test that we can complete a video playback cycle twice in a row.
    */
   runTest(function () {
     return getUserMedia({video: true}).then(stream => {
       var testVideo = createMediaElement('video', 'testVideo');
-      var streamPlayback = new MediaStreamPlayback(testVideo, stream);
+      var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
 
       return streamPlayback.playMediaWithoutStoppingTracks(false)
         .then(() => streamPlayback.playMedia(true));
     });
   });
 
 </script>
 </pre>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStream.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Audio Stream", bug: "822109" });
+  /**
+   * Run a test to verify that we can start an audio stream in a media element,
+   * call stop() on the stream, and successfully get an ended event fired.
+   */
+  runTest(function () {
+    return getUserMedia({audio: true})
+      .then(stream => {
+        var testAudio = createMediaElement('audio', 'testAudio');
+        var streamPlayback = new LocalMediaStreamPlayback(testAudio, stream);
+
+        return streamPlayback.playMediaWithDeprecatedStreamStop(false);
+      });
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopAudioStreamWithFollowupAudio.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Audio Stream With Followup Audio", bug: "822109" });
+  /**
+   * Run a test to verify that I can complete an audio gum playback in a media
+   * element, stop the stream, and then complete another audio gum playback
+   * in a media element.
+   */
+  runTest(function () {
+    return getUserMedia({audio: true})
+      .then(firstStream => {
+        var testAudio = createMediaElement('audio', 'testAudio');
+        var streamPlayback = new LocalMediaStreamPlayback(testAudio, firstStream);
+
+        return streamPlayback.playMediaWithDeprecatedStreamStop(false)
+          .then(() => getUserMedia({audio: true}))
+          .then(secondStream => {
+            streamPlayback.mediaStream = secondStream;
+
+            return streamPlayback.playMedia(false)
+              .then(() => secondStream.stop());
+          });
+      });
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStream.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Video Audio Stream", bug: "822109" });
+  /**
+   * Run a test to verify that we can start a video+audio stream in a
+   * media element, call stop() on the stream, and successfully get an
+   * ended event fired.
+   */
+  runTest(function () {
+    return getUserMedia({video: true, audio: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var playback = new LocalMediaStreamPlayback(testVideo, stream);
+
+        return playback.playMediaWithDeprecatedStreamStop(false);
+      });
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoAudioStreamWithFollowupVideoAudio.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({
+    title: "getUserMedia Stop Video+Audio Stream With Followup Video+Audio",
+    bug: "822109"
+  });
+  /**
+   * Run a test to verify that I can complete an video+audio gum playback in a
+   * media element, stop the stream, and then complete another video+audio gum
+   * playback in a media element.
+   */
+  runTest(function () {
+    return getUserMedia({video: true, audio: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+
+        return streamPlayback.playMediaWithDeprecatedStreamStop(false)
+          .then(() => getUserMedia({video: true, audio: true}))
+          .then(secondStream => {
+            streamPlayback.mediaStream = secondStream;
+
+            return streamPlayback.playMedia(false)
+              .then(() => secondStream.stop());
+          });
+      });
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStream.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Video Stream", bug: "822109" });
+  /**
+   * Run a test to verify that we can start a video stream in a
+   * media element, call stop() on the stream, and successfully get an
+   * ended event fired.
+   */
+  runTest(function () {
+    return getUserMedia({video: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+
+        return streamPlayback.playMediaWithDeprecatedStreamStop(false);
+      });
+  });
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_getUserMedia_stopVideoStreamWithFollowupVideo.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+  createHTML({ title: "getUserMedia Stop Video Stream With Followup Video", bug: "822109" });
+  /**
+   * Run a test to verify that I can complete an video gum playback in a
+   * media element, stop the stream, and then complete another video gum
+   * playback in a media element.
+   */
+  runTest(function () {
+    return getUserMedia({video: true})
+      .then(stream => {
+        var testVideo = createMediaElement('video', 'testVideo');
+        var streamPlayback = new LocalMediaStreamPlayback(testVideo, stream);
+
+        return streamPlayback.playMediaWithDeprecatedStreamStop(false)
+          .then(() => getUserMedia({video: true}))
+          .then(secondStream => {
+            streamPlayback.mediaStream = secondStream;
+
+            return streamPlayback.playMedia(false)
+              .then(() => secondStream.stop());
+          });
+      });
+  });
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_interfaces.js
+++ b/dom/tests/mochitest/general/test_interfaces.js
@@ -602,16 +602,18 @@ var interfaceNamesInGlobalScope =
     {name: "IntersectionObserverEntry", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyboardEvent", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffect", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    {name: "LocalMediaStream", insecureContext: true},
+// IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "Location", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "MediaCapabilities", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "MediaCapabilitiesInfo", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "MediaDeviceInfo", insecureContext: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
new file mode 100644
--- /dev/null
+++ b/dom/webidl/LocalMediaStream.webidl
@@ -0,0 +1,15 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origins of this IDL file are
+ * http://dev.w3.org/2011/webrtc/editor/getusermedia.html
+ *
+ * Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
+ * liability, trademark and document use rules apply.
+ */
+
+interface LocalMediaStream : MediaStream {
+    void stop();
+};
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -167,16 +167,19 @@ with Files("Key*Event*"):
     BUG_COMPONENT = ("Core", "DOM: Events")
 
 with Files("KeyIdsInitData.webidl"):
     BUG_COMPONENT = ("Core", "Audio/Video: Playback")
 
 with Files("Keyframe*"):
     BUG_COMPONENT = ("Core", "DOM: Animation")
 
+with Files("LocalMediaStream.webidl"):
+    BUG_COMPONENT = ("Core", "Audio/Video")
+
 with Files("MediaDevice*"):
     BUG_COMPONENT = ("Core", "WebRTC")
 
 with Files("Media*Source*"):
     BUG_COMPONENT = ("Core", "Web Audio")
 
 with Files("MediaStream*"):
     BUG_COMPONENT = ("Core", "WebRTC")
@@ -630,16 +633,17 @@ WEBIDL_FILES = [
     'KeyboardEvent.webidl',
     'KeyEvent.webidl',
     'KeyframeAnimationOptions.webidl',
     'KeyframeEffect.webidl',
     'KeyIdsInitData.webidl',
     'L10nUtils.webidl',
     'LegacyQueryInterface.webidl',
     'LinkStyle.webidl',
+    'LocalMediaStream.webidl',
     'Location.webidl',
     'MediaCapabilities.webidl',
     'MediaDeviceInfo.webidl',
     'MediaDevices.webidl',
     'MediaElementAudioSourceNode.webidl',
     'MediaEncryptedEvent.webidl',
     'MediaError.webidl',
     'MediaKeyError.webidl',
--- a/media/webrtc/signaling/test/FakeMediaStreams.h
+++ b/media/webrtc/signaling/test/FakeMediaStreams.h
@@ -487,16 +487,18 @@ public:
     Fake_SourceMediaStream *source = new Fake_SourceMediaStream();
 
     RefPtr<Fake_DOMMediaStream> ds = new Fake_DOMMediaStream(source);
     ds->SetHintContents(aHintContents);
 
     return ds.forget();
   }
 
+  virtual void Stop() {} // Really DOMLocalMediaStream
+
   virtual bool AddDirectListener(Fake_MediaStreamListener *aListener) { return false; }
   virtual void RemoveDirectListener(Fake_MediaStreamListener *aListener) {}
 
   Fake_MediaStream *GetInputStream() { return mMediaStream; }
   Fake_MediaStream *GetOwnedStream() { return mMediaStream; }
   Fake_MediaStream *GetPlaybackStream() { return mMediaStream; }
   Fake_MediaStream *GetStream() { return mMediaStream; }
   std::string GetId() const { return mID; }
@@ -638,16 +640,17 @@ class Fake_VideoStreamSource : public Fa
 
 namespace mozilla {
 typedef Fake_MediaStream MediaStream;
 typedef Fake_SourceMediaStream SourceMediaStream;
 typedef Fake_MediaStreamListener MediaStreamListener;
 typedef Fake_MediaStreamTrackListener MediaStreamTrackListener;
 typedef Fake_DirectMediaStreamTrackListener DirectMediaStreamTrackListener;
 typedef Fake_DOMMediaStream DOMMediaStream;
+typedef Fake_DOMMediaStream DOMLocalMediaStream;
 typedef Fake_MediaStreamVideoSink MediaStreamVideoSink;
 
 namespace dom {
 typedef Fake_MediaStreamTrack MediaStreamTrack;
 typedef Fake_MediaStreamTrackSource MediaStreamTrackSource;
 typedef Fake_MediaStreamTrack VideoStreamTrack;
 }
 }
--- a/mobile/android/tests/browser/robocop/robocop_getusermedia.html
+++ b/mobile/android/tests/browser/robocop/robocop_getusermedia.html
@@ -30,24 +30,24 @@
       },
       audio: true
     };
     startMedia(mediaConstraints);
   }
 
   function stopMedia() {
     if (video_status) {
-      video.srcObject.getTracks().forEach(t => t.stop());
+      video.srcObject.stop();
       video.srcObject = null;
       content.removeChild(video);
       capturing = false;
       video_status = false;
     }
     if (audio_status) {
-      audio.srcObject.getTracks().forEach(t => t.stop());
+      audio.srcObject.stop();
       audio.srcObject = null;
       content.removeChild(audio);
       audio_status = false;
     }
   }
 
   function startMedia(param) {
     try {
@@ -67,18 +67,18 @@
         document.title = "";
         if (audioTracks.length > 0) {
           document.title += "audio";
         }
         if (videoTracks.length > 0) {
           document.title += "video";
         }
         document.title += " gumtest";
-        audio.srcObject.getTracks().forEach(t => t.stop());
-        video.srcObject.getTracks().forEach(t => t.stop());
+        audio.srcObject.stop();
+        video.srcObject.stop();
       }, function(err) {
         document.title = "failed gumtest";
         stopMedia();
       });
     } catch (e) {
       stopMedia();
     }
   }
--- a/mobile/android/tests/browser/robocop/robocop_getusermedia2.html
+++ b/mobile/android/tests/browser/robocop/robocop_getusermedia2.html
@@ -27,24 +27,24 @@
       video: true,
       audio: true
     };
     startMedia(mediaConstraints);
   }
 
   function stopMedia() {
     if (video_status) {
-      video.srcObject.getTracks().forEach(t => t.stop());
+      video.srcObject.stop();
       video.srcObject = null;
       content.removeChild(video);
       capturing = false;
       video_status = false;
     }
     if (audio_status) {
-      audio.srcObject.getTracks().forEach(t => t.stop());
+      audio.srcObject.stop();
       audio.srcObject = null;
       content.removeChild(audio);
       audio_status = false;
     }
   }
 
   function startMedia(param) {
     try {
@@ -64,18 +64,18 @@
         document.title = "";
         if (audioTracks.length > 0) {
           document.title += "audio";
         }
         if (videoTracks.length > 0) {
           document.title += "video";
         }
         document.title += " gumtest";
-        audio.srcObject.getTracks().forEach(t => t.stop());
-        video.srcObject.getTracks().forEach(t => t.stop());
+        audio.srcObject.stop();
+        video.srcObject.stop();
       }, function(err) {
         document.title = "failed gumtest";
         stopMedia();
       });
     } catch (e) {
       stopMedia();
     }
   }
--- a/testing/web-platform/tests/html/dom/interfaces.https.html
+++ b/testing/web-platform/tests/html/dom/interfaces.https.html
@@ -189,16 +189,17 @@ idl_test(
       Navigator: ['window.navigator'],
       External: ['window.external'],
       DataTransfer: [],
       DataTransferItemList: [],
       DataTransferItem: [],
       DragEvent: [],
       NavigatorUserMediaError: [],
       MediaStream: [],
+      LocalMediaStream: [],
       MediaStreamTrack: [],
       MediaStreamRecorder: [],
       PeerConnection: [],
       MediaStreamEvent: [],
       ErrorEvent: [],
       WebSocket: ['new WebSocket("wss://foo")'],
       CloseEvent: ['new CloseEvent("close")'],
       AbstractWorker: [],
--- a/testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js
+++ b/testing/web-platform/tests/mediacapture-streams/idlharness.https.window.js
@@ -29,32 +29,32 @@ promise_test(async () => {
         media = item;
       default:
         assert_unreached(
           'media.kind should be one of "audioinput", "videoinput", or "audiooutput".');
       }
     }
   } catch (e) {}
 
-  let stream, track, trackEvent;
+  let track, trackEvent;
   try {
-    stream = await navigator.mediaDevices.getUserMedia({audio: true});
+    const stream = await navigator.mediaDevices.getUserMedia({audio: true});
     track = stream.getTracks()[0];
     trackEvent = new MediaStreamTrackEvent("type", {
       track: track,
     });
   } catch (e) { throw e}
 
   if (input) {
     idl_array.add_objects({ InputDeviceInfo: [input] });
   } else {
     idl_array.add_objects({ MediaDeviceInfo: [media] });
   }
   idl_array.add_objects({
-    MediaStream: [stream, 'new MediaStream()'],
+    MediaStream: ['new MediaStream()'],
     Navigator: ['navigator'],
     MediaDevices: ['navigator.mediaDevices'],
     MediaStreamTrack: [track],
     MediaStreamTrackEvent: [trackEvent],
     OverconstrainedErrorEvent: ['new OverconstrainedErrorEvent("type", {})'],
   });
   idl_array.test();
 }, 'mediacapture-streams interfaces.');