Bug 1299515 - Stop Camera and Microphone device when tracks become disabled. r=jib
authorAndreas Pehrson <pehrsons@mozilla.com>
Fri, 17 Nov 2017 19:56:00 +0100
changeset 401735 ae1d261df7881bfb980ec4b574badbf63412b8b9
parent 401734 7989fd5fee8ac0188bedbafd7a96950000224846
child 401736 42743183facc80469128a4bb7eeceb24dbc41b72
push id33353
push userapavel@mozilla.com
push dateWed, 31 Jan 2018 17:38:48 +0000
treeherdermozilla-central@205707b678d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjib
bugs1299515
milestone60.0a1
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
Bug 1299515 - Stop Camera and Microphone device when tracks become disabled. r=jib This wires up the disabling of a track with actually stopping the device if we allow it. This is possible for: - Camera (enabled by default, controlled by pref "media.getusermedia.camera.off_while_disabled.enabled") - Microphone (disabled by default, controlled by pref "media.getusermedia.microphone.off_while_disabled.enabled") Screen-, app-, or windowsharing is not supported at this time. On disabling, there's a delay before the device is ordered to stop. This is now defaulting to 3 seconds but can be overriden by prefs "media.getusermedia.camera.off_while_disabled.delay_ms" and "media.getusermedia.microphone.off_while_disabled.delay_ms". The delay is in place to prevent misuse by malicious sites. If a track is re-enabled before the delay has passed, the device will not be touched until another disable followed by the full delay happens. MozReview-Commit-ID: D4nZWzrYZGm
dom/media/MediaManager.cpp
dom/media/webrtc/MediaEngineDefault.cpp
dom/media/webrtc/MediaEngineDefault.h
dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
dom/media/webrtc/MediaEngineRemoteVideoSource.h
dom/media/webrtc/MediaEngineSource.h
dom/media/webrtc/MediaEngineTabVideoSource.h
dom/media/webrtc/MediaEngineWebRTC.h
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -3,16 +3,17 @@
 /* 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/. */
 
 #include "MediaManager.h"
 
 #include "AllocationHandle.h"
 #include "MediaStreamGraph.h"
+#include "MediaTimer.h"
 #include "mozilla/dom/MediaStreamTrack.h"
 #include "MediaStreamListener.h"
 #include "nsArray.h"
 #include "nsContentUtils.h"
 #include "nsGlobalWindow.h"
 #include "nsHashPropertyBag.h"
 #include "nsIEventTarget.h"
 #include "nsIUUIDGenerator.h"
@@ -146,26 +147,63 @@ using media::NewRunnableFrom;
 using media::NewTaskFrom;
 using media::Pledge;
 using media::Refcountable;
 
 static Atomic<bool> sHasShutdown;
 
 typedef media::Pledge<bool, dom::MediaStreamError*> PledgeVoid;
 
