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 496616 2fe9ae64504697968b89d5ec4ef0d027a0dc46fa
parent 496615 94ccc53c61a75edd4c218d70ab37b69e25b452df
child 496617 defaf4df79d35b6377589bfa8be43420880eafd1
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1258143, 791330
milestone64.0a1
backs out514420f15a67df92c218191b23a3b4bcd4b7ea95
905c871bcf0366ad037e4409ce21afe2a2fba440
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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.');