Bug 1299515 - Stop Camera and Microphone device when tracks become disabled. r?jib draft
authorAndreas Pehrson <pehrsons@mozilla.com>
Fri, 17 Nov 2017 19:56:00 +0100
changeset 749425 c308ecbfb205a7e83005e273472f7b3fbb03dc04
parent 749424 3a1aa4108affecb32a57cdab76fe843d94309cc7
child 749426 2406f3fcf40ea75aab5f12d32b034a5c98c40512
push id97396
push userbmo:apehrson@mozilla.com
push dateWed, 31 Jan 2018 13:27:39 +0000
reviewersjib
bugs1299515
milestone60.0a1
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,