+struct DeviceState {
+  DeviceState(const RefPtr<MediaDevice>& aDevice, bool aOffWhileDisabled)
+    : mOffWhileDisabled(aOffWhileDisabled)
+    , mDisableTimer(new MediaTimer())
+    , mDevice(aDevice)
+  {
+    MOZ_ASSERT(mDevice);
+  }
+
+  // true if we have stopped mDevice, this is a terminal state.
+  // MainThread only.
+  bool mStopped = false;
+
+  // true if mDevice is currently enabled, i.e., turned on and capturing.
+  // MainThread only.
+  bool mDeviceEnabled = true;
+
+  // true if the application has currently enabled mDevice.
+  // MainThread only.
+  bool mTrackEnabled = true;
+
+  // true if an operation to Start() or Stop() mDevice has been dispatched to
+  // the media thread and is not finished yet.
+  // MainThread only.
+  bool mOperationInProgress = false;
+
+  // true if we are allowed to turn off the underlying source while all tracks
+  // are disabled.
+  // MainThread only.
+  bool mOffWhileDisabled = false;
+
+  // Timer triggered by a MediaStreamTrackSource signaling that all tracks got
+  // disabled. When the timer fires we initiate Stop()ing mDevice.
+  // If set we allow dynamically stopping and starting mDevice.
+  // Any thread.
+  const RefPtr<MediaTimer> mDisableTimer;
+
+  // The underlying device we keep state for. Always non-null.
+  // Threadsafe access, but see method declarations for individual constraints.
+  const RefPtr<MediaDevice> mDevice;
+};
+
 class SourceListener : public MediaStreamListener {
 public:
   SourceListener();
 
   /**
-   * Returns the current device for the given track.
-   */
-  MediaDevice* GetDevice(TrackID aTrackID) const;
-
-  /**
    * Registers this source listener as belonging to the given window listener.
    */
   void Register(GetUserMediaWindowListener* aListener);
 
   /**
    * Marks this listener as active and adds itself as a listener to aStream.
    */
   void Activate(SourceMediaStream* aStream,
@@ -186,49 +224,66 @@ public:
   /**
    * Posts a task to stop the device associated with aTrackID and notifies the
    * associated window listener that a track was stopped.
    * Should this track be the last live one to be stopped, we'll also clean up.
    */
   void StopTrack(TrackID aTrackID);
 
   /**
-   * Posts a task to disable the device associated with aTrackID and notifies
-   * the associated window listener that a track has been disabled.
+   * Gets the main thread MediaTrackSettings from the MediaEngineSource
+   * associated with aTrackID.
    */
-  void DisableTrack(TrackID aTrackID);
-
+  void GetSettingsFor(TrackID aTrackID, dom::MediaTrackSettings& aOutSettings) const;
 
   /**
-   * Posts a task to enable the device associated with aTrackID and notifies
-   * the associated window listener that a track has been enabled.
+   * Posts a task to set the enabled state of the device associated with
+   * aTrackID to aEnabled and notifies the associated window listener that a
+   * track's state has changed.
+   *
+   * Turning the hardware off while the device is disabled is supported for:
+   * - Camera (enabled by default, controlled by pref
+   *   "media.getusermedia.camera.off_while_disabled.enabled")
+   * - Microphone (disabled by default, controlled by pref
+   *   "media.getusermedia.microphone.off_while_disabled.enabled")
+   * Screen-, app-, or windowsharing is not supported at this time.
+   *
+   * The behavior is also different between disabling and enabling a device.
+   * While enabling is immediate, disabling only happens after a delay.
+   * This is now defaulting to 3 seconds but can be overriden by prefs:
+   * - "media.getusermedia.camera.off_while_disabled.delay_ms" and
+   * - "media.getusermedia.microphone.off_while_disabled.delay_ms".
+   *
+   * The delay is in place to prevent misuse by malicious sites. If a track is
+   * re-enabled before the delay has passed, the device will not be touched
+   * until another disable followed by the full delay happens.
    */
-  void EnableTrack(TrackID aTrackID);
+  void SetEnabledFor(TrackID aTrackID, bool aEnabled);
 
   /**
    * Stops all screen/app/window/audioCapture sharing, but not camera or
    * microphone.
    */
   void StopSharing();
 
   MediaStream* Stream() const
   {
     return mStream;
   }
 
   SourceMediaStream* GetSourceStream();
 
   MediaDevice* GetAudioDevice() const
   {
-    return mAudioDevice;
+    return mAudioDeviceState ? mAudioDeviceState->mDevice.get() : nullptr;
   }
 
   MediaDevice* GetVideoDevice() const
   {
-    return mVideoDevice;
+    return mVideoDeviceState ? mVideoDeviceState->mDevice.get() : nullptr;
   }
 
   void NotifyPull(MediaStreamGraph* aGraph,
                   StreamTime aDesiredTime) override;
 
   void NotifyEvent(MediaStreamGraph* aGraph,
                    MediaStreamGraphEvent aEvent) override;
 
@@ -270,48 +325,52 @@ public:
   ApplyConstraintsToTrack(nsPIDOMWindowInner* aWindow,
                           TrackID aTrackID,
                           const dom::MediaTrackConstraints& aConstraints,
                           dom::CallerType aCallerType);
 
   PrincipalHandle GetPrincipalHandle() const;
 
 private:
+  /**
+   * Returns a pointer to the device state for aTrackID.
+   *
+   * This is intended for internal use where we need to figure out which state
+   * corresponds to aTrackID, not for availability checks. As such, we assert
+   * that the device does indeed exist.
+   *
+   * Since this is a raw pointer and the state lifetime depends on the
+   * SourceListener's lifetime, it's internal use only.
+   */
+  DeviceState& GetDeviceStateFor(TrackID aTrackID) const;
+
   // true after this listener has had all devices stopped. MainThread only.
   bool mStopped;
 
   // true after the stream this listener is listening to has finished in the
   // MediaStreamGraph. MainThread only.
   bool mFinished;
 
   // true after this listener has been removed from its MediaStream.
   // MainThread only.
   bool mRemoved;
 
-  // true if we have stopped mAudioDevice. MainThread only.
-  bool mAudioStopped;
-
-  // true if we have stopped mVideoDevice. MainThread only.
-  bool mVideoStopped;
-
   // never ever indirect off this; just for assertions
   PRThread* mMainThreadCheck;
 
   // Set in Register() on main thread, then read from any thread.
   PrincipalHandle mPrincipalHandle;
 
   // Weak pointer to the window listener that owns us. MainThread only.
   GetUserMediaWindowListener* mWindowListener;
 
-  // Set at Activate on MainThread
-
   // Accessed from MediaStreamGraph thread, MediaManager thread, and MainThread
-  // No locking needed as they're only addrefed except on the MediaManager thread
-  RefPtr<MediaDevice> mAudioDevice; // threadsafe refcnt
-  RefPtr<MediaDevice> mVideoDevice; // threadsafe refcnt
+  // No locking needed as they're set on Activate() and never assigned to again.
+  UniquePtr<DeviceState> mAudioDeviceState;
+  UniquePtr<DeviceState> mVideoDeviceState;
   RefPtr<SourceMediaStream> mStream; // threadsafe refcnt
 };
 
 /**
  * This class represents a WindowID and handles all MediaStreamListeners
  * (here subclassed as SourceListeners) used to feed GetUserMedia source
  * streams. It proxies feedback from them into messages for browser chrome.
  * The SourceListeners are used to Start() and Stop() the underlying
@@ -334,57 +393,43 @@ public:
   {}
 
   /**
    * Registers an inactive gUM source listener for this WindowListener.
    */
   void Register(SourceListener* aListener)
   {
     MOZ_ASSERT(NS_IsMainThread());
-    if (!aListener || aListener->Activated()) {
-      MOZ_ASSERT(false, "Invalid listener");
-      return;
-    }
-    if (mInactiveListeners.Contains(aListener)) {
-      MOZ_ASSERT(false, "Already registered");
-      return;
-    }
-    if (mActiveListeners.Contains(aListener)) {
-      MOZ_ASSERT(false, "Already activated");
-      return;
-    }
+    MOZ_ASSERT(aListener);
+    MOZ_ASSERT(!aListener->Activated());
+    MOZ_ASSERT(!mInactiveListeners.Contains(aListener), "Already registered");
+    MOZ_ASSERT(!mActiveListeners.Contains(aListener), "Already activated");
 
     aListener->Register(this);
     mInactiveListeners.AppendElement(aListener);
   }
 
   /**
    * Activates an already registered and inactive gUM source listener for this
    * WindowListener.
    */
   void Activate(SourceListener* aListener,
                 SourceMediaStream* aStream,
                 MediaDevice* aAudioDevice,
                 MediaDevice* aVideoDevice)
   {
     MOZ_ASSERT(NS_IsMainThread());
-
-    if (!aListener || aListener->Activated()) {
-      MOZ_ASSERT(false, "Cannot activate already activated source listener");
-      return;
-    }
-
-    if (!mInactiveListeners.RemoveElement(aListener)) {
-      MOZ_ASSERT(false, "Cannot activate non-registered source listener");
-      return;
-    }
-
-    RefPtr<SourceListener> listener = aListener;
-    listener->Activate(aStream, aAudioDevice, aVideoDevice);
-    mActiveListeners.AppendElement(listener.forget());
+    MOZ_ASSERT(aListener);
+    MOZ_ASSERT(!aListener->Activated());
+    MOZ_ASSERT(mInactiveListeners.Contains(aListener), "Must be registered to activate");
+    MOZ_ASSERT(!mActiveListeners.Contains(aListener), "Already activated");
+
+    mInactiveListeners.RemoveElement(aListener);
+    aListener->Activate(aStream, aAudioDevice, aVideoDevice);
+    mActiveListeners.AppendElement(do_AddRef(aListener));
   }
 
   // Can be invoked from EITHER MainThread or MSG thread
   void Stop()
   {
     MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
 
     for (auto& source : mActiveListeners) {
@@ -700,17 +745,17 @@ MediaDevice::MediaDevice(MediaEngineSour
 {
 }
 
 /**
  * Helper functions that implement the constraints algorithm from
  * http://dev.w3.org/2011/webrtc/editor/getusermedia.html#methods-5
  */
 
-bool
+/* static */ bool
 MediaDevice::StringsContain(const OwningStringOrStringSequence& aStrings,
                             nsString aN)
 {
   return aStrings.IsString() ? aStrings.GetAsString() == aN
                              : aStrings.GetAsStringSequence().Contains(aN);
 }
 
 /* static */ uint32_t
@@ -747,16 +792,18 @@ MediaDevice::FitnessDistance(nsString aN
   }
 }
 
 uint32_t
 MediaDevice::GetBestFitnessDistance(
     const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
     bool aIsChrome)
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
+
   nsString mediaSource;
   GetMediaSource(mediaSource);
 
   // This code is reused for audio, where the mediaSource constraint does
   // not currently have a function, but because it defaults to "camera" in
   // webidl, we ignore it for audio here.
   if (!mediaSource.EqualsASCII("microphone")) {
     for (const auto& constraint : aConstraintSets) {
@@ -770,128 +817,143 @@ MediaDevice::GetBestFitnessDistance(
   // Pass in device's origin-specific id for deviceId constraint comparison.
   const nsString& id = aIsChrome ? mRawID : mID;
   return mSource->GetBestFitnessDistance(aConstraintSets, id);
 }
 
 NS_IMETHODIMP
 MediaDevice::GetName(nsAString& aName)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   aName.Assign(mName);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 MediaDevice::GetType(nsAString& aType)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   aType.Assign(mType);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 MediaDevice::GetId(nsAString& aID)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   aID.Assign(mID);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 MediaDevice::GetRawId(nsAString& aID)
 {
+  MOZ_ASSERT(NS_IsMainThread());
   aID.Assign(mRawID);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 MediaDevice::GetScary(bool* aScary)
 {
   *aScary = mScary;
   return NS_OK;
 }
 
 void
 MediaDevice::GetSettings(dom::MediaTrackSettings& aOutSettings) const
 {
+  MOZ_ASSERT(NS_IsMainThread());
   mSource->GetSettings(aOutSettings);
 }
 
+  // Threadsafe since mSource is const.
 NS_IMETHODIMP
 MediaDevice::GetMediaSource(nsAString& aMediaSource)
 {
-  MediaSourceEnum source = GetMediaSource();
-  if (source == MediaSourceEnum::Microphone) {
-    aMediaSource.AssignLiteral(u"microphone");
-  } else if (source == MediaSourceEnum::AudioCapture) {
-    aMediaSource.AssignLiteral(u"audioCapture");
-  } else if (source == MediaSourceEnum::Window) { // this will go away
-    aMediaSource.AssignLiteral(u"window");
-  } else { // all the rest are shared
-    aMediaSource.Assign(NS_ConvertUTF8toUTF16(
-      dom::MediaSourceEnumValues::strings[uint32_t(source)].value));
-  }
+  aMediaSource.Assign(NS_ConvertUTF8toUTF16(
+    dom::MediaSourceEnumValues::strings[uint32_t(GetMediaSource())].value));
   return NS_OK;
 }
 
-nsresult MediaDevice::Allocate(const dom::MediaTrackConstraints &aConstraints,
-                               const MediaEnginePrefs &aPrefs,
-                               const ipc::PrincipalInfo& aPrincipalInfo,
-                               const char** aOutBadConstraint)
+nsresult
+MediaDevice::Allocate(const dom::MediaTrackConstraints &aConstraints,
+                      const MediaEnginePrefs &aPrefs,
+                      const ipc::PrincipalInfo& aPrincipalInfo,
+                      const char** aOutBadConstraint)
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
   return mSource->Allocate(aConstraints,
                            aPrefs,
                            mID,
                            aPrincipalInfo,
                            getter_AddRefs(mAllocationHandle),
                            aOutBadConstraint);
 }
 
-nsresult MediaDevice::SetTrack(const RefPtr<SourceMediaStream>& aStream,
-                               TrackID aTrackID,
-                               const PrincipalHandle& aPrincipalHandle)
+nsresult
+MediaDevice::SetTrack(const RefPtr<SourceMediaStream>& aStream,
+                      TrackID aTrackID,
+                      const PrincipalHandle& aPrincipalHandle)
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
   return mSource->SetTrack(mAllocationHandle, aStream, aTrackID, aPrincipalHandle);
 }
 
-nsresult MediaDevice::Start()
+nsresult
+MediaDevice::Start()
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
   return mSource->Start(mAllocationHandle);
 }
 
-nsresult MediaDevice::Reconfigure(const dom::MediaTrackConstraints &aConstraints,
-                              const MediaEnginePrefs &aPrefs,
-                              const char** aOutBadConstraint)
+nsresult
+MediaDevice::Reconfigure(const dom::MediaTrackConstraints &aConstraints,
+                         const MediaEnginePrefs &aPrefs,
+                         const char** aOutBadConstraint)
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
   return mSource->Reconfigure(mAllocationHandle,
                               aConstraints,
                               aPrefs,
                               mID,
                               aOutBadConstraint);
 }
 
-nsresult MediaDevice::Stop()
+nsresult
+MediaDevice::Stop()
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
   return mSource->Stop(mAllocationHandle);
 }
 
-nsresult MediaDevice::Deallocate()
+nsresult
+MediaDevice::Deallocate()
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
   return mSource->Deallocate(mAllocationHandle);
 }
 
-void MediaDevice::Pull(const RefPtr<SourceMediaStream>& aStream,
-                       TrackID aTrackID,
-                       StreamTime aDesiredTime,
-                       const PrincipalHandle& aPrincipal)
+void
+MediaDevice::Pull(const RefPtr<SourceMediaStream>& aStream,
+                  TrackID aTrackID,
+                  StreamTime aDesiredTime,
+                  const PrincipalHandle& aPrincipal)
 {
+  // This is on the graph thread, but mAllocationHandle is safe since we never
+  // change it after it's been set, which is guaranteed to happen before
+  // registering the listener for pulls.
   mSource->Pull(mAllocationHandle, aStream, aTrackID, aDesiredTime, aPrincipal);
 }
 
 dom::MediaSourceEnum
 MediaDevice::GetMediaSource() const
 {
+  // Threadsafe because mSource is const. GetMediaSource() might have other
+  // requirements.
   return mSource->GetMediaSource();
 }
 
 static bool
 IsOn(const OwningBooleanOrMediaTrackConstraints &aUnion) {
   return !aUnion.IsBoolean() || aUnion.GetAsBoolean();
 }
 
@@ -1107,39 +1169,39 @@ public:
           return mListener->ApplyConstraintsToTrack(aWindow, mTrackID,
                                                     aConstraints, aCallerType);
         }
 
         void
         GetSettings(dom::MediaTrackSettings& aOutSettings) override
         {
           if (mListener) {
-            mListener->GetDevice(mTrackID)->GetSettings(aOutSettings);
+            mListener->GetSettingsFor(mTrackID, aOutSettings);
           }
         }
 
         void Stop() override
         {
           if (mListener) {
             mListener->StopTrack(mTrackID);
             mListener = nullptr;
           }
         }
 
         void Disable() override
         {
           if (mListener) {
-            mListener->DisableTrack(mTrackID);
+            mListener->SetEnabledFor(mTrackID, false);
           }
         }
 
         void Enable() override
         {
           if (mListener) {
-            mListener->EnableTrack(mTrackID);
+            mListener->SetEnabledFor(mTrackID, true);
           }
         }
 
       protected:
         ~LocalTrackSource() {}
 
         RefPtr<SourceListener> mListener;
         const MediaSourceEnum mSource;
@@ -1161,32 +1223,30 @@ public:
         "GetUserMediaStreamRunnable::DOMMediaStreamMainThreadHolder",
         DOMLocalMediaStream::CreateSourceStreamAsInput(window, msg,
                                                        new FakeTrackSourceGetter(principal)));
       stream = domStream->GetInputStream()->AsSourceStream();
 
       if (mAudioDevice) {
         nsString audioDeviceName;
         mAudioDevice->GetName(audioDeviceName);
-        const MediaSourceEnum source =
-          mAudioDevice->GetMediaSource();
+        const MediaSourceEnum source = mAudioDevice->GetMediaSource();
         RefPtr<MediaStreamTrackSource> audioSource =
           new LocalTrackSource(principal, audioDeviceName, mSourceListener,
                                source, kAudioTrack, mPeerIdentity);
         MOZ_ASSERT(IsOn(mConstraints.mAudio));
         RefPtr<MediaStreamTrack> track =
           domStream->CreateDOMTrack(kAudioTrack, MediaSegment::AUDIO, audioSource,
                                     GetInvariant(mConstraints.mAudio));
         domStream->AddTrackInternal(track);
       }
       if (mVideoDevice) {
         nsString videoDeviceName;
         mVideoDevice->GetName(videoDeviceName);
-        const MediaSourceEnum source =
-          mVideoDevice->GetMediaSource();
+        const MediaSourceEnum source = mVideoDevice->GetMediaSource();
         RefPtr<MediaStreamTrackSource> videoSource =
           new LocalTrackSource(principal, videoDeviceName, mSourceListener,
                                source, kVideoTrack, mPeerIdentity);
         MOZ_ASSERT(IsOn(mConstraints.mVideo));
         RefPtr<MediaStreamTrack> track =
           domStream->CreateDOMTrack(kVideoTrack, MediaSegment::VIDEO, videoSource,
                                     GetInvariant(mConstraints.mVideo));
         domStream->AddTrackInternal(track);
@@ -1333,16 +1393,18 @@ private:
 
 // Source getter returning full list
 
 static void
 GetSources(MediaEngine *engine, MediaSourceEnum aSrcType,
            nsTArray<RefPtr<MediaDevice>>& aResult,
            const char* media_device_name = nullptr)
 {
+  MOZ_ASSERT(MediaManager::IsInMediaThread());
+
   nsTArray<RefPtr<MediaEngineSource>> sources;
   engine->EnumerateDevices(aSrcType, &sources);
 
   /*
    * We're allowing multiple tabs to access the same camera for parity
    * with Chrome.  See bug 811757 for some of the issues surrounding
    * this decision.  To disallow, we'd filter by IsAvailable() as we used
    * to.
@@ -1519,34 +1581,34 @@ public:
 
     if (mAudioDevice) {
       auto& constraints = GetInvariant(mConstraints.mAudio);
       rv = mAudioDevice->Allocate(constraints, mPrefs, mPrincipalInfo,
                                   &badConstraint);
       if (NS_FAILED(rv)) {
         errorMsg = "Failed to allocate audiosource";
         if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
-          nsTArray<RefPtr<MediaDevice>> audios;
-          audios.AppendElement(mAudioDevice);
+          nsTArray<RefPtr<MediaDevice>> devices;
+          devices.AppendElement(mAudioDevice);
           badConstraint = MediaConstraintsHelper::SelectSettings(
-              NormalizedConstraints(constraints), audios, mIsChrome);
+              NormalizedConstraints(constraints), devices, mIsChrome);
         }
       }
     }
     if (!errorMsg && mVideoDevice) {
       auto& constraints = GetInvariant(mConstraints.mVideo);
       rv = mVideoDevice->Allocate(constraints, mPrefs, mPrincipalInfo,
                                   &badConstraint);
       if (NS_FAILED(rv)) {
         errorMsg = "Failed to allocate videosource";
         if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
-          nsTArray<RefPtr<MediaDevice>> videos;
-          videos.AppendElement(mVideoDevice);
+          nsTArray<RefPtr<MediaDevice>> devices;
+          devices.AppendElement(mVideoDevice);
           badConstraint = MediaConstraintsHelper::SelectSettings(
-              NormalizedConstraints(constraints), videos, mIsChrome);
+              NormalizedConstraints(constraints), devices, mIsChrome);
         }
         if (mAudioDevice) {
           mAudioDevice->Deallocate();
         }
       }
     }
     if (errorMsg) {
       LOG(("%s %" PRIu32, errorMsg, static_cast<uint32_t>(rv)));
@@ -3641,81 +3703,64 @@ MediaManager::IsActivelyCapturingOrHasAP
   return audio == nsIPermissionManager::ALLOW_ACTION ||
          video == nsIPermissionManager::ALLOW_ACTION;
 }
 
 SourceListener::SourceListener()
   : mStopped(false)
   , mFinished(false)
   , mRemoved(false)
-  , mAudioStopped(false)
-  , mVideoStopped(false)
   , mMainThreadCheck(nullptr)
   , mPrincipalHandle(PRINCIPAL_HANDLE_NONE)
   , mWindowListener(nullptr)
 {}
 
-MediaDevice*
-SourceListener::GetDevice(TrackID aTrackID) const
-{
-  switch (aTrackID) {
-    case kAudioTrack:
-      return mAudioDevice;
-    case kVideoTrack:
-      return mVideoDevice;
-    default:
-      MOZ_ASSERT(false, "Unknown track id");
-      return nullptr;
-  }
-}
-
 void
 SourceListener::Register(GetUserMediaWindowListener* aListener)
 {
   LOG(("SourceListener %p registering with window listener %p", this, aListener));
 
-  if (mWindowListener) {
-    MOZ_ASSERT(false, "Already registered");
-    return;
-  }
-  if (Activated()) {
-    MOZ_ASSERT(false, "Already activated");
-    return;
-  }
-  if (!aListener) {
-    MOZ_ASSERT(false, "No listener");
-    return;
-  }
+  MOZ_ASSERT(aListener, "No listener");
+  MOZ_ASSERT(!mWindowListener, "Already registered");
+  MOZ_ASSERT(!Activated(), "Already activated");
+
   mPrincipalHandle = aListener->GetPrincipalHandle();
   mWindowListener = aListener;
 }
 
 void
 SourceListener::Activate(SourceMediaStream* aStream,
                          MediaDevice* aAudioDevice,
                          MediaDevice* aVideoDevice)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
 
   LOG(("SourceListener %p activating audio=%p video=%p", this, aAudioDevice, aVideoDevice));
 
-  if (mStopped) {
-    MOZ_ASSERT(false, "Cannot activate stopped source listener");
-    return;
-  }
-
-  if (Activated()) {
-    MOZ_ASSERT(false, "Already activated");
-    return;
-  }
+  MOZ_ASSERT(!mStopped, "Cannot activate stopped source listener");
+  MOZ_ASSERT(!Activated(), "Already activated");
 
   mMainThreadCheck = GetCurrentVirtualThread();
   mStream = aStream;
-  mAudioDevice = aAudioDevice;
-  mVideoDevice = aVideoDevice;
+  if (aAudioDevice) {
+    mAudioDeviceState =
+      MakeUnique<DeviceState>(
+          aAudioDevice,
+          aAudioDevice->GetMediaSource() == dom::MediaSourceEnum::Microphone &&
+          Preferences::GetBool("media.getusermedia.microphone.off_while_disabled.enabled", false));
+  }
+
+  if (aVideoDevice) {
+    mVideoDeviceState =
+      MakeUnique<DeviceState>(
+          aVideoDevice,
+          aVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Camera &&
+          Preferences::GetBool("media.getusermedia.camera.off_while_disabled.enabled", true));
+  }
+
   mStream->AddListener(this);
 }
 
 void
 SourceListener::Stop()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
 
@@ -3726,44 +3771,44 @@ SourceListener::Stop()
   LOG(("SourceListener %p stopping", this));
 
   // StopSharing() has some special logic, at least for audio capture.
   // It must be called when all tracks have stopped, before setting mStopped.
   StopSharing();
 
   mStopped = true;
 
-  if (!Activated()) {
-    MOZ_ASSERT(false, "There are no devices or any source stream to stop");
-    return;
-  }
-
-  if (mAudioDevice && !mAudioStopped) {
+  MOZ_ASSERT(Activated(), "There are no devices or any source stream to stop");
+  MOZ_ASSERT(mStream, "Can't end tracks. No source stream.");
+
+  if (mAudioDeviceState && !mAudioDeviceState->mStopped) {
     StopTrack(kAudioTrack);
   }
-  if (mVideoDevice && !mVideoStopped) {
+  if (mVideoDeviceState && !mVideoDeviceState->mStopped) {
     StopTrack(kVideoTrack);
   }
 
-  RefPtr<SourceMediaStream> source = mStream;
-  if (!source) {
-    MOZ_ASSERT(false, "Can't end tracks. No source stream.");
-    return;
-  }
-
-  MediaManager::PostTask(NewTaskFrom([source]() {
+  MediaManager::PostTask(NewTaskFrom([source = mStream]() {
     MOZ_ASSERT(MediaManager::IsInMediaThread());
     source->EndAllTrackAndFinish();
   }));
 }
 
 void
 SourceListener::Remove()
 {
   MOZ_ASSERT(NS_IsMainThread());
+
+  if (mAudioDeviceState) {
+    mAudioDeviceState->mDisableTimer->Cancel();
+  }
+  if (mVideoDeviceState) {
+    mVideoDeviceState->mDisableTimer->Cancel();
+  }
+
   if (!mStream || mRemoved) {
     return;
   }
 
   LOG(("SourceListener %p removed on purpose, mFinished = %d", this, (int) mFinished));
   mRemoved = true; // RemoveListener is async, avoid races
   mWindowListener = nullptr;
 
@@ -3776,196 +3821,234 @@ SourceListener::Remove()
     mStream->RemoveListener(this);
   }
 }
 
 void
 SourceListener::StopTrack(TrackID aTrackID)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
-
-  RefPtr<MediaDevice> device;
-
-  if (!Activated()) {
-    MOZ_ASSERT(false, "No device to stop");
+  MOZ_ASSERT(Activated(), "No device to stop");
+  MOZ_ASSERT(aTrackID == kAudioTrack || aTrackID == kVideoTrack,
+             "Unknown track id");
+  DeviceState& state = GetDeviceStateFor(aTrackID);
+
+  LOG(("SourceListener %p stopping %s track %d",
+       this, aTrackID == kAudioTrack ? "audio" : "video", aTrackID));
+
+  if (state.mStopped) {
+    // device already stopped.
     return;
   }
-
-  switch (aTrackID) {
-    case kAudioTrack: {
-      LOG(("SourceListener %p stopping audio track %d", this, aTrackID));
-      if (!mAudioDevice) {
-        NS_ASSERTION(false, "Can't stop audio. No device.");
-        return;
-      }
-      if (mAudioStopped) {
-        // Audio already stopped
-        return;
-      }
-      device = mAudioDevice;
-      mAudioStopped = true;
-      break;
-    }
-    case kVideoTrack: {
-      LOG(("SourceListener %p stopping video track %d", this, aTrackID));
-      if (!mVideoDevice) {
-        NS_ASSERTION(false, "Can't stop video. No device.");
-        return;
-      }
-      if (mVideoStopped) {
-        // Video already stopped
-        return;
-      }
-      device = mVideoDevice;
-      mVideoStopped = true;
-      break;
-    }
-    default: {
-      MOZ_ASSERT(false, "Unknown track id");
-      return;
-    }
-  }
-
-  MediaManager::PostTask(NewTaskFrom([device]() {
+  state.mStopped = true;
+
+  state.mDisableTimer->Cancel();
+
+  MediaManager::PostTask(NewTaskFrom([device = state.mDevice]() {
     device->Stop();
     device->Deallocate();
   }));
 
-  if ((!mAudioDevice || mAudioStopped) &&
-      (!mVideoDevice || mVideoStopped)) {
+  if ((!mAudioDeviceState || mAudioDeviceState->mStopped) &&
+      (!mVideoDeviceState || mVideoDeviceState->mStopped)) {
     LOG(("SourceListener %p this was the last track stopped", this));
     Stop();
   }
 
-  if (!mWindowListener) {
-    MOZ_ASSERT(false, "Should still have window listener");
-    return;
-  }
+  MOZ_ASSERT(mWindowListener, "Should still have window listener");
   mWindowListener->ChromeAffectingStateChanged();
 }
 
 void
-SourceListener::DisableTrack(TrackID aTrackID)
+SourceListener::GetSettingsFor(TrackID aTrackID,
+                               dom::MediaTrackSettings& aOutSettings) const
 {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
-
-  if (!Activated()) {
-    MOZ_ASSERT(false, "No device to disable");
-    return;
-  }
-
-  RefPtr<MediaDevice> device;
-
-  switch (aTrackID) {
-    case kAudioTrack: {
-      LOG(("SourceListener %p disabling audio track %d", this, aTrackID));
-      if (!mAudioDevice) {
-        NS_ASSERTION(false, "Can't disable audio. No device.");
-        return;
-      }
-      if (mAudioStopped) {
-        // Audio stopped. Disabling is pointless.
-        return;
-      }
-      device = mAudioDevice;
-      break;
-    }
-    case kVideoTrack: {
-      LOG(("SourceListener %p disabling video track %d", this, aTrackID));
-      if (!mVideoDevice) {
-        NS_ASSERTION(false, "Can't disable video. No device.");
-        return;
-      }
-      if (mVideoStopped) {
-        // Video stopped. Disabling is pointless.
-        return;
-      }
-      device = mVideoDevice;
-      break;
-    }
-    default: {
-      MOZ_ASSERT(false, "Unknown track id");
-      return;
-    }
-  }
-
-  // XXX Later patch
+  GetDeviceStateFor(aTrackID).mDevice->GetSettings(aOutSettings);
 }
 
 void
-SourceListener::EnableTrack(TrackID aTrackID)
+SourceListener::SetEnabledFor(TrackID aTrackID, bool aEnable)
 {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
-
-  if (!Activated()) {
-    MOZ_ASSERT(false, "No device to enable");
+  MOZ_ASSERT(Activated(), "No device to set enabled state for");
+  MOZ_ASSERT(aTrackID == kAudioTrack || aTrackID == kVideoTrack,
+             "Unknown track id");
+
+  if (mRemoved) {
+    return;
+  }
+
+  LOG(("SourceListener %p %s %s track %d",
+       this, aEnable ? "enabling" : "disabling",
+       aTrackID == kAudioTrack ? "audio" : "video", aTrackID));
+
+  DeviceState& state = GetDeviceStateFor(aTrackID);
+
+  state.mTrackEnabled = aEnable;
+
+  if (state.mStopped) {
+    // Device terminally stopped. Updating device state is pointless.
+    return;
+  }
+
+
+  if (state.mOperationInProgress) {
+    // If a timer is in progress, it needs to be canceled now so the next
+    // DisableTrack() gets a fresh start. Canceling will trigger another
+    // operation.
+    state.mDisableTimer->Cancel();
+    return;
+  }
+
+  if (state.mDeviceEnabled == aEnable) {
+    // Device is already in the desired state.
     return;
   }
 
-  RefPtr<MediaDevice> device;
-
-  switch (aTrackID) {
-    case kAudioTrack: {
-      LOG(("SourceListener %p enabling audio track %d", this, aTrackID));
-      if (!mAudioDevice) {
-        NS_ASSERTION(false, "Can't enable audio. No device.");
-        return;
+  // All paths from here on must end in setting `state.mOperationInProgress`
+  // to false.
+  state.mOperationInProgress = true;
+
+  RefPtr<MediaTimerPromise> timerPromise;
+  if (aEnable) {
+    timerPromise = MediaTimerPromise::CreateAndResolve(true, __func__);
+  } else {
+    const TimeDuration offDelay = TimeDuration::FromMilliseconds(
+      Preferences::GetUint(
+        aTrackID == kAudioTrack
+          ? "media.getusermedia.microphone.off_while_disabled.delay_ms"
+          : "media.getusermedia.camera.off_while_disabled.delay_ms",
+        3000));
+    timerPromise = state.mDisableTimer->WaitFor(offDelay, __func__);
+  }
+
+  typedef MozPromise<nsresult, bool, /* IsExclusive = */ true> DeviceOperationPromise;
+  RefPtr<SourceListener> self = this;
+  timerPromise->Then(GetMainThreadSerialEventTarget(), __func__,
+    [self, this, &state, aTrackID, aEnable](bool aDummy) mutable {
+      MOZ_ASSERT(state.mDeviceEnabled != aEnable,
+                 "Device operation hasn't started");
+      MOZ_ASSERT(state.mOperationInProgress,
+                 "It's our responsibility to reset the inProgress state");
+
+      LOG(("SourceListener %p %s %s track %d - starting device operation",
+           this, aEnable ? "enabling" : "disabling",
+           aTrackID == kAudioTrack ? "audio" : "video",
+           aTrackID));
+
+      state.mDeviceEnabled = aEnable;
+
+      if (mWindowListener) {
+        mWindowListener->ChromeAffectingStateChanged();
       }
-      if (mAudioStopped) {
-        // Audio stopped. Enabling is pointless.
+
+      if (!state.mOffWhileDisabled) {
+        // If the feature to turn a device off while disabled is itself disabled
+        // we shortcut the device operation and tell the ux-updating code
+        // that everything went fine.
+        return DeviceOperationPromise::CreateAndResolve(NS_OK, __func__);
+      }
+
+      RefPtr<DeviceOperationPromise::Private> promise =
+        new DeviceOperationPromise::Private(__func__);
+      MediaManager::PostTask(NewTaskFrom([self, device = state.mDevice,
+                                          aEnable, promise]() mutable {
+        promise->Resolve(aEnable ? device->Start() : device->Stop(), __func__);
+      }));
+      RefPtr<DeviceOperationPromise> result = promise.get();
+      return result;
+    }, [](bool aDummy) {
+      // Timer was canceled by us. We signal this with NS_ERROR_ABORT.
+      return DeviceOperationPromise::CreateAndResolve(NS_ERROR_ABORT, __func__);
+    })->Then(GetMainThreadSerialEventTarget(), __func__,
+    [self, this, &state, aTrackID, aEnable](nsresult aResult) mutable {
+      MOZ_ASSERT(state.mOperationInProgress);
+      state.mOperationInProgress = false;
+
+      if (state.mStopped) {
+        // Device was stopped on main thread during the operation. Nothing to do.
         return;
       }
-      device = mAudioDevice;
-      break;
-    }
-    case kVideoTrack: {
-      LOG(("SourceListener %p enabling video track %d", this, aTrackID));
-      if (!mVideoDevice) {
-        NS_ASSERTION(false, "Can't enable video. No device.");
+
+      LOG(("SourceListener %p %s %s track %d %s",
+           this,
+           aEnable ? "enabling" : "disabling",
+           aTrackID == kAudioTrack ? "audio" : "video",
+           aTrackID,
+           NS_SUCCEEDED(aResult) ? "succeeded" : "failed"));
+
+      if (NS_FAILED(aResult) && aResult != NS_ERROR_ABORT) {
+        // This path handles errors from starting or stopping the device.
+        // NS_ERROR_ABORT are for cases where *we* aborted. They need graceful
+        // handling.
+        MOZ_ASSERT(state.mDeviceEnabled != aEnable,
+                   "If operating the device failed, the device's `enabled` "
+                   "state must remain at its old value");
+        if (aEnable) {
+          // Starting the device failed. Stopping the track here will make the
+          // MediaStreamTrack end after a pass through the MediaStreamGraph.
+          StopTrack(aTrackID);
+        } else {
+          // Stopping the device failed. This is odd, but not fatal.
+          MOZ_ASSERT_UNREACHABLE("The device should be stoppable");
+
+          // To keep our internal state sane in this case, we disallow future
+          // stops due to disable.
+          state.mOffWhileDisabled = false;
+        }
         return;
       }
-      if (mVideoStopped) {
-        // Video stopped. Enabling is pointless.
+
+      // This path is for a device operation aResult that was success or
+      // NS_ERROR_ABORT (*we* canceled the operation).
+      // At this point we have to follow up on the intended state, i.e., update
+      // the device state if the track state changed in the meantime.
+      MOZ_ASSERT_IF(NS_SUCCEEDED(aResult), state.mDeviceEnabled == aEnable);
+
+      if (state.mTrackEnabled == state.mDeviceEnabled) {
+        // Intended state is same as device's current state.
+        // Nothing more to do.
         return;
       }
-      device = mVideoDevice;
-      break;
-    }
-    default: {
-      MOZ_ASSERT(false, "Unknown track id");
-      return;
-    }
-  }
-
-  // XXX Later patch
+
+      // Track state changed during this operation. We'll start over.
+      if (state.mTrackEnabled) {
+        SetEnabledFor(aTrackID, true);
+      } else {
+        SetEnabledFor(aTrackID, false);
+      }
+    }, [](bool aDummy) {
+      MOZ_ASSERT_UNREACHABLE("Unexpected and unhandled reject");
+    });
 }
 
 void
 SourceListener::StopSharing()
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_RELEASE_ASSERT(mWindowListener);
 
   if (mStopped) {
     return;
   }
 
   LOG(("SourceListener %p StopSharing", this));
 
-  if (mVideoDevice &&
-      (mVideoDevice->GetMediaSource() == MediaSourceEnum::Screen ||
-       mVideoDevice->GetMediaSource() == MediaSourceEnum::Application ||
-       mVideoDevice->GetMediaSource() == MediaSourceEnum::Window)) {
+  if (mVideoDeviceState &&
+      (mVideoDeviceState->mDevice->GetMediaSource() == MediaSourceEnum::Screen ||
+       mVideoDeviceState->mDevice->GetMediaSource() == MediaSourceEnum::Application ||
+       mVideoDeviceState->mDevice->GetMediaSource() == MediaSourceEnum::Window)) {
     // We want to stop the whole stream if there's no audio;
     // just the video track if we have both.
     // StopTrack figures this out for us.
     StopTrack(kVideoTrack);
   }
-  if (mAudioDevice &&
-      mAudioDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) {
+  if (mAudioDeviceState &&
+      mAudioDeviceState->mDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) {
     uint64_t windowID = mWindowListener->WindowID();
     nsCOMPtr<nsPIDOMWindowInner> window = nsGlobalWindowInner::GetInnerWindowWithId(windowID)->AsInner();
     MOZ_RELEASE_ASSERT(window);
     window->SetAudioCapture(false);
     MediaStreamGraph* graph =
       MediaStreamGraph::GetInstance(MediaStreamGraph::AUDIO_THREAD_DRIVER, window);
     graph->UnregisterCaptureStreamForWindow(windowID);
     mStream->Destroy();
@@ -3974,30 +4057,29 @@ SourceListener::StopSharing()
 
 SourceMediaStream*
 SourceListener::GetSourceStream()
 {
   NS_ASSERTION(mStream,"Getting stream from never-activated SourceListener");
   return mStream;
 }
 
+
 // Proxy NotifyPull() to sources
 void
 SourceListener::NotifyPull(MediaStreamGraph* aGraph,
                            StreamTime aDesiredTime)
 {
-  // Currently audio sources ignore NotifyPull, but they could
-  // watch it especially for fake audio.
-  if (mAudioDevice) {
-    mAudioDevice->Pull(mStream, kAudioTrack,
-                       aDesiredTime, mPrincipalHandle);
+  if (mAudioDeviceState) {
+    mAudioDeviceState->mDevice->Pull(mStream, kAudioTrack,
+                                     aDesiredTime, mPrincipalHandle);
   }
-  if (mVideoDevice) {
-    mVideoDevice->Pull(mStream, kVideoTrack,
-                       aDesiredTime, mPrincipalHandle);
+  if (mVideoDeviceState) {
+    mVideoDeviceState->mDevice->Pull(mStream, kVideoTrack,
+                                     aDesiredTime, mPrincipalHandle);
   }
 }
 
 void
 SourceListener::NotifyEvent(MediaStreamGraph* aGraph,
                             MediaStreamGraphEvent aEvent)
 {
   nsCOMPtr<nsIEventTarget> target;
@@ -4065,88 +4147,85 @@ SourceListener::NotifyRemoved()
 
   mWindowListener = nullptr;
 }
 
 bool
 SourceListener::CapturingVideo() const
 {
   MOZ_ASSERT(NS_IsMainThread());
-  return Activated() && mVideoDevice && !mVideoStopped &&
-         !mVideoDevice->mSource->IsAvailable() &&
-         mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Camera &&
-         (!mVideoDevice->mSource->IsFake() ||
+  return Activated() && mVideoDeviceState &&
+         !mVideoDeviceState->mStopped &&
+         mVideoDeviceState->mDevice->GetMediaSource() == dom::MediaSourceEnum::Camera &&
+         (!mVideoDeviceState->mDevice->mSource->IsFake() ||
           Preferences::GetBool("media.navigator.permission.fake"));
 }
 
 bool
 SourceListener::CapturingAudio() const
 {
   MOZ_ASSERT(NS_IsMainThread());
-  return Activated() && mAudioDevice && !mAudioStopped &&
-         !mAudioDevice->mSource->IsAvailable() &&
-         (!mAudioDevice->mSource->IsFake() ||
+  return Activated() && mAudioDeviceState &&
+         !mAudioDeviceState->mStopped &&
+         mAudioDeviceState->mDevice->GetMediaSource() == dom::MediaSourceEnum::Microphone &&
+         (mAudioDeviceState->mDevice->mSource->IsFake() ||
           Preferences::GetBool("media.navigator.permission.fake"));
 }
 
 bool
 SourceListener::CapturingScreen() const
 {
   MOZ_ASSERT(NS_IsMainThread());
-  return Activated() && mVideoDevice && !mVideoStopped &&
-         !mVideoDevice->mSource->IsAvailable() &&
-         mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Screen;
+  return Activated() && mVideoDeviceState &&
+         !mVideoDeviceState->mStopped &&
+         mVideoDeviceState->mDevice->GetMediaSource() == dom::MediaSourceEnum::Screen;
 }
 
 bool
 SourceListener::CapturingWindow() const
 {
   MOZ_ASSERT(NS_IsMainThread());
-  return Activated() && mVideoDevice && !mVideoStopped &&
-         !mVideoDevice->mSource->IsAvailable() &&
-         mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Window;
+  return Activated() && mVideoDeviceState &&
+         !mVideoDeviceState->mStopped &&
+         mVideoDeviceState->mDevice->GetMediaSource() == dom::MediaSourceEnum::Window;
 }
 
 bool
 SourceListener::CapturingApplication() const
 {
   MOZ_ASSERT(NS_IsMainThread());
-  return Activated() && mVideoDevice && !mVideoStopped &&
-         !mVideoDevice->mSource->IsAvailable() &&
-         mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Application;
+  return Activated() && mVideoDeviceState &&
+         !mVideoDeviceState->mStopped &&
+         mVideoDeviceState->mDevice->GetMediaSource() == dom::MediaSourceEnum::Application;
 }
 
 bool
 SourceListener::CapturingBrowser() const
 {
   MOZ_ASSERT(NS_IsMainThread());
-  return Activated() && mVideoDevice && !mVideoStopped &&
-         !mVideoDevice->mSource->IsAvailable() &&
-         mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Browser;
+  return Activated() && mVideoDeviceState &&
+         !mVideoDeviceState->mStopped &&
+         mVideoDeviceState->mDevice->GetMediaSource() == dom::MediaSourceEnum::Browser;
 }
 
 already_AddRefed<PledgeVoid>
 SourceListener::ApplyConstraintsToTrack(
     nsPIDOMWindowInner* aWindow,
     TrackID aTrackID,
     const MediaTrackConstraints& aConstraintsPassedIn,
     dom::CallerType aCallerType)
 {
   MOZ_ASSERT(NS_IsMainThread());
   RefPtr<PledgeVoid> p = new PledgeVoid();
 
-  // XXX to support multiple tracks of a type in a stream, this should key off
-  // the TrackID and not just the type
-  RefPtr<MediaDevice> audioDevice =
-    aTrackID == kAudioTrack ? mAudioDevice.get() : nullptr;
-  RefPtr<MediaDevice> videoDevice =
-    aTrackID == kVideoTrack ? mVideoDevice.get() : nullptr;
-
-  if (mStopped || (!audioDevice && !videoDevice))
-  {
+  MOZ_ASSERT(aTrackID == kAudioTrack || aTrackID == kVideoTrack,
+             "Unknown track id");
+
+  DeviceState& state = GetDeviceStateFor(aTrackID);
+  if (mStopped || state.mStopped) {
     LOG(("gUM track %d applyConstraints, but we don't have type %s",
          aTrackID, aTrackID == kAudioTrack ? "audio" : "video"));
     p->Resolve(false);
     return p.forget();
   }
   MediaTrackConstraints c(aConstraintsPassedIn); // use a modifiable copy
 
   MediaConstraintsHelper::ConvertOldWithWarning(c.mMozAutoGainControl,
@@ -4162,40 +4241,29 @@ SourceListener::ApplyConstraintsToTrack(
   if (!mgr) {
     return p.forget();
   }
   uint32_t id = mgr->mOutstandingVoidPledges.Append(*p);
   uint64_t windowId = aWindow->WindowID();
   bool isChrome = (aCallerType == dom::CallerType::System);
 
   MediaManager::PostTask(NewTaskFrom([id, windowId,
-                                      audioDevice, videoDevice,
+                                      device = state.mDevice,
                                       c, isChrome]() mutable {
     MOZ_ASSERT(MediaManager::IsInMediaThread());
     MediaManager* mgr = MediaManager::GetIfExists();
     MOZ_RELEASE_ASSERT(mgr); // Must exist while media thread is alive
     const char* badConstraint = nullptr;
-    nsresult rv = NS_OK;
-
-    if (audioDevice) {
-      rv = audioDevice->Reconfigure(c, mgr->mPrefs, &badConstraint);
-      if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
-        nsTArray<RefPtr<MediaDevice>> audios;
-        audios.AppendElement(audioDevice);
-        badConstraint = MediaConstraintsHelper::SelectSettings(
-            NormalizedConstraints(c), audios, isChrome);
-      }
-    } else {
-      rv = videoDevice->Reconfigure(c, mgr->mPrefs, &badConstraint);
-      if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
-        nsTArray<RefPtr<MediaDevice>> videos;
-        videos.AppendElement(videoDevice);
-        badConstraint = MediaConstraintsHelper::SelectSettings(
-            NormalizedConstraints(c), videos, isChrome);
-      }
+
+    nsresult rv = device->Reconfigure(c, mgr->mPrefs, &badConstraint);
+    if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
+      nsTArray<RefPtr<MediaDevice>> devices;
+      devices.AppendElement(device);
+      badConstraint = MediaConstraintsHelper::SelectSettings(
+          NormalizedConstraints(c), devices, isChrome);
     }
     NS_DispatchToMainThread(NewRunnableFrom([id, windowId, rv,
                                              badConstraint]() mutable {
       MOZ_ASSERT(NS_IsMainThread());
       MediaManager* mgr = MediaManager::GetIfExists();
       if (!mgr) {
         return NS_OK;
       }
@@ -4231,16 +4299,33 @@ SourceListener::ApplyConstraintsToTrack(
 }
 
 PrincipalHandle
 SourceListener::GetPrincipalHandle() const
 {
   return mPrincipalHandle;
 }
 
+DeviceState&
+SourceListener::GetDeviceStateFor(TrackID aTrackID) const
+{
+  // XXX to support multiple tracks of a type in a stream, this should key off
+  // the TrackID and not just the type
+  switch (aTrackID) {
+    case kAudioTrack:
+      MOZ_ASSERT(mAudioDeviceState, "No audio device");
+      return *mAudioDeviceState;
+    case kVideoTrack:
+      MOZ_ASSERT(mVideoDeviceState, "No video device");
+      return *mVideoDeviceState;
+    default:
+      MOZ_CRASH("Unknown track id");
+  }
+}
+
 // Doesn't kill audio
 void
 GetUserMediaWindowListener::StopSharing()
 {
   MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
 
   for (auto& source : mActiveListeners) {
     source->StopSharing();
@@ -4271,17 +4356,17 @@ GetUserMediaWindowListener::StopRawID(co
 }
 
 void
 GetUserMediaWindowListener::ChromeAffectingStateChanged()
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // We wait until stable state before notifying chrome so chrome only does one
-  // update if more tracks are stopped in this event loop.
+  // update if more updates happen in this event loop.
 
   if (mChromeNotificationTaskPosted) {
     return;
   }
 
   nsCOMPtr<nsIRunnable> runnable =
     NewRunnableMethod("GetUserMediaWindowListener::NotifyChrome",
                       this,
@@ -4300,14 +4385,17 @@ GetUserMediaWindowListener::NotifyChrome
                                                  [windowID = mWindowID]() {
     nsGlobalWindowInner* window =
       nsGlobalWindowInner::GetInnerWindowWithId(windowID);
     if (!window) {
       MOZ_ASSERT_UNREACHABLE("Should have window");
       return;
     }
 
-    DebugOnly<nsresult> rv = MediaManager::NotifyRecordingStatusChange(window->AsInner());
-    MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to notify chrome");
+    nsresult rv = MediaManager::NotifyRecordingStatusChange(window->AsInner());
+    if (NS_FAILED(rv)) {
+      MOZ_ASSERT_UNREACHABLE("Should be able to notify chrome");
+      return;
+    }
   }));
 }
 
 } // namespace mozilla
--- a/dom/media/webrtc/MediaEngineDefault.cpp
+++ b/dom/media/webrtc/MediaEngineDefault.cpp
@@ -392,16 +392,24 @@ MediaEngineDefaultAudioSource::GetBestFi
   for (const auto* cs : aConstraintSets) {
     distance = MediaConstraintsHelper::GetMinimumFitnessDistance(*cs, aDeviceId);
     break; // distance is read from first entry only
   }
 #endif
   return distance;
 }
 
+bool
+MediaEngineDefaultAudioSource::IsAvailable() const
+{
+  AssertIsOnOwningThread();
+
+  return mState == kReleased;
+}
+
 nsresult
 MediaEngineDefaultAudioSource::Allocate(const dom::MediaTrackConstraints &aConstraints,
                                         const MediaEnginePrefs &aPrefs,
                                         const nsString& aDeviceId,
                                         const mozilla::ipc::PrincipalInfo& aPrincipalInfo,
                                         AllocationHandle** aOutHandle,
                                         const char** aOutBadConstraint)
 {
--- a/dom/media/webrtc/MediaEngineDefault.h
+++ b/dom/media/webrtc/MediaEngineDefault.h
@@ -35,21 +35,16 @@ class MediaEngineDefault;
 /**
  * The default implementation of the MediaEngine interface.
  */
 class MediaEngineDefaultVideoSource : public MediaEngineSource
 {
 public:
   MediaEngineDefaultVideoSource();
 
-  bool IsAvailable() const override
-  {
-    AssertIsOnOwningThread();
-    return mState == kReleased;
-  }
   nsString GetName() const override;
   nsCString GetUUID() const override;
 
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs &aPrefs,
                     const nsString& aDeviceId,
                     const ipc::PrincipalInfo& aPrincipalInfo,
                     AllocationHandle** aOutHandle,
@@ -115,21 +110,16 @@ protected:
 
 class SineWaveGenerator;
 
 class MediaEngineDefaultAudioSource : public MediaEngineSource
 {
 public:
   MediaEngineDefaultAudioSource();
 
-  bool IsAvailable() const override
-  {
-    AssertIsOnOwningThread();
-    return mState == kReleased;
-  }
   nsString GetName() const override;
   nsCString GetUUID() const override;
 
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs &aPrefs,
                     const nsString& aDeviceId,
                     const ipc::PrincipalInfo& aPrincipalInfo,
                     AllocationHandle** aOutHandle,
@@ -164,16 +154,17 @@ public:
   {
     return dom::MediaSourceEnum::Microphone;
   }
 
   uint32_t GetBestFitnessDistance(
       const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
       const nsString& aDeviceId) const override;
 
+  bool IsAvailable() const;
 
 protected:
   ~MediaEngineDefaultAudioSource();
 
   // mMutex protects mState, mStream, mTrackID
   Mutex mMutex;
 
   // Current state of this source.
--- a/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
+++ b/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
@@ -923,17 +923,16 @@ MediaEngineRemoteVideoSource::ChooseCapa
 
   LogCapability("Chosen capability", aCapability, sameDistance);
   return true;
 }
 
 void
 MediaEngineRemoteVideoSource::GetSettings(MediaTrackSettings& aOutSettings) const
 {
-  MOZ_ASSERT(NS_IsMainThread());
   aOutSettings = *mSettings;
 }
 
 void
 MediaEngineRemoteVideoSource::Refresh(int aIndex)
 {
   LOG((__PRETTY_FUNCTION__));
   AssertIsOnOwningThread();
--- a/dom/media/webrtc/MediaEngineRemoteVideoSource.h
+++ b/dom/media/webrtc/MediaEngineRemoteVideoSource.h
@@ -107,21 +107,16 @@ public:
                                dom::MediaSourceEnum aMediaSource,
                                bool aScary);
 
   // ExternalRenderer
   int DeliverFrame(uint8_t* buffer,
                    const camera::VideoFrameProperties& properties) override;
 
   // MediaEngineSource
-  bool IsAvailable() const override
-  {
-    AssertIsOnOwningThread();
-    return mState == kReleased;
-  }
   dom::MediaSourceEnum GetMediaSource() const override
   {
     return mMediaSource;
   }
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs &aPrefs,
                     const nsString& aDeviceId,
                     const ipc::PrincipalInfo& aPrincipalInfo,
--- a/dom/media/webrtc/MediaEngineSource.h
+++ b/dom/media/webrtc/MediaEngineSource.h
@@ -90,21 +90,16 @@ public:
 
   /**
    * Return true if this is a fake source. I.e., if it is generating media
    * itself rather than being an interface to underlying hardware.
    */
   virtual bool IsFake() const = 0;
 
   /**
-   * Returns true if this source is available to allocate.
-   */
-  virtual bool IsAvailable() const = 0;
-
-  /**
    * Gets the human readable name of this device.
    */
   virtual nsString GetName() const = 0;
 
   /**
    * Gets the UUID of this device.
    */
   virtual nsCString GetUUID() const = 0;
--- a/dom/media/webrtc/MediaEngineTabVideoSource.h
+++ b/dom/media/webrtc/MediaEngineTabVideoSource.h
@@ -14,22 +14,16 @@ namespace mozilla {
 class MediaEngineTabVideoSource : public MediaEngineSource
 {
 public:
   MediaEngineTabVideoSource();
 
   nsString GetName() const override;
   nsCString GetUUID() const override;
 
-  bool IsAvailable() const override
-  {
-    AssertIsOnOwningThread();
-    return mState == kReleased;
-  }
-
   bool GetScary() const override
   {
     return true;
   }
 
   dom::MediaSourceEnum GetMediaSource() const override
   {
     return dom::MediaSourceEnum::Browser;
--- a/dom/media/webrtc/MediaEngineWebRTC.h
+++ b/dom/media/webrtc/MediaEngineWebRTC.h
@@ -62,21 +62,16 @@ class MediaEngineWebRTCMicrophoneSource;
 class MediaEngineWebRTCAudioCaptureSource : public MediaEngineSource
 {
 public:
   explicit MediaEngineWebRTCAudioCaptureSource(const char* aUuid)
   {
   }
   nsString GetName() const override;
   nsCString GetUUID() const override;
-  bool IsAvailable() const override
-  {
-    AssertIsOnOwningThread();
-    return false;
-  }
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs &aPrefs,
                     const nsString& aDeviceId,
                     const ipc::PrincipalInfo& aPrincipalInfo,
                     AllocationHandle** aOutHandle,
                     const char** aOutBadConstraint) override
   {
     // Nothing to do here, everything is managed in MediaManager.cpp
@@ -397,22 +392,16 @@ public:
   bool RequiresSharing() const override
   {
     return true;
   }
 
   nsString GetName() const override;
   nsCString GetUUID() const override;
 
-  bool IsAvailable() const override
-  {
-    AssertIsOnOwningThread();
-    return mState == kReleased;
-  }
-
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs& aPrefs,
                     const nsString& aDeviceId,
                     const ipc::PrincipalInfo& aPrincipalInfo,
                     AllocationHandle** aOutHandle,
                     const char** aOutBadConstraint) override;
   nsresult Deallocate(const RefPtr<const AllocationHandle>& aHandle) override;
   nsresult SetTrack(const RefPtr<const AllocationHandle>& aHandle,