/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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 "AudioCaptureTrack.h"
#include "AudioDeviceInfo.h"
#include "AudioStreamTrack.h"
#include "CubebDeviceEnumerator.h"
#include "MediaTimer.h"
#include "MediaTrackConstraints.h"
#include "MediaTrackGraphImpl.h"
#include "MediaTrackListener.h"
#include "VideoStreamTrack.h"
#include "VideoUtils.h"
#include "mozilla/Base64.h"
#include "mozilla/MozPromise.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/PeerIdentity.h"
#include "mozilla/PermissionDelegateHandler.h"
#include "mozilla/Sprintf.h"
#include "mozilla/StaticPrefs_media.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Types.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/FeaturePolicyUtils.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/GetUserMediaRequestBinding.h"
#include "mozilla/dom/MediaDeviceInfo.h"
#include "mozilla/dom/MediaDevices.h"
#include "mozilla/dom/MediaDevicesBinding.h"
#include "mozilla/dom/MediaStreamBinding.h"
#include "mozilla/dom/MediaStreamTrackBinding.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/UserActivation.h"
#include "mozilla/dom/WindowContext.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/media/MediaChild.h"
#include "mozilla/media/MediaTaskUtils.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsArray.h"
#include "nsContentUtils.h"
#include "nsGlobalWindow.h"
#include "nsHashPropertyBag.h"
#include "nsIEventTarget.h"
#include "nsIPermissionManager.h"
#include "nsIUUIDGenerator.h"
#include "nsJSUtils.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindow.h"
#include "nsProxyRelease.h"
#include "nspr.h"
#include "nss.h"
#include "pk11pub.h"

/* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */
#include "MediaEngineFake.h"
#include "MediaEngineSource.h"
#if defined(MOZ_WEBRTC)
#  include "MediaEngineWebRTC.h"
#  include "MediaEngineWebRTCAudio.h"
#  include "browser_logging/WebRtcLog.h"
#  include "modules/audio_processing/include/audio_processing.h"
#endif

#if defined(XP_WIN)
#  include <iphlpapi.h>
#  include <objbase.h>
#  include <tchar.h>
#  include <winsock2.h>

#  include "mozilla/WindowsVersion.h"
#endif

// A specialization of nsMainThreadPtrHolder for
// mozilla::dom::CallbackObjectHolder.  See documentation for
// nsMainThreadPtrHolder in nsProxyRelease.h.  This specialization lets us avoid
// wrapping the CallbackObjectHolder into a separate refcounted object.
template <class WebIDLCallbackT, class XPCOMCallbackT>
class nsMainThreadPtrHolder<
    mozilla::dom::CallbackObjectHolder<WebIDLCallbackT, XPCOMCallbackT>>
    final {
  typedef mozilla::dom::CallbackObjectHolder<WebIDLCallbackT, XPCOMCallbackT>
      Holder;

 public:
  nsMainThreadPtrHolder(const char* aName, Holder&& aHolder)
      : mHolder(std::move(aHolder))
#ifndef RELEASE_OR_BETA
        ,
        mName(aName)
#endif
  {
    MOZ_ASSERT(NS_IsMainThread());
  }

 private:
  // We can be released on any thread.
  ~nsMainThreadPtrHolder() {
    if (NS_IsMainThread()) {
      mHolder.Reset();
    } else if (mHolder.GetISupports()) {
      nsCOMPtr<nsIEventTarget> target = do_GetMainThread();
      MOZ_ASSERT(target);
      NS_ProxyRelease(
#ifdef RELEASE_OR_BETA
          nullptr,
#else
          mName,
#endif
          target, mHolder.Forget());
    }
  }

 public:
  Holder* get() {
    // Nobody should be touching the raw pointer off-main-thread.
    if (MOZ_UNLIKELY(!NS_IsMainThread())) {
      NS_ERROR("Can't dereference nsMainThreadPtrHolder off main thread");
      MOZ_CRASH();
    }
    return &mHolder;
  }

  bool operator!() const { return !mHolder; }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsMainThreadPtrHolder<Holder>)

 private:
  // Our holder.
  Holder mHolder;

#ifndef RELEASE_OR_BETA
  const char* mName = nullptr;
#endif

  // Copy constructor and operator= not implemented. Once constructed, the
  // holder is immutable.
  Holder& operator=(const nsMainThreadPtrHolder& aOther) = delete;
  nsMainThreadPtrHolder(const nsMainThreadPtrHolder& aOther) = delete;
};

namespace mozilla {

LazyLogModule gMediaManagerLog("MediaManager");
#define LOG(...) MOZ_LOG(gMediaManagerLog, LogLevel::Debug, (__VA_ARGS__))

class GetUserMediaStreamTask;
class LocalTrackSource;
class SelectAudioOutputTask;

using dom::BFCacheStatus;
using dom::CallerType;
using dom::ConstrainDOMStringParameters;
using dom::ConstrainDoubleRange;
using dom::ConstrainLongRange;
using dom::DisplayMediaStreamConstraints;
using dom::Document;
using dom::Element;
using dom::FeaturePolicyUtils;
using dom::File;
using dom::GetUserMediaRequest;
using dom::MediaDeviceKind;
using dom::MediaDevices;
using dom::MediaSourceEnum;
using dom::MediaStreamConstraints;
using dom::MediaStreamError;
using dom::MediaStreamTrack;
using dom::MediaStreamTrackSource;
using dom::MediaTrackConstraints;
using dom::MediaTrackConstraintSet;
using dom::MediaTrackSettings;
using dom::OwningBooleanOrMediaTrackConstraints;
using dom::OwningStringOrStringSequence;
using dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters;
using dom::Promise;
using dom::Sequence;
using dom::UserActivation;
using dom::WindowGlobalChild;
using ConstDeviceSetPromise = MediaManager::ConstDeviceSetPromise;
using DeviceSetPromise = MediaManager::DeviceSetPromise;
using LocalDevicePromise = MediaManager::LocalDevicePromise;
using LocalDeviceSetPromise = MediaManager::LocalDeviceSetPromise;
using LocalMediaDeviceSetRefCnt = MediaManager::LocalMediaDeviceSetRefCnt;
using MediaDeviceSetRefCnt = MediaManager::MediaDeviceSetRefCnt;
using media::NewRunnableFrom;
using media::NewTaskFrom;
using media::Refcountable;

// Whether main thread actions of MediaManager shutdown (except for clearing
// of sSingleton) have completed.
static bool sHasMainThreadShutdown;

struct DeviceState {
  DeviceState(RefPtr<LocalMediaDevice> aDevice,
              RefPtr<LocalTrackSource> aTrackSource, bool aOffWhileDisabled)
      : mOffWhileDisabled(aOffWhileDisabled),
        mDevice(std::move(aDevice)),
        mTrackSource(std::move(aTrackSource)) {
    MOZ_ASSERT(mDevice);
    MOZ_ASSERT(mTrackSource);
  }

  // true if we have stopped mDevice, this is a terminal state.
  // MainThread only.
  bool mStopped = false;

  // true if mDevice is currently enabled.
  // A device must be both enabled and unmuted to be turned on and capturing.
  // MainThread only.
  bool mDeviceEnabled = false;

  // true if mDevice is currently muted.
  // A device that is either muted or disabled is turned off and not capturing.
  // MainThread only.
  bool mDeviceMuted;

  // true if the application has currently enabled mDevice.
  // MainThread only.
  bool mTrackEnabled = false;

  // Time when the application last enabled mDevice.
  // MainThread only.
  TimeStamp mTrackEnabledTime;

  // 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. Only affects disabling; always turns off on user-agent mute.
  // 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 = new MediaTimer();

  // The underlying device we keep state for. Always non-null.
  // Threadsafe access, but see method declarations for individual constraints.
  const RefPtr<LocalMediaDevice> mDevice;

  // The MediaStreamTrackSource for any tracks (original and clones) originating
  // from this device. Always non-null. Threadsafe access, but see method
  // declarations for individual constraints.
  const RefPtr<LocalTrackSource> mTrackSource;
};

/**
 * This mimics the capture state from nsIMediaManagerService.
 */
enum class CaptureState : uint16_t {
  Off = nsIMediaManagerService::STATE_NOCAPTURE,
  Enabled = nsIMediaManagerService::STATE_CAPTURE_ENABLED,
  Disabled = nsIMediaManagerService::STATE_CAPTURE_DISABLED,
};

static CaptureState CombineCaptureState(CaptureState aFirst,
                                        CaptureState aSecond) {
  if (aFirst == CaptureState::Enabled || aSecond == CaptureState::Enabled) {
    return CaptureState::Enabled;
  }
  if (aFirst == CaptureState::Disabled || aSecond == CaptureState::Disabled) {
    return CaptureState::Disabled;
  }
  MOZ_ASSERT(aFirst == CaptureState::Off);
  MOZ_ASSERT(aSecond == CaptureState::Off);
  return CaptureState::Off;
}

static uint16_t FromCaptureState(CaptureState aState) {
  MOZ_ASSERT(aState == CaptureState::Off || aState == CaptureState::Enabled ||
             aState == CaptureState::Disabled);
  return static_cast<uint16_t>(aState);
}

void MediaManager::CallOnError(GetUserMediaErrorCallback& aCallback,
                               MediaStreamError& aError) {
  aCallback.Call(aError);
}

void MediaManager::CallOnSuccess(GetUserMediaSuccessCallback& aCallback,
                                 DOMMediaStream& aStream) {
  aCallback.Call(aStream);
}

/**
 * DeviceListener has threadsafe refcounting for use across the main, media and
 * MTG threads. But it has a non-threadsafe SupportsWeakPtr for WeakPtr usage
 * only from main thread, to ensure that garbage- and cycle-collected objects
 * don't hold a reference to it during late shutdown.
 */
class DeviceListener : public SupportsWeakPtr {
 public:
  typedef MozPromise<bool /* aIgnored */, RefPtr<MediaMgrError>, true>
      DeviceListenerPromise;

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
      DeviceListener)

  DeviceListener();

  /**
   * Registers this device listener as belonging to the given window listener.
   * Stop() must be called on registered DeviceListeners before destruction.
   */
  void Register(GetUserMediaWindowListener* aListener);

  /**
   * Marks this listener as active and creates the internal device state.
   */
  void Activate(RefPtr<LocalMediaDevice> aDevice,
                RefPtr<LocalTrackSource> aTrackSource, bool aStartMuted);

  /**
   * Posts a task to initialize and start the associated device.
   */
  RefPtr<DeviceListenerPromise> InitializeAsync();

  /**
   * Posts a task to stop the device associated with this DeviceListener and
   * notifies the associated window listener that a track was stopped.
   *
   * This will also clean up the weak reference to the associated window
   * listener, and tell the window listener to remove its hard reference to this
   * DeviceListener, so any caller will need to keep its own hard ref.
   */
  void Stop();

  /**
   * Gets the main thread MediaTrackSettings from the MediaEngineSource
   * associated with aTrack.
   */
  void GetSettings(MediaTrackSettings& aOutSettings) const;

  /**
   * Posts a task to set the enabled state of the device associated with this
   * DeviceListener 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 SetDeviceEnabled(bool aEnabled);

  /**
   * Posts a task to set the muted state of the device associated with this
   * DeviceListener to aMuted and notifies the associated window listener that a
   * track's state has changed.
   *
   * Turning the hardware off while the device is muted 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.
   */
  void SetDeviceMuted(bool aMuted);

  /**
   * Mutes or unmutes the associated video device if it is a camera.
   */
  void MuteOrUnmuteCamera(bool aMute);
  void MuteOrUnmuteMicrophone(bool aMute);

  LocalMediaDevice* GetDevice() const {
    return mDeviceState ? mDeviceState->mDevice.get() : nullptr;
  }

  bool Activated() const { return static_cast<bool>(mDeviceState); }

  bool Stopped() const { return mStopped; }

  bool CapturingVideo() const;

  bool CapturingAudio() const;

  CaptureState CapturingSource(MediaSourceEnum aSource) const;

  RefPtr<DeviceListenerPromise> ApplyConstraints(
      const MediaTrackConstraints& aConstraints, CallerType aCallerType);

  PrincipalHandle GetPrincipalHandle() const;

  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
    size_t amount = aMallocSizeOf(this);
    // Assume mPrincipalHandle refers to a principal owned elsewhere.
    // DeviceState does not have support for memory accounting.
    return amount;
  }

 private:
  virtual ~DeviceListener() {
    MOZ_ASSERT(mStopped);
    MOZ_ASSERT(!mWindowListener);
  }

  using DeviceOperationPromise =
      MozPromise<nsresult, bool, /* IsExclusive = */ true>;

  /**
   * Posts a task to start or stop the device associated with aTrack, based on
   * a passed-in boolean. Private method used by SetDeviceEnabled and
   * SetDeviceMuted.
   */
  RefPtr<DeviceOperationPromise> UpdateDevice(bool aOn);

  // true after this listener has had all devices stopped. MainThread only.
  bool mStopped;

  // 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;

  // Accessed from MediaTrackGraph thread, MediaManager thread, and MainThread
  // No locking needed as it's set on Activate() and never assigned to again.
  UniquePtr<DeviceState> mDeviceState;
};

/**
 * This class represents a WindowID and handles all MediaTrackListeners
 * (here subclassed as DeviceListeners) used to feed GetUserMedia tracks.
 * It proxies feedback from them into messages for browser chrome.
 * The DeviceListeners are used to Start() and Stop() the underlying
 * MediaEngineSource when MediaStreams are assigned and deassigned in content.
 */
class GetUserMediaWindowListener {
  friend MediaManager;

 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GetUserMediaWindowListener)

  // Create in an inactive state
  GetUserMediaWindowListener(uint64_t aWindowID,
                             const PrincipalHandle& aPrincipalHandle)
      : mWindowID(aWindowID),
        mPrincipalHandle(aPrincipalHandle),
        mChromeNotificationTaskPosted(false) {}

  /**
   * Registers an inactive gUM device listener for this WindowListener.
   */
  void Register(RefPtr<DeviceListener> aListener) {
    MOZ_ASSERT(NS_IsMainThread());
    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(std::move(aListener));
  }

  /**
   * Activates an already registered and inactive gUM device listener for this
   * WindowListener.
   */
  void Activate(RefPtr<DeviceListener> aListener,
                RefPtr<LocalMediaDevice> aDevice,
                RefPtr<LocalTrackSource> aTrackSource) {
    MOZ_ASSERT(NS_IsMainThread());
    MOZ_ASSERT(aListener);
    MOZ_ASSERT(!aListener->Activated());
    MOZ_ASSERT(mInactiveListeners.Contains(aListener),
               "Must be registered to activate");
    MOZ_ASSERT(!mActiveListeners.Contains(aListener), "Already activated");

    bool muted = false;
    if (aDevice->Kind() == MediaDeviceKind::Videoinput) {
      muted = mCamerasAreMuted;
    } else if (aDevice->Kind() == MediaDeviceKind::Audioinput) {
      muted = mMicrophonesAreMuted;
    } else {
      MOZ_CRASH("Unexpected device kind");
    }

    mInactiveListeners.RemoveElement(aListener);
    aListener->Activate(std::move(aDevice), std::move(aTrackSource), muted);
    mActiveListeners.AppendElement(std::move(aListener));
  }

  /**
   * Removes all DeviceListeners from this window listener.
   * Removes this window listener from the list of active windows, so callers
   * need to make sure to hold a strong reference.
   */
  void RemoveAll() {
    MOZ_ASSERT(NS_IsMainThread());

    for (auto& l : mInactiveListeners.Clone()) {
      Remove(l);
    }
    for (auto& l : mActiveListeners.Clone()) {
      Remove(l);
    }
    MOZ_ASSERT(mInactiveListeners.Length() == 0);
    MOZ_ASSERT(mActiveListeners.Length() == 0);

    MediaManager* mgr = MediaManager::GetIfExists();
    if (!mgr) {
      MOZ_ASSERT(false, "MediaManager should stay until everything is removed");
      return;
    }
    GetUserMediaWindowListener* windowListener =
        mgr->GetWindowListener(mWindowID);

    if (!windowListener) {
      nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
      auto* globalWindow = nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);
      if (globalWindow) {
        auto req = MakeRefPtr<GetUserMediaRequest>(
            globalWindow, VoidString(), VoidString(),
            UserActivation::IsHandlingUserInput());
        obs->NotifyWhenScriptSafe(req, "recording-device-stopped", nullptr);
      }
      return;
    }

    MOZ_ASSERT(windowListener == this,
               "There should only be one window listener per window ID");

    LOG("GUMWindowListener %p removing windowID %" PRIu64, this, mWindowID);
    mgr->RemoveWindowID(mWindowID);
  }

  /**
   * Removes a listener from our lists. Safe to call without holding a hard
   * reference. That said, you'll still want to iterate on a copy of said lists,
   * if you end up calling this method (or methods that may call this method) in
   * the loop, to avoid inadvertently skipping members.
   *
   * For use only from GetUserMediaWindowListener and DeviceListener.
   */
  bool Remove(RefPtr<DeviceListener> aListener) {
    // We refcount aListener on entry since we're going to proxy-release it
    // below to prevent the refcount going to zero on callers who might be
    // inside the listener, but operating without a hard reference to self.
    MOZ_ASSERT(NS_IsMainThread());

    if (!mInactiveListeners.RemoveElement(aListener) &&
        !mActiveListeners.RemoveElement(aListener)) {
      return false;
    }
    MOZ_ASSERT(!mInactiveListeners.Contains(aListener),
               "A DeviceListener should only be once in one of "
               "mInactiveListeners and mActiveListeners");
    MOZ_ASSERT(!mActiveListeners.Contains(aListener),
               "A DeviceListener should only be once in one of "
               "mInactiveListeners and mActiveListeners");

    LOG("GUMWindowListener %p stopping DeviceListener %p.", this,
        aListener.get());
    aListener->Stop();

    if (LocalMediaDevice* removedDevice = aListener->GetDevice()) {
      bool revokePermission = true;
      nsString removedRawId;
      nsString removedSourceType;
      removedDevice->GetRawId(removedRawId);
      removedDevice->GetMediaSource(removedSourceType);

      for (const auto& l : mActiveListeners) {
        if (LocalMediaDevice* device = l->GetDevice()) {
          nsString rawId;
          device->GetRawId(rawId);
          if (removedRawId.Equals(rawId)) {
            revokePermission = false;
            break;
          }
        }
      }

      if (revokePermission) {
        nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
        auto* window = nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);
        auto req = MakeRefPtr<GetUserMediaRequest>(
            window, removedRawId, removedSourceType,
            UserActivation::IsHandlingUserInput());
        obs->NotifyWhenScriptSafe(req, "recording-device-stopped", nullptr);
      }
    }

    if (mInactiveListeners.Length() == 0 && mActiveListeners.Length() == 0) {
      LOG("GUMWindowListener %p Removed last DeviceListener. Cleaning up.",
          this);
      RemoveAll();
    }

    nsCOMPtr<nsIEventTarget> mainTarget = do_GetMainThread();
    // To allow being invoked by callers not holding a strong reference to self,
    // hold the listener alive until the stack has unwound, by always
    // dispatching a runnable (aAlwaysProxy = true)
    NS_ProxyRelease(__func__, mainTarget, aListener.forget(), true);
    return true;
  }

  /**
   * Stops all screen/window/audioCapture sharing, but not camera or microphone.
   */
  void StopSharing();

  void StopRawID(const nsString& removedDeviceID);

  void MuteOrUnmuteCameras(bool aMute);
  void MuteOrUnmuteMicrophones(bool aMute);

  /**
   * Called by one of our DeviceListeners when one of its tracks has changed so
   * that chrome state is affected.
   * Schedules an event for the next stable state to update chrome.
   */
  void ChromeAffectingStateChanged();

  /**
   * Called in stable state to send a notification to update chrome.
   */
  void NotifyChrome();

  bool CapturingVideo() const {
    MOZ_ASSERT(NS_IsMainThread());
    for (auto& l : mActiveListeners) {
      if (l->CapturingVideo()) {
        return true;
      }
    }
    return false;
  }

  bool CapturingAudio() const {
    MOZ_ASSERT(NS_IsMainThread());
    for (auto& l : mActiveListeners) {
      if (l->CapturingAudio()) {
        return true;
      }
    }
    return false;
  }

  CaptureState CapturingSource(MediaSourceEnum aSource) const {
    MOZ_ASSERT(NS_IsMainThread());
    CaptureState result = CaptureState::Off;
    for (auto& l : mActiveListeners) {
      result = CombineCaptureState(result, l->CapturingSource(aSource));
    }
    return result;
  }

  RefPtr<LocalMediaDeviceSetRefCnt> GetDevices() {
    RefPtr devices = new LocalMediaDeviceSetRefCnt();
    for (auto& l : mActiveListeners) {
      devices->AppendElement(l->GetDevice());
    }
    return devices;
  }

  uint64_t WindowID() const { return mWindowID; }

  PrincipalHandle GetPrincipalHandle() const { return mPrincipalHandle; }

  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
    size_t amount = aMallocSizeOf(this);
    // Assume mPrincipalHandle refers to a principal owned elsewhere.
    amount += mInactiveListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
    for (const RefPtr<DeviceListener>& listener : mInactiveListeners) {
      amount += listener->SizeOfIncludingThis(aMallocSizeOf);
    }
    amount += mActiveListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
    for (const RefPtr<DeviceListener>& listener : mActiveListeners) {
      amount += listener->SizeOfIncludingThis(aMallocSizeOf);
    }
    return amount;
  }

 private:
  ~GetUserMediaWindowListener() {
    MOZ_ASSERT(mInactiveListeners.Length() == 0,
               "Inactive listeners should already be removed");
    MOZ_ASSERT(mActiveListeners.Length() == 0,
               "Active listeners should already be removed");
  }

  uint64_t mWindowID;
  const PrincipalHandle mPrincipalHandle;

  // true if we have scheduled a task to notify chrome in the next stable state.
  // The task will reset this to false. MainThread only.
  bool mChromeNotificationTaskPosted;

  nsTArray<RefPtr<DeviceListener>> mInactiveListeners;
  nsTArray<RefPtr<DeviceListener>> mActiveListeners;

  // Whether camera and microphone access in this window are currently
  // User Agent (UA) muted. When true, new and cloned tracks must start
  // out muted, to avoid JS circumventing UA mute. Per-camera and
  // per-microphone UA muting is not supported.
  bool mCamerasAreMuted = false;
  bool mMicrophonesAreMuted = false;
};

class LocalTrackSource : public MediaStreamTrackSource {
 public:
  LocalTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel,
                   const RefPtr<DeviceListener>& aListener,
                   MediaSourceEnum aSource, MediaTrack* aTrack,
                   RefPtr<PeerIdentity> aPeerIdentity,
                   TrackingId aTrackingId = TrackingId())
      : MediaStreamTrackSource(aPrincipal, aLabel, std::move(aTrackingId)),
        mSource(aSource),
        mTrack(aTrack),
        mPeerIdentity(std::move(aPeerIdentity)),
        mListener(aListener.get()) {}

  MediaSourceEnum GetMediaSource() const override { return mSource; }

  const PeerIdentity* GetPeerIdentity() const override { return mPeerIdentity; }

  RefPtr<MediaStreamTrackSource::ApplyConstraintsPromise> ApplyConstraints(
      const MediaTrackConstraints& aConstraints,
      CallerType aCallerType) override {
    MOZ_ASSERT(NS_IsMainThread());
    if (sHasMainThreadShutdown || !mListener) {
      // Track has been stopped, or we are in shutdown. In either case
      // there's no observable outcome, so pretend we succeeded.
      return MediaStreamTrackSource::ApplyConstraintsPromise::CreateAndResolve(
          false, __func__);
    }
    return mListener->ApplyConstraints(aConstraints, aCallerType);
  }

  void GetSettings(MediaTrackSettings& aOutSettings) override {
    if (mListener) {
      mListener->GetSettings(aOutSettings);
    }
  }

  void Stop() override {
    if (mListener) {
      mListener->Stop();
      mListener = nullptr;
    }
    if (!mTrack->IsDestroyed()) {
      mTrack->Destroy();
    }
  }

  void Disable() override {
    if (mListener) {
      mListener->SetDeviceEnabled(false);
    }
  }

  void Enable() override {
    if (mListener) {
      mListener->SetDeviceEnabled(true);
    }
  }

  void Mute() {
    MutedChanged(true);
    mTrack->SetDisabledTrackMode(DisabledTrackMode::SILENCE_BLACK);
  }

  void Unmute() {
    MutedChanged(false);
    mTrack->SetDisabledTrackMode(DisabledTrackMode::ENABLED);
  }

  const MediaSourceEnum mSource;
  const RefPtr<MediaTrack> mTrack;
  const RefPtr<const PeerIdentity> mPeerIdentity;

 protected:
  ~LocalTrackSource() {
    MOZ_ASSERT(NS_IsMainThread());
    MOZ_ASSERT(mTrack->IsDestroyed());
  }

  // This is a weak pointer to avoid having the DeviceListener (which may
  // have references to threads and threadpools) kept alive by DOM-objects
  // that may have ref-cycles and thus are released very late during
  // shutdown, even after xpcom-shutdown-threads. See bug 1351655 for what
  // can happen.
  WeakPtr<DeviceListener> mListener;
};

class AudioCaptureTrackSource : public LocalTrackSource {
 public:
  AudioCaptureTrackSource(nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aWindow,
                          const nsString& aLabel,
                          AudioCaptureTrack* aAudioCaptureTrack,
                          RefPtr<PeerIdentity> aPeerIdentity)
      : LocalTrackSource(aPrincipal, aLabel, nullptr,
                         MediaSourceEnum::AudioCapture, aAudioCaptureTrack,
                         std::move(aPeerIdentity)),
        mWindow(aWindow),
        mAudioCaptureTrack(aAudioCaptureTrack) {
    mAudioCaptureTrack->Start();
    mAudioCaptureTrack->Graph()->RegisterCaptureTrackForWindow(
        mWindow->WindowID(), mAudioCaptureTrack);
    mWindow->SetAudioCapture(true);
  }

  void Stop() override {
    MOZ_ASSERT(NS_IsMainThread());
    if (!mAudioCaptureTrack->IsDestroyed()) {
      MOZ_ASSERT(mWindow);
      mWindow->SetAudioCapture(false);
      mAudioCaptureTrack->Graph()->UnregisterCaptureTrackForWindow(
          mWindow->WindowID());
      mWindow = nullptr;
    }
    // LocalTrackSource destroys the track.
    LocalTrackSource::Stop();
    MOZ_ASSERT(mAudioCaptureTrack->IsDestroyed());
  }

  ProcessedMediaTrack* InputTrack() const { return mAudioCaptureTrack.get(); }

 protected:
  ~AudioCaptureTrackSource() {
    MOZ_ASSERT(NS_IsMainThread());
    MOZ_ASSERT(mAudioCaptureTrack->IsDestroyed());
  }

  RefPtr<nsPIDOMWindowInner> mWindow;
  const RefPtr<AudioCaptureTrack> mAudioCaptureTrack;
};

/**
 * nsIMediaDevice implementation.
 */
NS_IMPL_ISUPPORTS(LocalMediaDevice, nsIMediaDevice)

MediaDevice::MediaDevice(MediaEngine* aEngine, MediaSourceEnum aMediaSource,
                         const nsString& aRawName, const nsString& aRawID,
                         const nsString& aRawGroupID, IsScary aIsScary,
                         const OsPromptable canRequestOsLevelPrompt)
    : mEngine(aEngine),
      mAudioDeviceInfo(nullptr),
      mMediaSource(aMediaSource),
      mKind(MediaEngineSource::IsVideo(aMediaSource)
                ? MediaDeviceKind::Videoinput
                : MediaDeviceKind::Audioinput),
      mScary(aIsScary == IsScary::Yes),
      mCanRequestOsLevelPrompt(canRequestOsLevelPrompt == OsPromptable::Yes),
      mIsFake(mEngine->IsFake()),
      mType(
          NS_ConvertASCIItoUTF16(dom::MediaDeviceKindValues::GetString(mKind))),
      mRawID(aRawID),
      mRawGroupID(aRawGroupID),
      mRawName(aRawName) {
  MOZ_ASSERT(mEngine);
}

MediaDevice::MediaDevice(MediaEngine* aEngine,
                         const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
                         const nsString& aRawID)
    : mEngine(aEngine),
      mAudioDeviceInfo(aAudioDeviceInfo),
      mMediaSource(mAudioDeviceInfo->Type() == AudioDeviceInfo::TYPE_INPUT
                       ? MediaSourceEnum::Microphone
                       : MediaSourceEnum::Other),
      mKind(mMediaSource == MediaSourceEnum::Microphone
                ? MediaDeviceKind::Audioinput
                : MediaDeviceKind::Audiooutput),
      mScary(false),
      mCanRequestOsLevelPrompt(false),
      mIsFake(false),
      mType(
          NS_ConvertASCIItoUTF16(dom::MediaDeviceKindValues::GetString(mKind))),
      mRawID(aRawID),
      mRawGroupID(mAudioDeviceInfo->GroupID()),
      mRawName(mAudioDeviceInfo->Name()) {}

/* static */
RefPtr<MediaDevice> MediaDevice::CopyWithNewRawGroupId(
    const RefPtr<MediaDevice>& aOther, const nsString& aRawGroupID) {
  MOZ_ASSERT(!aOther->mAudioDeviceInfo, "device not supported");
  return new MediaDevice(aOther->mEngine, aOther->mMediaSource,
                         aOther->mRawName, aOther->mRawID, aRawGroupID,
                         IsScary(aOther->mScary),
                         OsPromptable(aOther->mCanRequestOsLevelPrompt));
}

MediaDevice::~MediaDevice() = default;

LocalMediaDevice::LocalMediaDevice(RefPtr<const MediaDevice> aRawDevice,
                                   const nsString& aID,
                                   const nsString& aGroupID,
                                   const nsString& aName)
    : mRawDevice(std::move(aRawDevice)),
      mName(aName),
      mID(aID),
      mGroupID(aGroupID) {
  MOZ_ASSERT(mRawDevice);
}

/**
 * Helper functions that implement the constraints algorithm from
 * http://dev.w3.org/2011/webrtc/editor/getusermedia.html#methods-5
 */

/* static */
bool LocalMediaDevice::StringsContain(
    const OwningStringOrStringSequence& aStrings, nsString aN) {
  return aStrings.IsString() ? aStrings.GetAsString() == aN
                             : aStrings.GetAsStringSequence().Contains(aN);
}

/* static */
uint32_t LocalMediaDevice::FitnessDistance(
    nsString aN, const ConstrainDOMStringParameters& aParams) {
  if (aParams.mExact.WasPassed() &&
      !StringsContain(aParams.mExact.Value(), aN)) {
    return UINT32_MAX;
  }
  if (aParams.mIdeal.WasPassed() &&
      !StringsContain(aParams.mIdeal.Value(), aN)) {
    return 1;
  }
  return 0;
}

// Binding code doesn't templatize well...

/* static */
uint32_t LocalMediaDevice::FitnessDistance(
    nsString aN,
    const OwningStringOrStringSequenceOrConstrainDOMStringParameters&
        aConstraint) {
  if (aConstraint.IsString()) {
    ConstrainDOMStringParameters params;
    params.mIdeal.Construct();
    params.mIdeal.Value().SetAsString() = aConstraint.GetAsString();
    return FitnessDistance(aN, params);
  } else if (aConstraint.IsStringSequence()) {
    ConstrainDOMStringParameters params;
    params.mIdeal.Construct();
    params.mIdeal.Value().SetAsStringSequence() =
        aConstraint.GetAsStringSequence();
    return FitnessDistance(aN, params);
  } else {
    return FitnessDistance(aN, aConstraint.GetAsConstrainDOMStringParameters());
  }
}

uint32_t LocalMediaDevice::GetBestFitnessDistance(
    const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
    CallerType aCallerType) {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  MOZ_ASSERT(GetMediaSource() != MediaSourceEnum::Other);

  bool isChrome = aCallerType == CallerType::System;
  const nsString& id = isChrome ? RawID() : mID;
  auto type = GetMediaSource();
  uint64_t distance = 0;
  if (!aConstraintSets.IsEmpty()) {
    if (isChrome /* For the screen/window sharing preview */ ||
        type == MediaSourceEnum::Camera ||
        type == MediaSourceEnum::Microphone) {
      distance += uint64_t(MediaConstraintsHelper::FitnessDistance(
                      Some(id), aConstraintSets[0]->mDeviceId)) +
                  uint64_t(MediaConstraintsHelper::FitnessDistance(
                      Some(mGroupID), aConstraintSets[0]->mGroupId));
    }
  }
  if (distance < UINT32_MAX) {
    // Forward request to underlying object to interrogate per-mode
    // capabilities.
    distance += Source()->GetBestFitnessDistance(aConstraintSets);
  }
  return std::min<uint64_t>(distance, UINT32_MAX);
}

NS_IMETHODIMP
LocalMediaDevice::GetRawName(nsAString& aName) {
  MOZ_ASSERT(NS_IsMainThread());
  aName.Assign(mRawDevice->mRawName);
  return NS_OK;
}

NS_IMETHODIMP
LocalMediaDevice::GetType(nsAString& aType) {
  MOZ_ASSERT(NS_IsMainThread());
  aType.Assign(mRawDevice->mType);
  return NS_OK;
}

NS_IMETHODIMP
LocalMediaDevice::GetRawId(nsAString& aID) {
  MOZ_ASSERT(NS_IsMainThread());
  aID.Assign(RawID());
  return NS_OK;
}

NS_IMETHODIMP
LocalMediaDevice::GetId(nsAString& aID) {
  MOZ_ASSERT(NS_IsMainThread());
  aID.Assign(mID);
  return NS_OK;
}

NS_IMETHODIMP
LocalMediaDevice::GetScary(bool* aScary) {
  *aScary = mRawDevice->mScary;
  return NS_OK;
}

NS_IMETHODIMP
LocalMediaDevice::GetCanRequestOsLevelPrompt(bool* aCanRequestOsLevelPrompt) {
  *aCanRequestOsLevelPrompt = mRawDevice->mCanRequestOsLevelPrompt;
  return NS_OK;
}

void LocalMediaDevice::GetSettings(MediaTrackSettings& aOutSettings) {
  MOZ_ASSERT(NS_IsMainThread());
  Source()->GetSettings(aOutSettings);
}

MediaEngineSource* LocalMediaDevice::Source() {
  if (!mSource) {
    mSource = mRawDevice->mEngine->CreateSource(mRawDevice);
  }
  return mSource;
}

const TrackingId& LocalMediaDevice::GetTrackingId() const {
  return mSource->GetTrackingId();
}

// Threadsafe since mKind and mSource are const.
NS_IMETHODIMP
LocalMediaDevice::GetMediaSource(nsAString& aMediaSource) {
  if (Kind() == MediaDeviceKind::Audiooutput) {
    aMediaSource.Truncate();
  } else {
    aMediaSource.AssignASCII(
        dom::MediaSourceEnumValues::GetString(GetMediaSource()));
  }
  return NS_OK;
}

nsresult LocalMediaDevice::Allocate(const MediaTrackConstraints& aConstraints,
                                    const MediaEnginePrefs& aPrefs,
                                    uint64_t aWindowID,
                                    const char** aOutBadConstraint) {
  MOZ_ASSERT(MediaManager::IsInMediaThread());

  // Mock failure for automated tests.
  if (IsFake() && aConstraints.mDeviceId.WasPassed() &&
      aConstraints.mDeviceId.Value().IsString() &&
      aConstraints.mDeviceId.Value().GetAsString().EqualsASCII("bad device")) {
    return NS_ERROR_FAILURE;
  }

  return Source()->Allocate(aConstraints, aPrefs, aWindowID, aOutBadConstraint);
}

void LocalMediaDevice::SetTrack(const RefPtr<MediaTrack>& aTrack,
                                const PrincipalHandle& aPrincipalHandle) {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  Source()->SetTrack(aTrack, aPrincipalHandle);
}

nsresult LocalMediaDevice::Start() {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  MOZ_ASSERT(Source());
  return Source()->Start();
}

nsresult LocalMediaDevice::Reconfigure(
    const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
    const char** aOutBadConstraint) {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  auto type = GetMediaSource();
  if (type == MediaSourceEnum::Camera || type == MediaSourceEnum::Microphone) {
    NormalizedConstraints c(aConstraints);
    if (MediaConstraintsHelper::FitnessDistance(Some(mID), c.mDeviceId) ==
        UINT32_MAX) {
      *aOutBadConstraint = "deviceId";
      return NS_ERROR_INVALID_ARG;
    }
    if (MediaConstraintsHelper::FitnessDistance(Some(mGroupID), c.mGroupId) ==
        UINT32_MAX) {
      *aOutBadConstraint = "groupId";
      return NS_ERROR_INVALID_ARG;
    }
  }
  return Source()->Reconfigure(aConstraints, aPrefs, aOutBadConstraint);
}

nsresult LocalMediaDevice::FocusOnSelectedSource() {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  return Source()->FocusOnSelectedSource();
}

nsresult LocalMediaDevice::Stop() {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  MOZ_ASSERT(mSource);
  return mSource->Stop();
}

nsresult LocalMediaDevice::Deallocate() {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  MOZ_ASSERT(mSource);
  return mSource->Deallocate();
}

MediaSourceEnum MediaDevice::GetMediaSource() const { return mMediaSource; }

static const MediaTrackConstraints& GetInvariant(
    const OwningBooleanOrMediaTrackConstraints& aUnion) {
  static const MediaTrackConstraints empty;
  return aUnion.IsMediaTrackConstraints() ? aUnion.GetAsMediaTrackConstraints()
                                          : empty;
}

// Source getter returning full list

static void GetMediaDevices(MediaEngine* aEngine, MediaSourceEnum aSrcType,
                            MediaManager::MediaDeviceSet& aResult,
                            const char* aMediaDeviceName = nullptr) {
  MOZ_ASSERT(MediaManager::IsInMediaThread());

  LOG("%s: aEngine=%p, aSrcType=%" PRIu8 ", aMediaDeviceName=%s", __func__,
      aEngine, static_cast<uint8_t>(aSrcType),
      aMediaDeviceName ? aMediaDeviceName : "null");
  nsTArray<RefPtr<MediaDevice>> devices;
  aEngine->EnumerateDevices(aSrcType, MediaSinkEnum::Other, &devices);

  /*
   * 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.
   */
  if (aMediaDeviceName && *aMediaDeviceName) {
    for (auto& device : devices) {
      if (device->mRawName.EqualsASCII(aMediaDeviceName)) {
        aResult.AppendElement(device);
        LOG("%s: found aMediaDeviceName=%s", __func__, aMediaDeviceName);
        break;
      }
    }
  } else {
    aResult = std::move(devices);
    if (MOZ_LOG_TEST(gMediaManagerLog, mozilla::LogLevel::Debug)) {
      for (auto& device : aResult) {
        LOG("%s: appending device=%s", __func__,
            NS_ConvertUTF16toUTF8(device->mRawName).get());
      }
    }
  }
}

RefPtr<LocalDeviceSetPromise> MediaManager::SelectSettings(
    const MediaStreamConstraints& aConstraints, CallerType aCallerType,
    RefPtr<LocalMediaDeviceSetRefCnt> aDevices) {
  MOZ_ASSERT(NS_IsMainThread());

  // Algorithm accesses device capabilities code and must run on media thread.
  // Modifies passed-in aDevices.

  return MediaManager::Dispatch<LocalDeviceSetPromise>(
      __func__, [aConstraints, devices = std::move(aDevices),
                 aCallerType](MozPromiseHolder<LocalDeviceSetPromise>& holder) {
        auto& devicesRef = *devices;

        // Since the advanced part of the constraints algorithm needs to know
        // when a candidate set is overconstrained (zero members), we must split
        // up the list into videos and audios, and put it back together again at
        // the end.

        nsTArray<RefPtr<LocalMediaDevice>> videos;
        nsTArray<RefPtr<LocalMediaDevice>> audios;

        for (const auto& device : devicesRef) {
          MOZ_ASSERT(device->Kind() == MediaDeviceKind::Videoinput ||
                     device->Kind() == MediaDeviceKind::Audioinput);
          if (device->Kind() == MediaDeviceKind::Videoinput) {
            videos.AppendElement(device);
          } else if (device->Kind() == MediaDeviceKind::Audioinput) {
            audios.AppendElement(device);
          }
        }
        devicesRef.Clear();
        const char* badConstraint = nullptr;
        bool needVideo = IsOn(aConstraints.mVideo);
        bool needAudio = IsOn(aConstraints.mAudio);

        if (needVideo && videos.Length()) {
          badConstraint = MediaConstraintsHelper::SelectSettings(
              NormalizedConstraints(GetInvariant(aConstraints.mVideo)), videos,
              aCallerType);
        }
        if (!badConstraint && needAudio && audios.Length()) {
          badConstraint = MediaConstraintsHelper::SelectSettings(
              NormalizedConstraints(GetInvariant(aConstraints.mAudio)), audios,
              aCallerType);
        }
        if (badConstraint) {
          LOG("SelectSettings: bad constraint found! Calling error handler!");
          nsString constraint;
          constraint.AssignASCII(badConstraint);
          holder.Reject(
              new MediaMgrError(MediaMgrError::Name::OverconstrainedError, "",
                                constraint),
              __func__);
          return;
        }
        if (!needVideo == !videos.Length() && !needAudio == !audios.Length()) {
          for (auto& video : videos) {
            devicesRef.AppendElement(video);
          }
          for (auto& audio : audios) {
            devicesRef.AppendElement(audio);
          }
        }
        holder.Resolve(devices, __func__);
      });
}

/**
 * Describes a requested task that handles response from the UI and sends
 * results back to the DOM.
 */
class GetUserMediaTask {
 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GetUserMediaTask)
  GetUserMediaTask(uint64_t aWindowID, const ipc::PrincipalInfo& aPrincipalInfo,
                   CallerType aCallerType)
      : mPrincipalInfo(aPrincipalInfo),
        mWindowID(aWindowID),
        mCallerType(aCallerType) {}

  virtual void Denied(MediaMgrError::Name aName,
                      const nsCString& aMessage = ""_ns) = 0;

  virtual GetUserMediaStreamTask* AsGetUserMediaStreamTask() { return nullptr; }
  virtual SelectAudioOutputTask* AsSelectAudioOutputTask() { return nullptr; }

  uint64_t GetWindowID() const { return mWindowID; }
  enum CallerType CallerType() const { return mCallerType; }

  size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
    size_t amount = aMallocSizeOf(this);
    // Assume mWindowListener is owned by MediaManager.
    // Assume mAudioDeviceListener and mVideoDeviceListener are owned by
    // mWindowListener.
    // Assume PrincipalInfo string buffers are shared.
    // Member types without support for accounting of pointees:
    //   MozPromiseHolder, RefPtr<LocalMediaDevice>.
    // We don't have a good way to account for lambda captures for MozPromise
    // callbacks.
    return amount;
  }

 protected:
  virtual ~GetUserMediaTask() = default;

  // Call GetPrincipalKey again, if not private browing, this time with
  // persist = true, to promote deviceIds to persistent, in case they're not
  // already. Fire'n'forget.
  void PersistPrincipalKey() {
    if (IsPrincipalInfoPrivate(mPrincipalInfo)) {
      return;
    }
    media::GetPrincipalKey(mPrincipalInfo, true)
        ->Then(
            GetCurrentSerialEventTarget(), __func__,
            [](const media::PrincipalKeyPromise::ResolveOrRejectValue& aValue) {
              if (aValue.IsReject()) {
                LOG("Failed get Principal key. Persisting of deviceIds "
                    "will be broken");
              }
            });
  }

 private:
  // Thread-safe (object) principal of Window with ID mWindowID
  const ipc::PrincipalInfo mPrincipalInfo;

 protected:
  // The ID of the not-necessarily-toplevel inner Window relevant global
  // object of the MediaDevices on which getUserMedia() was called
  const uint64_t mWindowID;
  // Whether the JS caller of getUserMedia() has system (subject) principal
  const enum CallerType mCallerType;
};

/**
 * Describes a requested task that handles response from the UI to a
 * getUserMedia() request and sends results back to content.  If the request
 * is allowed and device initialization succeeds, then the MozPromise is
 * resolved with a DOMMediaStream having a track or tracks for the approved
 * device or devices.
 */
class GetUserMediaStreamTask final : public GetUserMediaTask {
 public:
  GetUserMediaStreamTask(
      const MediaStreamConstraints& aConstraints,
      MozPromiseHolder<MediaManager::StreamPromise>&& aHolder,
      uint64_t aWindowID, RefPtr<GetUserMediaWindowListener> aWindowListener,
      RefPtr<DeviceListener> aAudioDeviceListener,
      RefPtr<DeviceListener> aVideoDeviceListener,
      const MediaEnginePrefs& aPrefs, const ipc::PrincipalInfo& aPrincipalInfo,
      enum CallerType aCallerType, bool aShouldFocusSource)
      : GetUserMediaTask(aWindowID, aPrincipalInfo, aCallerType),
        mConstraints(aConstraints),
        mHolder(std::move(aHolder)),
        mWindowListener(std::move(aWindowListener)),
        mAudioDeviceListener(std::move(aAudioDeviceListener)),
        mVideoDeviceListener(std::move(aVideoDeviceListener)),
        mPrefs(aPrefs),
        mShouldFocusSource(aShouldFocusSource),
        mManager(MediaManager::GetInstance()) {}

  void Allowed(RefPtr<LocalMediaDevice> aAudioDevice,
               RefPtr<LocalMediaDevice> aVideoDevice) {
    MOZ_ASSERT(aAudioDevice || aVideoDevice);
    mAudioDevice = std::move(aAudioDevice);
    mVideoDevice = std::move(aVideoDevice);
    // Reuse the same thread to save memory.
    MediaManager::Dispatch(
        NewRunnableMethod("GetUserMediaStreamTask::AllocateDevices", this,
                          &GetUserMediaStreamTask::AllocateDevices));
  }

  GetUserMediaStreamTask* AsGetUserMediaStreamTask() override { return this; }

 private:
  ~GetUserMediaStreamTask() override {
    if (!mHolder.IsEmpty()) {
      Fail(MediaMgrError::Name::NotAllowedError);
    }
  }

  void Fail(MediaMgrError::Name aName, const nsCString& aMessage = ""_ns,
            const nsString& aConstraint = u""_ns) {
    mHolder.Reject(MakeRefPtr<MediaMgrError>(aName, aMessage, aConstraint),
                   __func__);
    // We add a disabled listener to the StreamListeners array until accepted
    // If this was the only active MediaStream, remove the window from the list.
    NS_DispatchToMainThread(NS_NewRunnableFunction(
        "DeviceListener::Stop",
        [audio = mAudioDeviceListener, video = mVideoDeviceListener] {
          if (audio) {
            audio->Stop();
          }
          if (video) {
            video->Stop();
          }
        }));
  }

  /**
   * Runs on a separate thread and is responsible for allocating devices.
   *
   * Do not run this on the main thread.
   */
  void AllocateDevices() {
    MOZ_ASSERT(!NS_IsMainThread());
    LOG("GetUserMediaStreamTask::AllocateDevices()");

    // Allocate a video or audio device and return a MediaStream via
    // PrepareDOMStream().

    nsresult rv;
    const char* errorMsg = nullptr;
    const char* badConstraint = nullptr;

    if (mAudioDevice) {
      auto& constraints = GetInvariant(mConstraints.mAudio);
      rv = mAudioDevice->Allocate(constraints, mPrefs, mWindowID,
                                  &badConstraint);
      if (NS_FAILED(rv)) {
        errorMsg = "Failed to allocate audiosource";
        if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
          nsTArray<RefPtr<LocalMediaDevice>> devices;
          devices.AppendElement(mAudioDevice);
          badConstraint = MediaConstraintsHelper::SelectSettings(
              NormalizedConstraints(constraints), devices, mCallerType);
        }
      }
    }
    if (!errorMsg && mVideoDevice) {
      auto& constraints = GetInvariant(mConstraints.mVideo);
      rv = mVideoDevice->Allocate(constraints, mPrefs, mWindowID,
                                  &badConstraint);
      if (NS_FAILED(rv)) {
        errorMsg = "Failed to allocate videosource";
        if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
          nsTArray<RefPtr<LocalMediaDevice>> devices;
          devices.AppendElement(mVideoDevice);
          badConstraint = MediaConstraintsHelper::SelectSettings(
              NormalizedConstraints(constraints), devices, mCallerType);
        }
        if (mAudioDevice) {
          mAudioDevice->Deallocate();
        }
      } else {
        mVideoTrackingId.emplace(mVideoDevice->GetTrackingId());
      }
    }
    if (errorMsg) {
      LOG("%s %" PRIu32, errorMsg, static_cast<uint32_t>(rv));
      if (badConstraint) {
        Fail(MediaMgrError::Name::OverconstrainedError, ""_ns,
             NS_ConvertUTF8toUTF16(badConstraint));
      } else {
        Fail(MediaMgrError::Name::NotReadableError, nsCString(errorMsg));
      }
      NS_DispatchToMainThread(
          NS_NewRunnableFunction("MediaManager::SendPendingGUMRequest", []() {
            if (MediaManager* manager = MediaManager::GetIfExists()) {
              manager->SendPendingGUMRequest();
            }
          }));
      return;
    }
    NS_DispatchToMainThread(
        NewRunnableMethod("GetUserMediaStreamTask::PrepareDOMStream", this,
                          &GetUserMediaStreamTask::PrepareDOMStream));
  }

 public:
  void Denied(MediaMgrError::Name aName, const nsCString& aMessage) override {
    MOZ_ASSERT(NS_IsMainThread());
    Fail(aName, aMessage);
  }

  const MediaStreamConstraints& GetConstraints() { return mConstraints; }

 private:
  void PrepareDOMStream();

  // Constraints derived from those passed to getUserMedia() but adjusted for
  // preferences, defaults, and security
  const MediaStreamConstraints mConstraints;

  MozPromiseHolder<MediaManager::StreamPromise> mHolder;
  // GetUserMediaWindowListener with which DeviceListeners are registered
  const RefPtr<GetUserMediaWindowListener> mWindowListener;
  const RefPtr<DeviceListener> mAudioDeviceListener;
  const RefPtr<DeviceListener> mVideoDeviceListener;
  // MediaDevices are set when selected and Allowed() by the UI.
  RefPtr<LocalMediaDevice> mAudioDevice;
  RefPtr<LocalMediaDevice> mVideoDevice;
  // Tracking id unique for a video frame source. Set when the corresponding
  // device has been allocated.
  Maybe<TrackingId> mVideoTrackingId;
  // Copy of MediaManager::mPrefs
  const MediaEnginePrefs mPrefs;
  // media.getusermedia.window.focus_source.enabled
  const bool mShouldFocusSource;
  // The MediaManager is referenced at construction so that it won't be
  // created after its ShutdownBlocker would run.
  const RefPtr<MediaManager> mManager;
};

/**
 * Creates a MediaTrack, attaches a listener and resolves a MozPromise to
 * provide the stream to the DOM.
 *
 * All of this must be done on the main thread!
 */
void GetUserMediaStreamTask::PrepareDOMStream() {
  MOZ_ASSERT(NS_IsMainThread());
  LOG("GetUserMediaStreamTask::PrepareDOMStream()");
  nsGlobalWindowInner* window =
      nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);

  // We're on main-thread, and the windowlist can only
  // be invalidated from the main-thread (see OnNavigation)
  if (!mManager->IsWindowListenerStillActive(mWindowListener)) {
    // This window is no longer live. mListener has already been removed.
    return;
  }

  MediaTrackGraph::GraphDriverType graphDriverType =
      mAudioDevice ? MediaTrackGraph::AUDIO_THREAD_DRIVER
                   : MediaTrackGraph::SYSTEM_THREAD_DRIVER;
  MediaTrackGraph* mtg = MediaTrackGraph::GetInstance(
      graphDriverType, window, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
      MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);

  auto domStream = MakeRefPtr<DOMMediaStream>(window);
  RefPtr<LocalTrackSource> audioTrackSource;
  RefPtr<LocalTrackSource> videoTrackSource;
  nsCOMPtr<nsIPrincipal> principal;
  RefPtr<PeerIdentity> peerIdentity = nullptr;
  if (!mConstraints.mPeerIdentity.IsEmpty()) {
    peerIdentity = new PeerIdentity(mConstraints.mPeerIdentity);
    principal = NullPrincipal::CreateWithInheritedAttributes(
        window->GetExtantDoc()->NodePrincipal());
  } else {
    principal = window->GetExtantDoc()->NodePrincipal();
  }
  RefPtr<GenericNonExclusivePromise> firstFramePromise;
  if (mAudioDevice) {
    if (mAudioDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) {
      // AudioCapture is a special case, here, in the sense that we're not
      // really using the audio source and the SourceMediaTrack, which acts
      // as placeholders. We re-route a number of tracks internally in the
      // MTG and mix them down instead.
      NS_WARNING(
          "MediaCaptureWindowState doesn't handle "
          "MediaSourceEnum::AudioCapture. This must be fixed with UX "
          "before shipping.");
      auto audioCaptureSource = MakeRefPtr<AudioCaptureTrackSource>(
          principal, window, u"Window audio capture"_ns,
          mtg->CreateAudioCaptureTrack(), peerIdentity);
      audioTrackSource = audioCaptureSource;
      RefPtr<MediaStreamTrack> track = new dom::AudioStreamTrack(
          window, audioCaptureSource->InputTrack(), audioCaptureSource);
      domStream->AddTrackInternal(track);
    } else {
      const nsString& audioDeviceName = mAudioDevice->mName;
      RefPtr<MediaTrack> track;
#ifdef MOZ_WEBRTC
      if (mAudioDevice->IsFake()) {
        track = mtg->CreateSourceTrack(MediaSegment::AUDIO);
      } else {
        track = AudioProcessingTrack::Create(mtg);
        track->Suspend();  // Microphone source resumes in SetTrack
      }
#else
      track = mtg->CreateSourceTrack(MediaSegment::AUDIO);
#endif
      audioTrackSource = new LocalTrackSource(
          principal, audioDeviceName, mAudioDeviceListener,
          mAudioDevice->GetMediaSource(), track, peerIdentity);
      MOZ_ASSERT(MediaManager::IsOn(mConstraints.mAudio));
      RefPtr<MediaStreamTrack> domTrack = new dom::AudioStreamTrack(
          window, track, audioTrackSource, dom::MediaStreamTrackState::Live,
          false, GetInvariant(mConstraints.mAudio));
      domStream->AddTrackInternal(domTrack);
    }
  }
  if (mVideoDevice) {
    const nsString& videoDeviceName = mVideoDevice->mName;
    RefPtr<MediaTrack> track = mtg->CreateSourceTrack(MediaSegment::VIDEO);
    videoTrackSource = new LocalTrackSource(
        principal, videoDeviceName, mVideoDeviceListener,
        mVideoDevice->GetMediaSource(), track, peerIdentity, *mVideoTrackingId);
    MOZ_ASSERT(MediaManager::IsOn(mConstraints.mVideo));
    RefPtr<MediaStreamTrack> domTrack = new dom::VideoStreamTrack(
        window, track, videoTrackSource, dom::MediaStreamTrackState::Live,
        false, GetInvariant(mConstraints.mVideo));
    domStream->AddTrackInternal(domTrack);
    switch (mVideoDevice->GetMediaSource()) {
      case MediaSourceEnum::Browser:
      case MediaSourceEnum::Screen:
      case MediaSourceEnum::Window:
        // Wait for first frame for screen-sharing devices, to ensure
        // with and height settings are available immediately, to pass wpt.
        firstFramePromise = mVideoDevice->Source()->GetFirstFramePromise();
        break;
      default:
        break;
    }
  }

  if (!domStream || (!audioTrackSource && !videoTrackSource) ||
      sHasMainThreadShutdown) {
    LOG("Returning error for getUserMedia() - no stream");

    mHolder.Reject(
        MakeRefPtr<MediaMgrError>(
            MediaMgrError::Name::AbortError,
            sHasMainThreadShutdown ? "In shutdown"_ns : "No stream."_ns),
        __func__);
    return;
  }

  // Activate our device listeners. We'll call Start() on the source when we
  // get a callback that the MediaStream has started consuming. The listener
  // is freed when the page is invalidated (on navigation or close).
  if (mAudioDeviceListener) {
    mWindowListener->Activate(mAudioDeviceListener, mAudioDevice,
                              std::move(audioTrackSource));
  }
  if (mVideoDeviceListener) {
    mWindowListener->Activate(mVideoDeviceListener, mVideoDevice,
                              std::move(videoTrackSource));
  }

  // Dispatch to the media thread to ask it to start the sources, because that
  // can take a while.
  typedef DeviceListener::DeviceListenerPromise PromiseType;
  AutoTArray<RefPtr<PromiseType>, 2> promises;
  if (mAudioDeviceListener) {
    promises.AppendElement(mAudioDeviceListener->InitializeAsync());
  }
  if (mVideoDeviceListener) {
    promises.AppendElement(mVideoDeviceListener->InitializeAsync());
  }
  PromiseType::All(GetMainThreadSerialEventTarget(), promises)
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [manager = mManager, windowListener = mWindowListener,
           firstFramePromise] {
            LOG("GetUserMediaStreamTask::PrepareDOMStream: starting success "
                "callback following InitializeAsync()");
            // Initiating and starting devices succeeded.
            windowListener->ChromeAffectingStateChanged();
            manager->SendPendingGUMRequest();
            if (!firstFramePromise) {
              return DeviceListener::DeviceListenerPromise::CreateAndResolve(
                  true, __func__);
            }
            RefPtr<DeviceListener::DeviceListenerPromise> resolvePromise =
                firstFramePromise->Then(
                    GetMainThreadSerialEventTarget(), __func__,
                    [] {
                      return DeviceListener::DeviceListenerPromise::
                          CreateAndResolve(true, __func__);
                    },
                    [] {
                      return DeviceListener::DeviceListenerPromise::
                          CreateAndReject(MakeRefPtr<MediaMgrError>(
                                              MediaMgrError::Name::AbortError,
                                              "In shutdown"),
                                          __func__);
                    });
            return resolvePromise;
          },
          [audio = mAudioDeviceListener,
           video = mVideoDeviceListener](RefPtr<MediaMgrError>&& aError) {
            LOG("GetUserMediaStreamTask::PrepareDOMStream: starting failure "
                "callback following InitializeAsync()");
            if (audio) {
              audio->Stop();
            }
            if (video) {
              video->Stop();
            }
            return DeviceListener::DeviceListenerPromise::CreateAndReject(
                aError, __func__);
          })
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [holder = std::move(mHolder), domStream, callerType = mCallerType,
           shouldFocus = mShouldFocusSource, videoDevice = mVideoDevice](
              const DeviceListener::DeviceListenerPromise::ResolveOrRejectValue&
                  aValue) mutable {
            if (aValue.IsResolve()) {
              if (auto* mgr = MediaManager::GetIfExists();
                  mgr && !sHasMainThreadShutdown && videoDevice &&
                  callerType == CallerType::NonSystem && shouldFocus) {
                // Device was successfully started. Attempt to focus the
                // source.
                MOZ_ALWAYS_SUCCEEDS(
                    mgr->mMediaThread->Dispatch(NS_NewRunnableFunction(
                        "GetUserMediaStreamTask::FocusOnSelectedSource",
                        [videoDevice = std::move(videoDevice)] {
                          nsresult rv = videoDevice->FocusOnSelectedSource();
                          if (NS_FAILED(rv)) {
                            LOG("FocusOnSelectedSource failed");
                          }
                        })));
              }

              holder.Resolve(domStream, __func__);
            } else {
              holder.Reject(aValue.RejectValue(), __func__);
            }
          });

  PersistPrincipalKey();
}

/**
 * Describes a requested task that handles response from the UI to a
 * selectAudioOutput() request and sends results back to content.  If the
 * request is allowed, then the MozPromise is resolved with a MediaDevice
 * for the approved device.
 */
class SelectAudioOutputTask final : public GetUserMediaTask {
 public:
  SelectAudioOutputTask(MozPromiseHolder<LocalDevicePromise>&& aHolder,
                        uint64_t aWindowID, enum CallerType aCallerType,
                        const ipc::PrincipalInfo& aPrincipalInfo)
      : GetUserMediaTask(aWindowID, aPrincipalInfo, aCallerType),
        mHolder(std::move(aHolder)) {}

  void Allowed(RefPtr<LocalMediaDevice> aAudioOutput) {
    MOZ_ASSERT(aAudioOutput);
    mHolder.Resolve(std::move(aAudioOutput), __func__);
    PersistPrincipalKey();
  }

  void Denied(MediaMgrError::Name aName, const nsCString& aMessage) override {
    MOZ_ASSERT(NS_IsMainThread());
    Fail(aName, aMessage);
  }

  SelectAudioOutputTask* AsSelectAudioOutputTask() override { return this; }

 private:
  ~SelectAudioOutputTask() override {
    if (!mHolder.IsEmpty()) {
      Fail(MediaMgrError::Name::NotAllowedError);
    }
  }

  void Fail(MediaMgrError::Name aName, const nsCString& aMessage = ""_ns) {
    mHolder.Reject(MakeRefPtr<MediaMgrError>(aName, aMessage), __func__);
  }

 private:
  MozPromiseHolder<LocalDevicePromise> mHolder;
};

/* static */
void MediaManager::GuessVideoDeviceGroupIDs(MediaDeviceSet& aDevices,
                                            const MediaDeviceSet& aAudios) {
  // Run the logic in a lambda to avoid duplication.
  auto updateGroupIdIfNeeded = [&](RefPtr<MediaDevice>& aVideo,
                                   const MediaDeviceKind aKind) -> bool {
    MOZ_ASSERT(aVideo->mKind == MediaDeviceKind::Videoinput);
    MOZ_ASSERT(aKind == MediaDeviceKind::Audioinput ||
               aKind == MediaDeviceKind::Audiooutput);
    // This will store the new group id if a match is found.
    nsString newVideoGroupID;
    // If the group id needs to be updated this will become true. It is
    // necessary when the new group id is an empty string. Without this extra
    // variable to signal the update, we would resort to test if
    // `newVideoGroupId` is empty. However,
    // that check does not work when the new group id is an empty string.
    bool updateGroupId = false;
    for (const RefPtr<MediaDevice>& dev : aAudios) {
      if (dev->mKind != aKind) {
        continue;
      }
      if (!FindInReadable(aVideo->mRawName, dev->mRawName)) {
        continue;
      }
      if (newVideoGroupID.IsEmpty()) {
        // This is only expected on first match. If that's the only match group
        // id will be updated to this one at the end of the loop.
        updateGroupId = true;
        newVideoGroupID = dev->mRawGroupID;
      } else {
        // More than one device found, it is impossible to know which group id
        // is the correct one.
        updateGroupId = false;
        newVideoGroupID = u""_ns;
        break;
      }
    }
    if (updateGroupId) {
      aVideo = MediaDevice::CopyWithNewRawGroupId(aVideo, newVideoGroupID);
      return true;
    }
    return false;
  };

  for (RefPtr<MediaDevice>& video : aDevices) {
    if (video->mKind != MediaDeviceKind::Videoinput) {
      continue;
    }
    if (updateGroupIdIfNeeded(video, MediaDeviceKind::Audioinput)) {
      // GroupId has been updated, continue to the next video device
      continue;
    }
    // GroupId has not been updated, check among the outputs
    updateGroupIdIfNeeded(video, MediaDeviceKind::Audiooutput);
  }
}

namespace {

// Class to hold the promise returned by EnumerateRawDevices() and to resolve
// even if |task| does not run, either because GeckoViewPermissionProcessChild
// gets destroyed before ask-device-permission receives its
// got-device-permission reply, or because the media thread is no longer
// available.  In either case, the process is shutting down so the result is
// not important.  Resolve with an empty set, so that callers do not need to
// handle rejection.
class DeviceSetPromiseHolderWithFallback
    : public MozPromiseHolder<DeviceSetPromise> {
 public:
  DeviceSetPromiseHolderWithFallback() = default;
  DeviceSetPromiseHolderWithFallback(DeviceSetPromiseHolderWithFallback&&) =
      default;
  ~DeviceSetPromiseHolderWithFallback() {
    if (!IsEmpty()) {
      Resolve(new MediaDeviceSetRefCnt(), __func__);
    }
  }
};

// Class to hold the promise used to request device access and to resolve
// even if |task| does not run, which can happen in case there is no
// observer for the ask-device-permission event.
class DeviceAccessRequestPromiseHolderWithFallback
    : public MozPromiseHolder<
          MozPromise<nsresult, mozilla::ipc::ResponseRejectReason, true>> {
 public:
  DeviceAccessRequestPromiseHolderWithFallback() = default;
  DeviceAccessRequestPromiseHolderWithFallback(
      DeviceAccessRequestPromiseHolderWithFallback&&) = default;
  ~DeviceAccessRequestPromiseHolderWithFallback() {
    if (!IsEmpty()) {
      Resolve(NS_OK, __func__);
    }
  }
};

}  // anonymous namespace

/**
 * EnumerateRawDevices - Enumerate a list of audio & video devices that
 * satisfy passed-in constraints. List contains raw id's.
 */

RefPtr<DeviceSetPromise> MediaManager::EnumerateRawDevices(
    MediaSourceEnum aVideoInputType, MediaSourceEnum aAudioInputType,
    EnumerationFlags aFlags) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aVideoInputType != MediaSourceEnum::Other ||
             aAudioInputType != MediaSourceEnum::Other ||
             aFlags.contains(EnumerationFlag::EnumerateAudioOutputs));

  LOG("%s: aVideoInputType=%" PRIu8 ", aAudioInputType=%" PRIu8, __func__,
      static_cast<uint8_t>(aVideoInputType),
      static_cast<uint8_t>(aAudioInputType));

  DeviceSetPromiseHolderWithFallback holder;
  RefPtr<DeviceSetPromise> promise = holder.Ensure(__func__);
  if (sHasMainThreadShutdown) {
    // The media thread is no longer available but the result will not be
    // observable.  |holder| will resolve |promise| on destruction.
    return promise;
  }

  const bool hasVideo = aVideoInputType != MediaSourceEnum::Other;
  const bool hasAudio = aAudioInputType != MediaSourceEnum::Other;
  const bool hasAudioOutput =
      aFlags.contains(EnumerationFlag::EnumerateAudioOutputs);
  const bool forceFakes = aFlags.contains(EnumerationFlag::ForceFakes);
  const bool fakeByPref = Preferences::GetBool("media.navigator.streams.fake");
  // Fake and loopback devices are supported for only Camera and Microphone.
  nsAutoCString videoLoopDev, audioLoopDev;
  bool hasFakeCams = false;
  bool hasFakeMics = false;
  if (aVideoInputType == MediaSourceEnum::Camera) {
    if (forceFakes) {
      hasFakeCams = true;
    } else {
      Preferences::GetCString("media.video_loopback_dev", videoLoopDev);
      // Loopback prefs take precedence over fake prefs
      hasFakeCams = fakeByPref && videoLoopDev.IsEmpty();
    }
  }
  if (aAudioInputType == MediaSourceEnum::Microphone) {
    if (forceFakes) {
      hasFakeMics = true;
    } else {
      Preferences::GetCString("media.audio_loopback_dev", audioLoopDev);
      // Loopback prefs take precedence over fake prefs
      hasFakeMics = fakeByPref && audioLoopDev.IsEmpty();
    }
  }
  // True if at least one of video input or audio input is a real device
  // or there is audio output.
  const bool realDeviceRequested = (!hasFakeCams && hasVideo) ||
                                   (!hasFakeMics && hasAudio) || hasAudioOutput;

  using NativePromise = MozPromise<nsresult, mozilla::ipc::ResponseRejectReason,
                                   /* IsExclusive = */ true>;
  RefPtr<NativePromise> deviceAccessPromise;
  if (realDeviceRequested &&
      aFlags.contains(EnumerationFlag::AllowPermissionRequest)) {
    if (Preferences::GetBool("media.navigator.permission.device", false)) {
      // Need to ask permission to retrieve list of all devices;
      // notify frontend observer and wait for callback notification to post
      // task.
      const char16_t* const type =
          (aVideoInputType != MediaSourceEnum::Camera)       ? u"audio"
          : (aAudioInputType != MediaSourceEnum::Microphone) ? u"video"
                                                             : u"all";
      nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
      DeviceAccessRequestPromiseHolderWithFallback deviceAccessPromiseHolder;
      deviceAccessPromise = deviceAccessPromiseHolder.Ensure(__func__);
      RefPtr task = NS_NewRunnableFunction(
          __func__, [holder = std::move(deviceAccessPromiseHolder)]() mutable {
            holder.Resolve(NS_OK, "getUserMedia:got-device-permission");
          });
      obs->NotifyObservers(static_cast<nsIRunnable*>(task),
                           "getUserMedia:ask-device-permission", type);
    } else if (hasVideo && aVideoInputType == MediaSourceEnum::Camera) {
      ipc::PBackgroundChild* backgroundChild =
          ipc::BackgroundChild::GetOrCreateForCurrentThread();
      deviceAccessPromise = backgroundChild->SendRequestCameraAccess();
    }
  }

  if (!deviceAccessPromise) {
    // No device access request needed. Proceed directly.
    deviceAccessPromise = NativePromise::CreateAndResolve(NS_OK, __func__);
  }

  deviceAccessPromise->Then(
      mMediaThread, __func__,
      [holder = std::move(holder), aVideoInputType, aAudioInputType,
       hasFakeCams, hasFakeMics, videoLoopDev, audioLoopDev, hasVideo, hasAudio,
       hasAudioOutput, realDeviceRequested](
          NativePromise::ResolveOrRejectValue&& aValue) mutable {
        if (aValue.IsReject()) {
          // IPC failure probably means we're in shutdown. Resolve with
          // an empty set, so that callers do not need to handle rejection.
          holder.Resolve(new MediaDeviceSetRefCnt(),
                         "EnumerateRawDevices: ipc failure");
          return;
        }

        if (nsresult value = aValue.ResolveValue(); NS_FAILED(value)) {
          holder.Reject(
              MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
              "EnumerateRawDevices: camera access rejected");
          return;
        }

        // Only enumerate what's asked for, and only fake cams and mics.
        RefPtr<MediaEngine> fakeBackend, realBackend;
        if (hasFakeCams || hasFakeMics) {
          fakeBackend = new MediaEngineFake();
        }
        if (realDeviceRequested) {
          MediaManager* manager = MediaManager::GetIfExists();
          MOZ_RELEASE_ASSERT(manager, "Must exist while media thread is alive");
          realBackend = manager->GetBackend();
        }

        RefPtr<MediaEngine> videoBackend;
        RefPtr<MediaEngine> audioBackend;
        Maybe<MediaDeviceSet> micsOfVideoBackend;
        Maybe<MediaDeviceSet> speakers;
        RefPtr devices = new MediaDeviceSetRefCnt();

        // Enumerate microphones first, then cameras, then speakers, since the
        // enumerateDevices() algorithm expects them listed in that order.
        if (hasAudio) {
          audioBackend = hasFakeMics ? fakeBackend : realBackend;
          MediaDeviceSet audios;
          LOG("EnumerateRawDevices Task: Getting audio sources with %s backend",
              audioBackend == fakeBackend ? "fake" : "real");
          GetMediaDevices(audioBackend, aAudioInputType, audios,
                          audioLoopDev.get());
          if (aAudioInputType == MediaSourceEnum::Microphone &&
              audioBackend == videoBackend) {
            micsOfVideoBackend = Some(MediaDeviceSet());
            micsOfVideoBackend->AppendElements(audios);
          }
          devices->AppendElements(audios);
        }
        if (hasVideo) {
          videoBackend = hasFakeCams ? fakeBackend : realBackend;
          MediaDeviceSet videos;
          LOG("EnumerateRawDevices Task: Getting video sources with %s backend",
              videoBackend == fakeBackend ? "fake" : "real");
          GetMediaDevices(videoBackend, aVideoInputType, videos,
                          videoLoopDev.get());
          devices->AppendElements(videos);
        }
        if (hasAudioOutput) {
          MediaDeviceSet outputs;
          MOZ_ASSERT(realBackend);
          realBackend->EnumerateDevices(MediaSourceEnum::Other,
                                        MediaSinkEnum::Speaker, &outputs);
          speakers = Some(MediaDeviceSet());
          speakers->AppendElements(outputs);
          devices->AppendElements(outputs);
        }
        if (hasVideo && aVideoInputType == MediaSourceEnum::Camera) {
          MediaDeviceSet audios;
          LOG("EnumerateRawDevices Task: Getting audio sources with %s backend "
              "for "
              "groupId correlation",
              videoBackend == fakeBackend ? "fake" : "real");
          // We need to correlate cameras with audio groupIds. We use the
          // backend of the camera to always do correlation on devices in the
          // same scope. If we don't do this, video-only getUserMedia will not
          // apply groupId constraints to the same set of groupIds as gets
          // returned by enumerateDevices.
          if (micsOfVideoBackend.isSome()) {
            // Microphones from the same backend used for the cameras have
            // already been enumerated. Avoid doing it again.
            audios.AppendElements(*micsOfVideoBackend);
          } else {
            GetMediaDevices(videoBackend, MediaSourceEnum::Microphone, audios,
                            audioLoopDev.get());
          }
          if (videoBackend == realBackend) {
            // When using the real backend for video, there could also be
            // speakers to correlate with. There are no fake speakers.
            if (speakers.isSome()) {
              // Speakers have already been enumerated. Avoid doing it again.
              audios.AppendElements(*speakers);
            } else {
              realBackend->EnumerateDevices(MediaSourceEnum::Other,
                                            MediaSinkEnum::Speaker, &audios);
            }
          }
          GuessVideoDeviceGroupIDs(*devices, audios);
        }

        holder.Resolve(std::move(devices), __func__);
      });

  return promise;
}

RefPtr<ConstDeviceSetPromise> MediaManager::GetPhysicalDevices() {
  MOZ_ASSERT(NS_IsMainThread());
  if (mPhysicalDevices) {
    return ConstDeviceSetPromise::CreateAndResolve(mPhysicalDevices, __func__);
  }
  if (mPendingDevicesPromises) {
    // Enumeration is already in progress.
    return mPendingDevicesPromises->AppendElement()->Ensure(__func__);
  }
  mPendingDevicesPromises =
      new Refcountable<nsTArray<MozPromiseHolder<ConstDeviceSetPromise>>>;
  EnumerateRawDevices(MediaSourceEnum::Camera, MediaSourceEnum::Microphone,
                      EnumerationFlag::EnumerateAudioOutputs)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr(this), this, promises = mPendingDevicesPromises](
              RefPtr<MediaDeviceSetRefCnt> aDevices) mutable {
            for (auto& promiseHolder : *promises) {
              promiseHolder.Resolve(aDevices, __func__);
            }
            // mPendingDevicesPromises may have changed if devices have changed.
            if (promises == mPendingDevicesPromises) {
              mPendingDevicesPromises = nullptr;
              mPhysicalDevices = std::move(aDevices);
            }
          },
          [](RefPtr<MediaMgrError>&& reason) {
            MOZ_ASSERT_UNREACHABLE("EnumerateRawDevices does not reject");
          });

  return mPendingDevicesPromises->AppendElement()->Ensure(__func__);
}

MediaManager::MediaManager(already_AddRefed<TaskQueue> aMediaThread)
    : mMediaThread(aMediaThread), mBackend(nullptr) {
  mPrefs.mFreq = 1000;  // 1KHz test tone
  mPrefs.mWidth = 0;    // adaptive default
  mPrefs.mHeight = 0;   // adaptive default
  mPrefs.mFPS = MediaEnginePrefs::DEFAULT_VIDEO_FPS;
  mPrefs.mAecOn = false;
  mPrefs.mUseAecMobile = false;
  mPrefs.mAgcOn = false;
  mPrefs.mHPFOn = false;
  mPrefs.mNoiseOn = false;
  mPrefs.mTransientOn = false;
  mPrefs.mResidualEchoOn = false;
  mPrefs.mAgc2Forced = false;
#ifdef MOZ_WEBRTC
  mPrefs.mAgc =
      webrtc::AudioProcessing::Config::GainController1::Mode::kAdaptiveDigital;
  mPrefs.mNoise =
      webrtc::AudioProcessing::Config::NoiseSuppression::Level::kModerate;
#else
  mPrefs.mAgc = 0;
  mPrefs.mNoise = 0;
#endif
  mPrefs.mChannels = 0;  // max channels default
  nsresult rv;
  nsCOMPtr<nsIPrefService> prefs =
      do_GetService("@mozilla.org/preferences-service;1", &rv);
  if (NS_SUCCEEDED(rv)) {
    nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs);
    if (branch) {
      GetPrefs(branch, nullptr);
    }
  }
  LOG("%s: default prefs: %dx%d @%dfps, %dHz test tones, aec: %s,"
      "agc: %s, hpf: %s, noise: %s, agc level: %d, agc version: %s, noise "
      "level: %d, transient: %s, residual echo: %s, channels %d",
      __FUNCTION__, mPrefs.mWidth, mPrefs.mHeight, mPrefs.mFPS, mPrefs.mFreq,
      mPrefs.mAecOn ? "on" : "off", mPrefs.mAgcOn ? "on" : "off",
      mPrefs.mHPFOn ? "on" : "off", mPrefs.mNoiseOn ? "on" : "off", mPrefs.mAgc,
      mPrefs.mAgc2Forced ? "2" : "1", mPrefs.mNoise,
      mPrefs.mTransientOn ? "on" : "off", mPrefs.mResidualEchoOn ? "on" : "off",
      mPrefs.mChannels);
}

NS_IMPL_ISUPPORTS(MediaManager, nsIMediaManagerService, nsIMemoryReporter,
                  nsIObserver)

/* static */
StaticRefPtr<MediaManager> MediaManager::sSingleton;

#ifdef DEBUG
/* static */
bool MediaManager::IsInMediaThread() {
  return sSingleton && sSingleton->mMediaThread->IsOnCurrentThread();
}
#endif

template <typename Function>
static void ForeachObservedPref(const Function& aFunction) {
  aFunction("media.navigator.video.default_width"_ns);
  aFunction("media.navigator.video.default_height"_ns);
  aFunction("media.navigator.video.default_fps"_ns);
  aFunction("media.navigator.audio.fake_frequency"_ns);
  aFunction("media.audio_loopback_dev"_ns);
  aFunction("media.video_loopback_dev"_ns);
  aFunction("media.getusermedia.fake-camera-name"_ns);
#ifdef MOZ_WEBRTC
  aFunction("media.getusermedia.aec_enabled"_ns);
  aFunction("media.getusermedia.aec"_ns);
  aFunction("media.getusermedia.agc_enabled"_ns);
  aFunction("media.getusermedia.agc"_ns);
  aFunction("media.getusermedia.hpf_enabled"_ns);
  aFunction("media.getusermedia.noise_enabled"_ns);
  aFunction("media.getusermedia.noise"_ns);
  aFunction("media.getusermedia.channels"_ns);
  aFunction("media.navigator.streams.fake"_ns);
#endif
}

// NOTE: never NS_DispatchAndSpinEventLoopUntilComplete to the MediaManager
// thread from the MainThread, as we NS_DispatchAndSpinEventLoopUntilComplete to
// MainThread from MediaManager thread.

// Guaranteed never to return nullptr.
/* static */
MediaManager* MediaManager::Get() {
  MOZ_ASSERT(NS_IsMainThread());

  if (!sSingleton) {
    static int timesCreated = 0;
    timesCreated++;
    MOZ_RELEASE_ASSERT(timesCreated == 1);

    RefPtr<TaskQueue> mediaThread = TaskQueue::Create(
        GetMediaThreadPool(MediaThreadType::SUPERVISOR), "MediaManager");
    LOG("New Media thread for gum");

    sSingleton = new MediaManager(mediaThread.forget());

    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
    if (obs) {
      obs->AddObserver(sSingleton, "last-pb-context-exited", false);
      obs->AddObserver(sSingleton, "getUserMedia:got-device-permission", false);
      obs->AddObserver(sSingleton, "getUserMedia:privileged:allow", false);
      obs->AddObserver(sSingleton, "getUserMedia:response:allow", false);
      obs->AddObserver(sSingleton, "getUserMedia:response:deny", false);
      obs->AddObserver(sSingleton, "getUserMedia:response:noOSPermission",
                       false);
      obs->AddObserver(sSingleton, "getUserMedia:revoke", false);
      obs->AddObserver(sSingleton, "getUserMedia:muteVideo", false);
      obs->AddObserver(sSingleton, "getUserMedia:unmuteVideo", false);
      obs->AddObserver(sSingleton, "getUserMedia:muteAudio", false);
      obs->AddObserver(sSingleton, "getUserMedia:unmuteAudio", false);
      obs->AddObserver(sSingleton, "application-background", false);
      obs->AddObserver(sSingleton, "application-foreground", false);
    }
    // else MediaManager won't work properly and will leak (see bug 837874)
    nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
    if (prefs) {
      ForeachObservedPref([&](const nsLiteralCString& aPrefName) {
        prefs->AddObserver(aPrefName, sSingleton, false);
      });
    }
    RegisterStrongMemoryReporter(sSingleton);

    // Prepare async shutdown

    class Blocker : public media::ShutdownBlocker {
     public:
      Blocker()
          : media::ShutdownBlocker(
                u"Media shutdown: blocking on media thread"_ns) {}

      NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
        MOZ_RELEASE_ASSERT(MediaManager::GetIfExists());
        MediaManager::GetIfExists()->Shutdown();
        return NS_OK;
      }
    };

    sSingleton->mShutdownBlocker = new Blocker();
    nsresult rv = media::MustGetShutdownBarrier()->AddBlocker(
        sSingleton->mShutdownBlocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
        __LINE__, u""_ns);
    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
  }
  return sSingleton;
}

/* static */
MediaManager* MediaManager::GetIfExists() {
  MOZ_ASSERT(NS_IsMainThread() || IsInMediaThread());
  return sSingleton;
}

/* static */
already_AddRefed<MediaManager> MediaManager::GetInstance() {
  // so we can have non-refcounted getters
  RefPtr<MediaManager> service = MediaManager::Get();
  return service.forget();
}

media::Parent<media::NonE10s>* MediaManager::GetNonE10sParent() {
  if (!mNonE10sParent) {
    mNonE10sParent = new media::Parent<media::NonE10s>();
  }
  return mNonE10sParent;
}

/* static */
void MediaManager::StartupInit() {
#ifdef WIN32
  if (!IsWin8OrLater()) {
    // Bug 1107702 - Older Windows fail in GetAdaptersInfo (and others) if the
    // first(?) call occurs after the process size is over 2GB (kb/2588507).
    // Attempt to 'prime' the pump by making a call at startup.
    unsigned long out_buf_len = sizeof(IP_ADAPTER_INFO);
    PIP_ADAPTER_INFO pAdapterInfo = (IP_ADAPTER_INFO*)moz_xmalloc(out_buf_len);
    if (GetAdaptersInfo(pAdapterInfo, &out_buf_len) == ERROR_BUFFER_OVERFLOW) {
      free(pAdapterInfo);
      pAdapterInfo = (IP_ADAPTER_INFO*)moz_xmalloc(out_buf_len);
      GetAdaptersInfo(pAdapterInfo, &out_buf_len);
    }
    if (pAdapterInfo) {
      free(pAdapterInfo);
    }
  }
#endif
}

/* static */
void MediaManager::Dispatch(already_AddRefed<Runnable> task) {
  MOZ_ASSERT(NS_IsMainThread());
  if (sHasMainThreadShutdown) {
    // Can't safely delete task here since it may have items with specific
    // thread-release requirements.
    // XXXkhuey well then who is supposed to delete it?! We don't signal
    // that we failed ...
    MOZ_CRASH();
    return;
  }
  NS_ASSERTION(Get(), "MediaManager singleton?");
  NS_ASSERTION(Get()->mMediaThread, "No thread yet");
  MOZ_ALWAYS_SUCCEEDS(Get()->mMediaThread->Dispatch(std::move(task)));
}

template <typename MozPromiseType, typename FunctionType>
/* static */
RefPtr<MozPromiseType> MediaManager::Dispatch(const char* aName,
                                              FunctionType&& aFunction) {
  MozPromiseHolder<MozPromiseType> holder;
  RefPtr<MozPromiseType> promise = holder.Ensure(aName);
  MediaManager::Dispatch(NS_NewRunnableFunction(
      aName, [h = std::move(holder), func = std::forward<FunctionType>(
                                         aFunction)]() mutable { func(h); }));
  return promise;
}

/* static */
nsresult MediaManager::NotifyRecordingStatusChange(
    nsPIDOMWindowInner* aWindow) {
  NS_ENSURE_ARG(aWindow);

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (!obs) {
    NS_WARNING(
        "Could not get the Observer service for GetUserMedia recording "
        "notification.");
    return NS_ERROR_FAILURE;
  }

  auto props = MakeRefPtr<nsHashPropertyBag>();

  nsCString pageURL;
  nsCOMPtr<nsIURI> docURI = aWindow->GetDocumentURI();
  NS_ENSURE_TRUE(docURI, NS_ERROR_FAILURE);

  nsresult rv = docURI->GetSpec(pageURL);
  NS_ENSURE_SUCCESS(rv, rv);

  NS_ConvertUTF8toUTF16 requestURL(pageURL);

  props->SetPropertyAsAString(u"requestURL"_ns, requestURL);
  props->SetPropertyAsInterface(u"window"_ns, aWindow);

  obs->NotifyObservers(static_cast<nsIPropertyBag2*>(props),
                       "recording-device-events", nullptr);
  LOG("Sent recording-device-events for url '%s'", pageURL.get());

  return NS_OK;
}

void MediaManager::DeviceListChanged() {
  MOZ_ASSERT(NS_IsMainThread());
  if (sHasMainThreadShutdown) {
    return;
  }
  // Invalidate immediately to provide an up-to-date device list for future
  // enumerations on platforms with sane device-list-changed events.
  InvalidateDeviceCache();

  // Wait 200 ms, because
  // A) on some Windows machines, if we call EnumerateRawDevices immediately
  //    after receiving devicechange event, we'd get an outdated devices list.
  // B) Waiting helps coalesce multiple calls on us into one, which can happen
  //    if a device with both audio input and output is attached or removed.
  //    We want to react & fire a devicechange event only once in that case.

  // The wait is extended if another hardware device-list-changed notification
  // is received to provide the full 200ms for EnumerateRawDevices().
  if (mDeviceChangeTimer) {
    mDeviceChangeTimer->Cancel();
  } else {
    mDeviceChangeTimer = MakeRefPtr<MediaTimer>();
  }
  // However, if this would cause a delay of over 1000ms in handling the
  // oldest unhandled event, then respond now and set the timer to run
  // EnumerateRawDevices() again in 200ms.
  auto now = TimeStamp::NowLoRes();
  auto enumerateDelay = TimeDuration::FromMilliseconds(200);
  auto coalescenceLimit = TimeDuration::FromMilliseconds(1000) - enumerateDelay;
  if (!mUnhandledDeviceChangeTime) {
    mUnhandledDeviceChangeTime = now;
  } else if (now - mUnhandledDeviceChangeTime > coalescenceLimit) {
    HandleDeviceListChanged();
    mUnhandledDeviceChangeTime = now;
  }
  RefPtr<MediaManager> self = this;
  mDeviceChangeTimer->WaitFor(enumerateDelay, __func__)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self, this] {
            // Invalidate again for the sake of platforms with inconsistent
            // timing between device-list-changed notification and enumeration.
            InvalidateDeviceCache();

            mUnhandledDeviceChangeTime = TimeStamp();
            HandleDeviceListChanged();
          },
          [] { /* Timer was canceled by us, or we're in shutdown. */ });
}

void MediaManager::InvalidateDeviceCache() {
  mPhysicalDevices = nullptr;
  // Disconnect any in-progress enumeration, which may now be out of date,
  // from updating mPhysicalDevices or resolving future device request
  // promises.
  mPendingDevicesPromises = nullptr;
}

void MediaManager::HandleDeviceListChanged() {
  mDeviceListChangeEvent.Notify();

  GetPhysicalDevices()->Then(
      GetCurrentSerialEventTarget(), __func__,
      [self = RefPtr(this), this](RefPtr<const MediaDeviceSetRefCnt> aDevices) {
        if (!MediaManager::GetIfExists()) {
          return;
        }

        nsTHashSet<nsString> deviceIDs;
        for (const auto& device : *aDevices) {
          deviceIDs.Insert(device->mRawID);
        }
        // For any real removed cameras or microphones, notify their
        // listeners cleanly that the source has stopped, so JS knows and
        // usage indicators update.
        // First collect the listeners in an array to stop them after
        // iterating the hashtable. The StopRawID() method indirectly
        // modifies the mActiveWindows and would assert-crash if the
        // iterator were active while the table is being enumerated.
        const auto windowListeners = ToArray(mActiveWindows.Values());
        for (const RefPtr<GetUserMediaWindowListener>& l : windowListeners) {
          const auto activeDevices = l->GetDevices();
          for (const RefPtr<LocalMediaDevice>& device : *activeDevices) {
            if (device->IsFake()) {
              continue;
            }
            MediaSourceEnum mediaSource = device->GetMediaSource();
            if (mediaSource != MediaSourceEnum::Microphone &&
                mediaSource != MediaSourceEnum::Camera) {
              continue;
            }
            if (!deviceIDs.Contains(device->RawID())) {
              // Device has been removed
              l->StopRawID(device->RawID());
            }
          }
        }
      },
      [](RefPtr<MediaMgrError>&& reason) {
        MOZ_ASSERT_UNREACHABLE("EnumerateRawDevices does not reject");
      });
}

size_t MediaManager::AddTaskAndGetCount(uint64_t aWindowID,
                                        const nsAString& aCallID,
                                        RefPtr<GetUserMediaTask> aTask) {
  // Store the task w/callbacks.
  mActiveCallbacks.InsertOrUpdate(aCallID, std::move(aTask));

  // Add a WindowID cross-reference so OnNavigation can tear things down
  nsTArray<nsString>* const array = mCallIds.GetOrInsertNew(aWindowID);
  array->AppendElement(aCallID);

  return array->Length();
}

RefPtr<GetUserMediaTask> MediaManager::TakeGetUserMediaTask(
    const nsAString& aCallID) {
  RefPtr<GetUserMediaTask> task;
  mActiveCallbacks.Remove(aCallID, getter_AddRefs(task));
  if (!task) {
    return nullptr;
  }
  nsTArray<nsString>* array;
  mCallIds.Get(task->GetWindowID(), &array);
  MOZ_ASSERT(array);
  array->RemoveElement(aCallID);
  return task;
}

void MediaManager::NotifyAllowed(const nsString& aCallID,
                                 const LocalMediaDeviceSet& aDevices) {
  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  nsCOMPtr<nsIMutableArray> devicesCopy = nsArray::Create();
  for (const auto& device : aDevices) {
    nsresult rv = devicesCopy->AppendElement(device);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      obs->NotifyObservers(nullptr, "getUserMedia:response:deny",
                           aCallID.get());
      return;
    }
  }
  obs->NotifyObservers(devicesCopy, "getUserMedia:privileged:allow",
                       aCallID.get());
}

nsresult MediaManager::GenerateUUID(nsAString& aResult) {
  nsresult rv;
  nsCOMPtr<nsIUUIDGenerator> uuidgen =
      do_GetService("@mozilla.org/uuid-generator;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  // Generate a call ID.
  nsID id;
  rv = uuidgen->GenerateUUIDInPlace(&id);
  NS_ENSURE_SUCCESS(rv, rv);

  char buffer[NSID_LENGTH];
  id.ToProvidedString(buffer);
  aResult.Assign(NS_ConvertUTF8toUTF16(buffer));
  return NS_OK;
}

enum class GetUserMediaSecurityState {
  Other = 0,
  HTTPS = 1,
  File = 2,
  App = 3,
  Localhost = 4,
  Loop = 5,
  Privileged = 6
};

/**
 * This function is used in getUserMedia when privacy.resistFingerprinting is
 * true. Only mediaSource of audio/video constraint will be kept.
 */
static void ReduceConstraint(
    OwningBooleanOrMediaTrackConstraints& aConstraint) {
  // Not requesting stream.
  if (!MediaManager::IsOn(aConstraint)) {
    return;
  }

  // It looks like {audio: true}, do nothing.
  if (!aConstraint.IsMediaTrackConstraints()) {
    return;
  }

  // Keep mediaSource, ignore all other constraints.
  Maybe<nsString> mediaSource;
  if (aConstraint.GetAsMediaTrackConstraints().mMediaSource.WasPassed()) {
    mediaSource =
        Some(aConstraint.GetAsMediaTrackConstraints().mMediaSource.Value());
  }
  aConstraint.Uninit();
  if (mediaSource) {
    aConstraint.SetAsMediaTrackConstraints().mMediaSource.Construct(
        *mediaSource);
  } else {
    Unused << aConstraint.SetAsMediaTrackConstraints();
  }
}

/**
 * The entry point for this file. A call from Navigator::mozGetUserMedia
 * will end up here. MediaManager is a singleton that is responsible
 * for handling all incoming getUserMedia calls from every window.
 */
RefPtr<MediaManager::StreamPromise> MediaManager::GetUserMedia(
    nsPIDOMWindowInner* aWindow,
    const MediaStreamConstraints& aConstraintsPassedIn,
    CallerType aCallerType) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aWindow);
  uint64_t windowID = aWindow->WindowID();

  MediaStreamConstraints c(aConstraintsPassedIn);  // use a modifiable copy

  if (sHasMainThreadShutdown) {
    return StreamPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError,
                                  "In shutdown"),
        __func__);
  }

  // Determine permissions early (while we still have a stack).

  nsIURI* docURI = aWindow->GetDocumentURI();
  if (!docURI) {
    return StreamPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError), __func__);
  }
  bool isChrome = (aCallerType == CallerType::System);
  bool privileged =
      isChrome ||
      Preferences::GetBool("media.navigator.permission.disabled", false);
  bool isSecure = aWindow->IsSecureContext();
  bool isHandlingUserInput = UserActivation::IsHandlingUserInput();
  nsCString host;
  nsresult rv = docURI->GetHost(host);

  nsCOMPtr<nsIPrincipal> principal =
      nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
  if (NS_WARN_IF(!principal)) {
    return StreamPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
        __func__);
  }

  Document* doc = aWindow->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    return StreamPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
        __func__);
  }

  // Disallow access to null principal pages and http pages (unless pref)
  if (principal->GetIsNullPrincipal() ||
      !(isSecure || StaticPrefs::media_getusermedia_insecure_enabled())) {
    return StreamPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
        __func__);
  }

  // This principal needs to be sent to different threads and so via IPC.
  // For this reason it's better to convert it to PrincipalInfo right now.
  ipc::PrincipalInfo principalInfo;
  rv = PrincipalToPrincipalInfo(principal, &principalInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return StreamPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
        __func__);
  }

  const bool resistFingerprinting =
      !isChrome && doc->ShouldResistFingerprinting(RFPTarget::MediaDevices);
  if (resistFingerprinting) {
    ReduceConstraint(c.mVideo);
    ReduceConstraint(c.mAudio);
  }

  if (!Preferences::GetBool("media.navigator.video.enabled", true)) {
    c.mVideo.SetAsBoolean() = false;
  }

  MediaSourceEnum videoType = MediaSourceEnum::Other;  // none
  MediaSourceEnum audioType = MediaSourceEnum::Other;  // none

  if (c.mVideo.IsMediaTrackConstraints()) {
    auto& vc = c.mVideo.GetAsMediaTrackConstraints();
    if (!vc.mMediaSource.WasPassed()) {
      vc.mMediaSource.Construct().AssignASCII(
          dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Camera));
    }
    videoType = StringToEnum(dom::MediaSourceEnumValues::strings,
                             vc.mMediaSource.Value(), MediaSourceEnum::Other);
    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
                          (uint32_t)videoType);
    switch (videoType) {
      case MediaSourceEnum::Camera:
        break;

      case MediaSourceEnum::Browser:
        // If no window id is passed in then default to the caller's window.
        // Functional defaults are helpful in tests, but also a natural outcome
        // of the constraints API's limited semantics for requiring input.
        if (!vc.mBrowserWindow.WasPassed()) {
          nsPIDOMWindowOuter* outer = aWindow->GetOuterWindow();
          vc.mBrowserWindow.Construct(outer->WindowID());
        }
        [[fallthrough]];
      case MediaSourceEnum::Screen:
      case MediaSourceEnum::Window:
        // Deny screensharing request if support is disabled, or
        // the requesting document is not from a host on the whitelist.
        if (!Preferences::GetBool(
                ((videoType == MediaSourceEnum::Browser)
                     ? "media.getusermedia.browser.enabled"
                     : "media.getusermedia.screensharing.enabled"),
                false) ||
            (!privileged && !aWindow->IsSecureContext())) {
          return StreamPromise::CreateAndReject(
              MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
              __func__);
        }
        break;

      case MediaSourceEnum::Microphone:
      case MediaSourceEnum::Other:
      default: {
        return StreamPromise::CreateAndReject(
            MakeRefPtr<MediaMgrError>(MediaMgrError::Name::OverconstrainedError,
                                      "", u"mediaSource"_ns),
            __func__);
      }
    }

    if (!privileged) {
      // Only allow privileged content to explicitly pick full-screen,
      // application or tabsharing, since these modes are still available for
      // testing. All others get "Window" (*) sharing.
      //
      // *) We overload "Window" with the new default getDisplayMedia spec-
      // mandated behavior of not influencing user-choice, which we currently
      // implement as a list containing BOTH windows AND screen(s).
      //
      // Notes on why we chose "Window" as the one to overload. Two reasons:
      //
      // 1. It's the closest logically & behaviorally (multi-choice, no default)
      // 2. Screen is still useful in tests (implicit default is entire screen)
      //
      // For UX reasons we don't want "Entire Screen" to be the first/default
      // choice (in our code first=default). It's a "scary" source that comes
      // with complicated warnings on-top that would be confusing as the first
      // thing people see, and also deserves to be listed as last resort for
      // privacy reasons.

      if (videoType == MediaSourceEnum::Screen ||
          videoType == MediaSourceEnum::Browser) {
        videoType = MediaSourceEnum::Window;
        vc.mMediaSource.Value().AssignASCII(
            dom::MediaSourceEnumValues::GetString(videoType));
      }
      // only allow privileged content to set the window id
      if (vc.mBrowserWindow.WasPassed()) {
        vc.mBrowserWindow.Value() = -1;
      }
      if (vc.mAdvanced.WasPassed()) {
        for (MediaTrackConstraintSet& cs : vc.mAdvanced.Value()) {
          if (cs.mBrowserWindow.WasPassed()) {
            cs.mBrowserWindow.Value() = -1;
          }
        }
      }
    }
  } else if (IsOn(c.mVideo)) {
    videoType = MediaSourceEnum::Camera;
    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
                          (uint32_t)videoType);
  }

  if (c.mAudio.IsMediaTrackConstraints()) {
    auto& ac = c.mAudio.GetAsMediaTrackConstraints();
    if (!ac.mMediaSource.WasPassed()) {
      ac.mMediaSource.Construct(NS_ConvertASCIItoUTF16(
          dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Microphone)));
    }
    audioType = StringToEnum(dom::MediaSourceEnumValues::strings,
                             ac.mMediaSource.Value(), MediaSourceEnum::Other);
    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
                          (uint32_t)audioType);

    switch (audioType) {
      case MediaSourceEnum::Microphone:
        break;

      case MediaSourceEnum::AudioCapture:
        // Only enable AudioCapture if the pref is enabled. If it's not, we can
        // deny right away.
        if (!Preferences::GetBool("media.getusermedia.audiocapture.enabled")) {
          return StreamPromise::CreateAndReject(
              MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
              __func__);
        }
        break;

      case MediaSourceEnum::Other:
      default: {
        return StreamPromise::CreateAndReject(
            MakeRefPtr<MediaMgrError>(MediaMgrError::Name::OverconstrainedError,
                                      "", u"mediaSource"_ns),
            __func__);
      }
    }
  } else if (IsOn(c.mAudio)) {
    audioType = MediaSourceEnum::Microphone;
    Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
                          (uint32_t)audioType);
  }

  // Create a window listener if it doesn't already exist.
  RefPtr<GetUserMediaWindowListener> windowListener =
      GetOrMakeWindowListener(aWindow);
  MOZ_ASSERT(windowListener);
  // Create an inactive DeviceListener to act as a placeholder, so the
  // window listener doesn't clean itself up until we're done.
  auto placeholderListener = MakeRefPtr<DeviceListener>();
  windowListener->Register(placeholderListener);

  {  // Check Permissions Policy.  Reject if a requested feature is disabled.
    bool disabled = !IsOn(c.mAudio) && !IsOn(c.mVideo);
    if (IsOn(c.mAudio)) {
      if (audioType == MediaSourceEnum::Microphone) {
        if (Preferences::GetBool("media.getusermedia.microphone.deny", false) ||
            !FeaturePolicyUtils::IsFeatureAllowed(doc, u"microphone"_ns)) {
          disabled = true;
        }
      } else if (!FeaturePolicyUtils::IsFeatureAllowed(doc,
                                                       u"display-capture"_ns)) {
        disabled = true;
      }
    }
    if (IsOn(c.mVideo)) {
      if (videoType == MediaSourceEnum::Camera) {
        if (Preferences::GetBool("media.getusermedia.camera.deny", false) ||
            !FeaturePolicyUtils::IsFeatureAllowed(doc, u"camera"_ns)) {
          disabled = true;
        }
      } else if (!FeaturePolicyUtils::IsFeatureAllowed(doc,
                                                       u"display-capture"_ns)) {
        disabled = true;
      }
    }

    if (disabled) {
      placeholderListener->Stop();
      return StreamPromise::CreateAndReject(
          MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
          __func__);
    }
  }

  // Get list of all devices, with origin-specific device ids.

  MediaEnginePrefs prefs = mPrefs;

  nsString callID;
  rv = GenerateUUID(callID);
  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));

  bool hasVideo = videoType != MediaSourceEnum::Other;
  bool hasAudio = audioType != MediaSourceEnum::Other;

  // Handle fake requests from content. For gUM we don't consider resist
  // fingerprinting as users should be prompted anyway.
  bool forceFakes = c.mFake.WasPassed() && c.mFake.Value();
  // fake:true is effective only for microphone and camera devices, so
  // permission must be requested for screen capture even if fake:true is set.
  bool hasOnlyForcedFakes =
      forceFakes && (!hasVideo || videoType == MediaSourceEnum::Camera) &&
      (!hasAudio || audioType == MediaSourceEnum::Microphone);
  bool askPermission =
      (!privileged ||
       Preferences::GetBool("media.navigator.permission.force")) &&
      (!hasOnlyForcedFakes ||
       Preferences::GetBool("media.navigator.permission.fake"));

  LOG("%s: Preparing to enumerate devices. windowId=%" PRIu64
      ", videoType=%" PRIu8 ", audioType=%" PRIu8
      ", forceFakes=%s, askPermission=%s",
      __func__, windowID, static_cast<uint8_t>(videoType),
      static_cast<uint8_t>(audioType), forceFakes ? "true" : "false",
      askPermission ? "true" : "false");

  EnumerationFlags flags = EnumerationFlag::AllowPermissionRequest;
  if (forceFakes) {
    flags += EnumerationFlag::ForceFakes;
  }
  RefPtr<MediaManager> self = this;
  return EnumerateDevicesImpl(aWindow, videoType, audioType, flags)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self, windowID, c, windowListener,
           aCallerType](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) {
            LOG("GetUserMedia: post enumeration promise success callback "
                "starting");
            // Ensure that our windowID is still good.
            RefPtr<nsPIDOMWindowInner> window =
                nsGlobalWindowInner::GetInnerWindowWithId(windowID);
            if (!window || !self->IsWindowListenerStillActive(windowListener)) {
              LOG("GetUserMedia: bad window (%" PRIu64
                  ") in post enumeration success callback!",
                  windowID);
              return LocalDeviceSetPromise::CreateAndReject(
                  MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
                  __func__);
            }
            // Apply any constraints. This modifies the passed-in list.
            return self->SelectSettings(c, aCallerType, std::move(aDevices));
          },
          [](RefPtr<MediaMgrError>&& aError) {
            LOG("GetUserMedia: post enumeration EnumerateDevicesImpl "
                "failure callback called!");
            return LocalDeviceSetPromise::CreateAndReject(std::move(aError),
                                                          __func__);
          })
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self, windowID, c, windowListener, placeholderListener, hasAudio,
           hasVideo, askPermission, prefs, isSecure, isHandlingUserInput,
           callID, principalInfo, aCallerType, resistFingerprinting](
              RefPtr<LocalMediaDeviceSetRefCnt> aDevices) mutable {
            LOG("GetUserMedia: starting post enumeration promise2 success "
                "callback!");

            // Ensure that the window is still good.
            RefPtr<nsPIDOMWindowInner> window =
                nsGlobalWindowInner::GetInnerWindowWithId(windowID);
            if (!window || !self->IsWindowListenerStillActive(windowListener)) {
              LOG("GetUserMedia: bad window (%" PRIu64
                  ") in post enumeration success callback 2!",
                  windowID);
              placeholderListener->Stop();
              return StreamPromise::CreateAndReject(
                  MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
                  __func__);
            }
            if (!aDevices->Length()) {
              LOG("GetUserMedia: no devices found in post enumeration promise2 "
                  "success callback! Calling error handler!");
              placeholderListener->Stop();
              // When privacy.resistFingerprinting = true, no
              // available device implies content script is requesting
              // a fake device, so report NotAllowedError.
              auto error = resistFingerprinting
                               ? MediaMgrError::Name::NotAllowedError
                               : MediaMgrError::Name::NotFoundError;
              return StreamPromise::CreateAndReject(
                  MakeRefPtr<MediaMgrError>(error), __func__);
            }

            // Time to start devices. Create the necessary device listeners and
            // remove the placeholder.
            RefPtr<DeviceListener> audioListener;
            RefPtr<DeviceListener> videoListener;
            if (hasAudio) {
              audioListener = MakeRefPtr<DeviceListener>();
              windowListener->Register(audioListener);
            }
            if (hasVideo) {
              videoListener = MakeRefPtr<DeviceListener>();
              windowListener->Register(videoListener);
            }
            placeholderListener->Stop();

            bool focusSource = mozilla::Preferences::GetBool(
                "media.getusermedia.window.focus_source.enabled", true);

            // Incremental hack to compile. To be replaced by deeper
            // refactoring. MediaManager allows
            // "neither-resolve-nor-reject" semantics, so we cannot
            // use MozPromiseHolder here.
            MozPromiseHolder<StreamPromise> holder;
            RefPtr<StreamPromise> p = holder.Ensure(__func__);

            // Pass callbacks and listeners along to GetUserMediaStreamTask.
            auto task = MakeRefPtr<GetUserMediaStreamTask>(
                c, std::move(holder), windowID, std::move(windowListener),
                std::move(audioListener), std::move(videoListener), prefs,
                principalInfo, aCallerType, focusSource);

            size_t taskCount =
                self->AddTaskAndGetCount(windowID, callID, std::move(task));

            if (!askPermission) {
              self->NotifyAllowed(callID, *aDevices);
            } else {
              auto req = MakeRefPtr<GetUserMediaRequest>(
                  window, callID, std::move(aDevices), c, isSecure,
                  isHandlingUserInput);
              if (!Preferences::GetBool("media.navigator.permission.force") &&
                  taskCount > 1) {
                // there is at least 1 pending gUM request
                // For the scarySources test case, always send the
                // request
                self->mPendingGUMRequest.AppendElement(req.forget());
              } else {
                nsCOMPtr<nsIObserverService> obs =
                    services::GetObserverService();
                obs->NotifyObservers(req, "getUserMedia:request", nullptr);
              }
            }
#ifdef MOZ_WEBRTC
            EnableWebRtcLog();
#endif
            return p;
          },
          [placeholderListener](RefPtr<MediaMgrError>&& aError) {
            LOG("GetUserMedia: post enumeration SelectSettings failure "
                "callback called!");
            placeholderListener->Stop();
            return StreamPromise::CreateAndReject(std::move(aError), __func__);
          });
};

RefPtr<LocalDeviceSetPromise> MediaManager::AnonymizeDevices(
    nsPIDOMWindowInner* aWindow, RefPtr<const MediaDeviceSetRefCnt> aDevices) {
  // Get an origin-key (for either regular or private browsing).
  MOZ_ASSERT(NS_IsMainThread());
  uint64_t windowId = aWindow->WindowID();
  nsCOMPtr<nsIPrincipal> principal =
      nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
  MOZ_ASSERT(principal);
  ipc::PrincipalInfo principalInfo;
  nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return LocalDeviceSetPromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
        __func__);
  }
  bool persist = IsActivelyCapturingOrHasAPermission(windowId);
  return media::GetPrincipalKey(principalInfo, persist)
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [rawDevices = std::move(aDevices),
           windowId](const nsCString& aOriginKey) {
            MOZ_ASSERT(!aOriginKey.IsEmpty());
            RefPtr anonymized = new LocalMediaDeviceSetRefCnt();
            for (const RefPtr<MediaDevice>& device : *rawDevices) {
              nsString id = device->mRawID;
              // An empty id represents a virtual default device, for which
              // the exposed deviceId is the empty string.
              if (!id.IsEmpty()) {
                nsContentUtils::AnonymizeId(id, aOriginKey);
              }
              nsString groupId = device->mRawGroupID;
              // Use window id to salt group id in order to make it session
              // based as required by the spec. This does not provide unique
              // group ids through out a browser restart. However, this is not
              // against the spec.  Furthermore, since device ids are the same
              // after a browser restart the fingerprint is not bigger.
              groupId.AppendInt(windowId);
              nsContentUtils::AnonymizeId(groupId, aOriginKey);

              nsString name = device->mRawName;
              if (name.Find(u"AirPods"_ns) != -1) {
                name = u"AirPods"_ns;
              }
              anonymized->EmplaceBack(
                  new LocalMediaDevice(device, id, groupId, name));
            }
            return LocalDeviceSetPromise::CreateAndResolve(anonymized,
                                                           __func__);
          },
          [](nsresult rs) {
            NS_WARNING("AnonymizeDevices failed to get Principal Key");
            return LocalDeviceSetPromise::CreateAndReject(
                MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
                __func__);
          });
}

RefPtr<LocalDeviceSetPromise> MediaManager::EnumerateDevicesImpl(
    nsPIDOMWindowInner* aWindow, MediaSourceEnum aVideoInputType,
    MediaSourceEnum aAudioInputType, EnumerationFlags aFlags) {
  MOZ_ASSERT(NS_IsMainThread());

  uint64_t windowId = aWindow->WindowID();
  LOG("%s: windowId=%" PRIu64 ", aVideoInputType=%" PRIu8
      ", aAudioInputType=%" PRIu8,
      __func__, windowId, static_cast<uint8_t>(aVideoInputType),
      static_cast<uint8_t>(aAudioInputType));

  // To get a device list anonymized for a particular origin, we must:
  // 1. Get the raw devices list
  // 2. Anonymize the raw list with an origin-key.

  // Add the window id here to check for that and abort silently if no longer
  // exists.
  RefPtr<GetUserMediaWindowListener> windowListener =
      GetOrMakeWindowListener(aWindow);
  MOZ_ASSERT(windowListener);
  // Create an inactive DeviceListener to act as a placeholder, so the
  // window listener doesn't clean itself up until we're done.
  auto placeholderListener = MakeRefPtr<DeviceListener>();
  windowListener->Register(placeholderListener);

  return EnumerateRawDevices(aVideoInputType, aAudioInputType, aFlags)
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [self = RefPtr(this), this, window = nsCOMPtr(aWindow),
           placeholderListener](RefPtr<MediaDeviceSetRefCnt> aDevices) mutable {
            // Only run if window is still on our active list.
            MediaManager* mgr = MediaManager::GetIfExists();
            if (!mgr || placeholderListener->Stopped()) {
              // The listener has already been removed if the window is no
              // longer active.
              return LocalDeviceSetPromise::CreateAndReject(
                  MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
                  __func__);
            }
            MOZ_ASSERT(mgr->IsWindowStillActive(window->WindowID()));
            placeholderListener->Stop();
            return AnonymizeDevices(window, aDevices);
          },
          [placeholderListener](RefPtr<MediaMgrError>&& aError) {
            // EnumerateDevicesImpl may fail if a new doc has been set, in which
            // case the OnNavigation() method should have removed all previous
            // active listeners, or if a platform device access request was not
            // granted.
            placeholderListener->Stop();
            return LocalDeviceSetPromise::CreateAndReject(std::move(aError),
                                                          __func__);
          });
}

RefPtr<LocalDevicePromise> MediaManager::SelectAudioOutput(
    nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions,
    CallerType aCallerType) {
  bool isHandlingUserInput = UserActivation::IsHandlingUserInput();
  nsCOMPtr<nsIPrincipal> principal =
      nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
  if (!FeaturePolicyUtils::IsFeatureAllowed(aWindow->GetExtantDoc(),
                                            u"speaker-selection"_ns)) {
    return LocalDevicePromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(
            MediaMgrError::Name::NotAllowedError,
            "Document's Permissions Policy does not allow selectAudioOutput()"),
        __func__);
  }
  if (NS_WARN_IF(!principal)) {
    return LocalDevicePromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
        __func__);
  }
  // Disallow access to null principal.
  if (principal->GetIsNullPrincipal()) {
    return LocalDevicePromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
        __func__);
  }
  ipc::PrincipalInfo principalInfo;
  nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return LocalDevicePromise::CreateAndReject(
        MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
        __func__);
  }
  uint64_t windowID = aWindow->WindowID();
  const bool resistFingerprinting =
      aWindow->AsGlobal()->ShouldResistFingerprinting(aCallerType,
                                                      RFPTarget::MediaDevices);
  return EnumerateDevicesImpl(aWindow, MediaSourceEnum::Other,
                              MediaSourceEnum::Other,
                              {EnumerationFlag::EnumerateAudioOutputs,
                               EnumerationFlag::AllowPermissionRequest})
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr<MediaManager>(this), windowID, aOptions, aCallerType,
           resistFingerprinting, isHandlingUserInput,
           principalInfo](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) mutable {
            // Ensure that the window is still good.
            RefPtr<nsPIDOMWindowInner> window =
                nsGlobalWindowInner::GetInnerWindowWithId(windowID);
            if (!window) {
              LOG("SelectAudioOutput: bad window (%" PRIu64
                  ") in post enumeration success callback!",
                  windowID);
              return LocalDevicePromise::CreateAndReject(
                  MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
                  __func__);
            }
            if (aDevices->IsEmpty()) {
              LOG("SelectAudioOutput: no devices found");
              auto error = resistFingerprinting
                               ? MediaMgrError::Name::NotAllowedError
                               : MediaMgrError::Name::NotFoundError;
              return LocalDevicePromise::CreateAndReject(
                  MakeRefPtr<MediaMgrError>(error), __func__);
            }
            MozPromiseHolder<LocalDevicePromise> holder;
            RefPtr<LocalDevicePromise> p = holder.Ensure(__func__);
            auto task = MakeRefPtr<SelectAudioOutputTask>(
                std::move(holder), windowID, aCallerType, principalInfo);
            nsString callID;
            nsresult rv = GenerateUUID(callID);
            MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
            size_t taskCount =
                self->AddTaskAndGetCount(windowID, callID, std::move(task));
            bool askPermission =
                !Preferences::GetBool("media.navigator.permission.disabled") ||
                Preferences::GetBool("media.navigator.permission.force");
            if (!askPermission) {
              self->NotifyAllowed(callID, *aDevices);
            } else {
              MOZ_ASSERT(window->IsSecureContext());
              auto req = MakeRefPtr<GetUserMediaRequest>(
                  window, callID, std::move(aDevices), aOptions, true,
                  isHandlingUserInput);
              if (taskCount > 1) {
                // there is at least 1 pending gUM request
                self->mPendingGUMRequest.AppendElement(req.forget());
              } else {
                nsCOMPtr<nsIObserverService> obs =
                    services::GetObserverService();
                obs->NotifyObservers(req, "getUserMedia:request", nullptr);
              }
            }
            return p;
          },
          [](RefPtr<MediaMgrError> aError) {
            LOG("SelectAudioOutput: EnumerateDevicesImpl "
                "failure callback called!");
            return LocalDevicePromise::CreateAndReject(std::move(aError),
                                                       __func__);
          });
}

MediaEngine* MediaManager::GetBackend() {
  MOZ_ASSERT(MediaManager::IsInMediaThread());
  // Plugin backends as appropriate. The default engine also currently
  // includes picture support for Android.
  // This IS called off main-thread.
  if (!mBackend) {
#if defined(MOZ_WEBRTC)
    mBackend = new MediaEngineWebRTC();
#else
    mBackend = new MediaEngineFake();
#endif
    mDeviceListChangeListener = mBackend->DeviceListChangeEvent().Connect(
        AbstractThread::MainThread(), this, &MediaManager::DeviceListChanged);
  }
  return mBackend;
}

void MediaManager::OnNavigation(uint64_t aWindowID) {
  MOZ_ASSERT(NS_IsMainThread());
  LOG("OnNavigation for %" PRIu64, aWindowID);

  // Stop the streams for this window. The runnables check this value before
  // making a call to content.

  nsTArray<nsString>* callIDs;
  if (mCallIds.Get(aWindowID, &callIDs)) {
    for (auto& callID : *callIDs) {
      mActiveCallbacks.Remove(callID);
      for (auto& request : mPendingGUMRequest.Clone()) {
        nsString id;
        request->GetCallID(id);
        if (id == callID) {
          mPendingGUMRequest.RemoveElement(request);
        }
      }
    }
    mCallIds.Remove(aWindowID);
  }

  if (RefPtr<GetUserMediaWindowListener> listener =
          GetWindowListener(aWindowID)) {
    listener->RemoveAll();
  }
  MOZ_ASSERT(!GetWindowListener(aWindowID));
}

void MediaManager::OnCameraMute(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread());
  LOG("OnCameraMute for all windows");
  mCamerasMuted = aMute;
  // This is safe since we're on main-thread, and the windowlist can only
  // be added to from the main-thread
  for (const auto& window : mActiveWindows.Values()) {
    window->MuteOrUnmuteCameras(aMute);
  }
}

void MediaManager::OnMicrophoneMute(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread());
  LOG("OnMicrophoneMute for all windows");
  mMicrophonesMuted = aMute;
  // This is safe since we're on main-thread, and the windowlist can only
  // be added to from the main-thread
  for (const auto& window : mActiveWindows.Values()) {
    window->MuteOrUnmuteMicrophones(aMute);
  }
}

RefPtr<GetUserMediaWindowListener> MediaManager::GetOrMakeWindowListener(
    nsPIDOMWindowInner* aWindow) {
  Document* doc = aWindow->GetExtantDoc();
  if (!doc) {
    // The window has been destroyed. Destroyed windows don't have listeners.
    return nullptr;
  }
  nsIPrincipal* principal = doc->NodePrincipal();
  uint64_t windowId = aWindow->WindowID();
  RefPtr<GetUserMediaWindowListener> windowListener =
      GetWindowListener(windowId);
  if (windowListener) {
    MOZ_ASSERT(PrincipalHandleMatches(windowListener->GetPrincipalHandle(),
                                      principal));
  } else {
    windowListener = new GetUserMediaWindowListener(
        windowId, MakePrincipalHandle(principal));
    AddWindowID(windowId, windowListener);
  }
  return windowListener;
}

void MediaManager::AddWindowID(uint64_t aWindowId,
                               RefPtr<GetUserMediaWindowListener> aListener) {
  MOZ_ASSERT(NS_IsMainThread());
  // Store the WindowID in a hash table and mark as active. The entry is removed
  // when this window is closed or navigated away from.
  // This is safe since we're on main-thread, and the windowlist can only
  // be invalidated from the main-thread (see OnNavigation)
  if (IsWindowStillActive(aWindowId)) {
    MOZ_ASSERT(false, "Window already added");
    return;
  }

  aListener->MuteOrUnmuteCameras(mCamerasMuted);
  aListener->MuteOrUnmuteMicrophones(mMicrophonesMuted);
  GetActiveWindows()->InsertOrUpdate(aWindowId, std::move(aListener));

  RefPtr<WindowGlobalChild> wgc =
      WindowGlobalChild::GetByInnerWindowId(aWindowId);
  if (wgc) {
    wgc->BlockBFCacheFor(BFCacheStatus::ACTIVE_GET_USER_MEDIA);
  }
}

void MediaManager::RemoveWindowID(uint64_t aWindowId) {
  RefPtr<WindowGlobalChild> wgc =
      WindowGlobalChild::GetByInnerWindowId(aWindowId);
  if (wgc) {
    wgc->UnblockBFCacheFor(BFCacheStatus::ACTIVE_GET_USER_MEDIA);
  }

  mActiveWindows.Remove(aWindowId);

  // get outer windowID
  auto* window = nsGlobalWindowInner::GetInnerWindowWithId(aWindowId);
  if (!window) {
    LOG("No inner window for %" PRIu64, aWindowId);
    return;
  }

  auto* outer = window->GetOuterWindow();
  if (!outer) {
    LOG("No outer window for inner %" PRIu64, aWindowId);
    return;
  }

  uint64_t outerID = outer->WindowID();

  // Notify the UI that this window no longer has gUM active
  char windowBuffer[32];
  SprintfLiteral(windowBuffer, "%" PRIu64, outerID);
  nsString data = NS_ConvertUTF8toUTF16(windowBuffer);

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  obs->NotifyWhenScriptSafe(nullptr, "recording-window-ended", data.get());
  LOG("Sent recording-window-ended for window %" PRIu64 " (outer %" PRIu64 ")",
      aWindowId, outerID);
}

bool MediaManager::IsWindowListenerStillActive(
    const RefPtr<GetUserMediaWindowListener>& aListener) {
  MOZ_DIAGNOSTIC_ASSERT(aListener);
  return aListener && aListener == GetWindowListener(aListener->WindowID());
}

void MediaManager::GetPref(nsIPrefBranch* aBranch, const char* aPref,
                           const char* aData, int32_t* aVal) {
  int32_t temp;
  if (aData == nullptr || strcmp(aPref, aData) == 0) {
    if (NS_SUCCEEDED(aBranch->GetIntPref(aPref, &temp))) {
      *aVal = temp;
    }
  }
}

void MediaManager::GetPrefBool(nsIPrefBranch* aBranch, const char* aPref,
                               const char* aData, bool* aVal) {
  bool temp;
  if (aData == nullptr || strcmp(aPref, aData) == 0) {
    if (NS_SUCCEEDED(aBranch->GetBoolPref(aPref, &temp))) {
      *aVal = temp;
    }
  }
}

void MediaManager::GetPrefs(nsIPrefBranch* aBranch, const char* aData) {
  GetPref(aBranch, "media.navigator.video.default_width", aData,
          &mPrefs.mWidth);
  GetPref(aBranch, "media.navigator.video.default_height", aData,
          &mPrefs.mHeight);
  GetPref(aBranch, "media.navigator.video.default_fps", aData, &mPrefs.mFPS);
  GetPref(aBranch, "media.navigator.audio.fake_frequency", aData,
          &mPrefs.mFreq);
#ifdef MOZ_WEBRTC
  GetPrefBool(aBranch, "media.getusermedia.aec_enabled", aData, &mPrefs.mAecOn);
  GetPrefBool(aBranch, "media.getusermedia.agc_enabled", aData, &mPrefs.mAgcOn);
  GetPrefBool(aBranch, "media.getusermedia.hpf_enabled", aData, &mPrefs.mHPFOn);
  GetPrefBool(aBranch, "media.getusermedia.noise_enabled", aData,
              &mPrefs.mNoiseOn);
  GetPrefBool(aBranch, "media.getusermedia.transient_enabled", aData,
              &mPrefs.mTransientOn);
  GetPrefBool(aBranch, "media.getusermedia.residual_echo_enabled", aData,
              &mPrefs.mResidualEchoOn);
  GetPrefBool(aBranch, "media.getusermedia.agc2_forced", aData,
              &mPrefs.mAgc2Forced);
  GetPref(aBranch, "media.getusermedia.agc", aData, &mPrefs.mAgc);
  GetPref(aBranch, "media.getusermedia.noise", aData, &mPrefs.mNoise);
  GetPref(aBranch, "media.getusermedia.channels", aData, &mPrefs.mChannels);
#endif
}

void MediaManager::Shutdown() {
  MOZ_ASSERT(NS_IsMainThread());
  if (sHasMainThreadShutdown) {
    return;
  }

  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();

  obs->RemoveObserver(this, "last-pb-context-exited");
  obs->RemoveObserver(this, "getUserMedia:privileged:allow");
  obs->RemoveObserver(this, "getUserMedia:response:allow");
  obs->RemoveObserver(this, "getUserMedia:response:deny");
  obs->RemoveObserver(this, "getUserMedia:response:noOSPermission");
  obs->RemoveObserver(this, "getUserMedia:revoke");
  obs->RemoveObserver(this, "getUserMedia:muteVideo");
  obs->RemoveObserver(this, "getUserMedia:unmuteVideo");
  obs->RemoveObserver(this, "getUserMedia:muteAudio");
  obs->RemoveObserver(this, "getUserMedia:unmuteAudio");
  obs->RemoveObserver(this, "application-background");
  obs->RemoveObserver(this, "application-foreground");

  nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
  if (prefs) {
    ForeachObservedPref([&](const nsLiteralCString& aPrefName) {
      prefs->RemoveObserver(aPrefName, this);
    });
  }

  if (mDeviceChangeTimer) {
    mDeviceChangeTimer->Cancel();
    // Drop ref to MediaTimer early to avoid blocking SharedThreadPool shutdown
    mDeviceChangeTimer = nullptr;
  }

  {
    // Close off any remaining active windows.

    // Live capture at this point is rare but can happen. Stopping it will make
    // the window listeners attempt to remove themselves from the active windows
    // table. We cannot touch the table at point so we grab a copy of the window
    // listeners first.
    const auto listeners = ToArray(GetActiveWindows()->Values());
    for (const auto& listener : listeners) {
      listener->RemoveAll();
    }
  }
  MOZ_ASSERT(GetActiveWindows()->Count() == 0);

  GetActiveWindows()->Clear();
  mActiveCallbacks.Clear();
  mCallIds.Clear();
  mPendingGUMRequest.Clear();
#ifdef MOZ_WEBRTC
  StopWebRtcLog();
#endif

  // From main thread's point of view, shutdown is now done.
  // All that remains is shutting down the media thread.
  sHasMainThreadShutdown = true;

  // Release the backend (and call Shutdown()) from within mMediaThread.
  // Don't use MediaManager::Dispatch() because we're
  // sHasMainThreadShutdown == true here!
  MOZ_ALWAYS_SUCCEEDS(mMediaThread->Dispatch(
      NS_NewRunnableFunction(__func__, [self = RefPtr(this), this]() {
        LOG("MediaManager Thread Shutdown");
        MOZ_ASSERT(IsInMediaThread());
        // Must shutdown backend on MediaManager thread, since that's
        // where we started it from!
        if (mBackend) {
          mBackend->Shutdown();  // idempotent
          mDeviceListChangeListener.DisconnectIfExists();
        }
        // last reference, will invoke Shutdown() again
        mBackend = nullptr;
      })));

  // note that this == sSingleton
  MOZ_ASSERT(this == sSingleton);

  // Explicitly shut down the TaskQueue so that it releases its
  // SharedThreadPool when all tasks have completed.  SharedThreadPool blocks
  // XPCOM shutdown from proceeding beyond "xpcom-shutdown-threads" until all
  // SharedThreadPools are released, but the nsComponentManager keeps a
  // reference to the MediaManager for the nsIMediaManagerService until much
  // later in shutdown.  This also provides additional assurance that no
  // further tasks will be queued.
  mMediaThread->BeginShutdown()->Then(
      GetMainThreadSerialEventTarget(), __func__, [] {
        LOG("MediaManager shutdown lambda running, releasing MediaManager "
            "singleton");
        // Remove async shutdown blocker
        media::MustGetShutdownBarrier()->RemoveBlocker(
            sSingleton->mShutdownBlocker);

        sSingleton = nullptr;
      });
}

void MediaManager::SendPendingGUMRequest() {
  if (mPendingGUMRequest.Length() > 0) {
    nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
    obs->NotifyObservers(mPendingGUMRequest[0], "getUserMedia:request",
                         nullptr);
    mPendingGUMRequest.RemoveElementAt(0);
  }
}

bool IsGUMResponseNoAccess(const char* aTopic,
                           MediaMgrError::Name& aErrorName) {
  if (!strcmp(aTopic, "getUserMedia:response:deny")) {
    aErrorName = MediaMgrError::Name::NotAllowedError;
    return true;
  }

  if (!strcmp(aTopic, "getUserMedia:response:noOSPermission")) {
    aErrorName = MediaMgrError::Name::NotFoundError;
    return true;
  }

  return false;
}

static MediaSourceEnum ParseScreenColonWindowID(const char16_t* aData,
                                                uint64_t* aWindowIDOut) {
  MOZ_ASSERT(aWindowIDOut);
  // may be windowid or screen:windowid
  const nsDependentString data(aData);
  if (Substring(data, 0, strlen("screen:")).EqualsLiteral("screen:")) {
    nsresult rv;
    *aWindowIDOut = Substring(data, strlen("screen:")).ToInteger64(&rv);
    MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
    return MediaSourceEnum::Screen;
  }
  nsresult rv;
  *aWindowIDOut = data.ToInteger64(&rv);
  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
  return MediaSourceEnum::Camera;
}

nsresult MediaManager::Observe(nsISupports* aSubject, const char* aTopic,
                               const char16_t* aData) {
  MOZ_ASSERT(NS_IsMainThread());

  MediaMgrError::Name gumNoAccessError = MediaMgrError::Name::NotAllowedError;

  if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
    nsCOMPtr<nsIPrefBranch> branch(do_QueryInterface(aSubject));
    if (branch) {
      GetPrefs(branch, NS_ConvertUTF16toUTF8(aData).get());
      LOG("%s: %dx%d @%dfps", __FUNCTION__, mPrefs.mWidth, mPrefs.mHeight,
          mPrefs.mFPS);
      DeviceListChanged();
    }
  } else if (!strcmp(aTopic, "last-pb-context-exited")) {
    // Clear memory of private-browsing-specific deviceIds. Fire and forget.
    media::SanitizeOriginKeys(0, true);
    return NS_OK;
  } else if (!strcmp(aTopic, "getUserMedia:got-device-permission")) {
    MOZ_ASSERT(aSubject);
    nsCOMPtr<nsIRunnable> task = do_QueryInterface(aSubject);
    MediaManager::Dispatch(NewTaskFrom([task] { task->Run(); }));
    return NS_OK;
  } else if (!strcmp(aTopic, "getUserMedia:privileged:allow") ||
             !strcmp(aTopic, "getUserMedia:response:allow")) {
    nsString key(aData);
    RefPtr<GetUserMediaTask> task = TakeGetUserMediaTask(key);
    if (!task) {
      return NS_OK;
    }

    if (sHasMainThreadShutdown) {
      task->Denied(MediaMgrError::Name::AbortError, "In shutdown"_ns);
      return NS_OK;
    }
    if (NS_WARN_IF(!aSubject)) {
      return NS_ERROR_FAILURE;  // ignored
    }
    // Permission has been granted.  aSubject contains the particular device
    // or devices selected and approved by the user, if any.
    nsCOMPtr<nsIArray> array(do_QueryInterface(aSubject));
    MOZ_ASSERT(array);
    uint32_t len = 0;
    array->GetLength(&len);
    RefPtr<LocalMediaDevice> audioInput;
    RefPtr<LocalMediaDevice> videoInput;
    RefPtr<LocalMediaDevice> audioOutput;
    for (uint32_t i = 0; i < len; i++) {
      nsCOMPtr<nsIMediaDevice> device;
      array->QueryElementAt(i, NS_GET_IID(nsIMediaDevice),
                            getter_AddRefs(device));
      MOZ_ASSERT(device);  // shouldn't be returning anything else...
      if (!device) {
        continue;
      }

      // Casting here is safe because a LocalMediaDevice is created
      // only in Gecko side, JS can only query for an instance.
      auto* dev = static_cast<LocalMediaDevice*>(device.get());
      switch (dev->Kind()) {
        case MediaDeviceKind::Videoinput:
          if (!videoInput) {
            videoInput = dev;
          }
          break;
        case MediaDeviceKind::Audioinput:
          if (!audioInput) {
            audioInput = dev;
          }
          break;
        case MediaDeviceKind::Audiooutput:
          if (!audioOutput) {
            audioOutput = dev;
          }
          break;
        default:
          MOZ_CRASH("Unexpected device kind");
      }
    }

    if (GetUserMediaStreamTask* streamTask = task->AsGetUserMediaStreamTask()) {
      bool needVideo = IsOn(streamTask->GetConstraints().mVideo);
      bool needAudio = IsOn(streamTask->GetConstraints().mAudio);
      MOZ_ASSERT(needVideo || needAudio);

      if ((needVideo && !videoInput) || (needAudio && !audioInput)) {
        task->Denied(MediaMgrError::Name::NotAllowedError);
        return NS_OK;
      }
      streamTask->Allowed(std::move(audioInput), std::move(videoInput));
      return NS_OK;
    }
    if (SelectAudioOutputTask* outputTask = task->AsSelectAudioOutputTask()) {
      if (!audioOutput) {
        task->Denied(MediaMgrError::Name::NotAllowedError);
        return NS_OK;
      }
      outputTask->Allowed(std::move(audioOutput));
      return NS_OK;
    }

    NS_WARNING("Unknown task type in getUserMedia");
    return NS_ERROR_FAILURE;

  } else if (IsGUMResponseNoAccess(aTopic, gumNoAccessError)) {
    nsString key(aData);
    RefPtr<GetUserMediaTask> task = TakeGetUserMediaTask(key);
    if (task) {
      task->Denied(gumNoAccessError);
      SendPendingGUMRequest();
    }
    return NS_OK;

  } else if (!strcmp(aTopic, "getUserMedia:revoke")) {
    uint64_t windowID;
    if (ParseScreenColonWindowID(aData, &windowID) == MediaSourceEnum::Screen) {
      LOG("Revoking ScreenCapture access for window %" PRIu64, windowID);
      StopScreensharing(windowID);
    } else {
      LOG("Revoking MediaCapture access for window %" PRIu64, windowID);
      OnNavigation(windowID);
    }
    return NS_OK;
  } else if (!strcmp(aTopic, "getUserMedia:muteVideo") ||
             !strcmp(aTopic, "getUserMedia:unmuteVideo")) {
    OnCameraMute(!strcmp(aTopic, "getUserMedia:muteVideo"));
    return NS_OK;
  } else if (!strcmp(aTopic, "getUserMedia:muteAudio") ||
             !strcmp(aTopic, "getUserMedia:unmuteAudio")) {
    OnMicrophoneMute(!strcmp(aTopic, "getUserMedia:muteAudio"));
    return NS_OK;
  } else if ((!strcmp(aTopic, "application-background") ||
              !strcmp(aTopic, "application-foreground")) &&
             StaticPrefs::media_getusermedia_camera_background_mute_enabled()) {
    // On mobile we turn off any cameras (but not mics) while in the background.
    // Keeping things simple for now by duplicating test-covered code above.
    //
    // NOTE: If a mobile device ever wants to implement "getUserMedia:muteVideo"
    // as well, it'd need to update this code to handle & test the combinations.
    OnCameraMute(!strcmp(aTopic, "application-background"));
  }

  return NS_OK;
}

NS_IMETHODIMP
MediaManager::CollectReports(nsIHandleReportCallback* aHandleReport,
                             nsISupports* aData, bool aAnonymize) {
  size_t amount = 0;
  amount += mActiveWindows.ShallowSizeOfExcludingThis(MallocSizeOf);
  for (const GetUserMediaWindowListener* listener : mActiveWindows.Values()) {
    amount += listener->SizeOfIncludingThis(MallocSizeOf);
  }
  amount += mActiveCallbacks.ShallowSizeOfExcludingThis(MallocSizeOf);
  for (const GetUserMediaTask* task : mActiveCallbacks.Values()) {
    // Assume nsString buffers for keys are accounted in mCallIds.
    amount += task->SizeOfIncludingThis(MallocSizeOf);
  }
  amount += mCallIds.ShallowSizeOfExcludingThis(MallocSizeOf);
  for (const auto& array : mCallIds.Values()) {
    amount += array->ShallowSizeOfExcludingThis(MallocSizeOf);
    for (const nsString& callID : *array) {
      amount += callID.SizeOfExcludingThisEvenIfShared(MallocSizeOf);
    }
  }
  amount += mPendingGUMRequest.ShallowSizeOfExcludingThis(MallocSizeOf);
  // GetUserMediaRequest pointees of mPendingGUMRequest do not have support
  // for memory accounting.  mPendingGUMRequest logic should probably be moved
  // to the front end (bug 1691625).
  MOZ_COLLECT_REPORT("explicit/media/media-manager-aggregates", KIND_HEAP,
                     UNITS_BYTES, amount,
                     "Memory used by MediaManager variable length members.");
  return NS_OK;
}

nsresult MediaManager::GetActiveMediaCaptureWindows(nsIArray** aArray) {
  MOZ_ASSERT(aArray);

  nsCOMPtr<nsIMutableArray> array = nsArray::Create();

  for (const auto& entry : mActiveWindows) {
    const uint64_t& id = entry.GetKey();
    RefPtr<GetUserMediaWindowListener> winListener = entry.GetData();
    if (!winListener) {
      continue;
    }

    auto* window = nsGlobalWindowInner::GetInnerWindowWithId(id);
    MOZ_ASSERT(window);
    // XXXkhuey ...
    if (!window) {
      continue;
    }

    if (winListener->CapturingVideo() || winListener->CapturingAudio()) {
      array->AppendElement(ToSupports(window));
    }
  }

  array.forget(aArray);
  return NS_OK;
}

struct CaptureWindowStateData {
  uint16_t* mCamera;
  uint16_t* mMicrophone;
  uint16_t* mScreenShare;
  uint16_t* mWindowShare;
  uint16_t* mAppShare;
  uint16_t* mBrowserShare;
};

NS_IMETHODIMP
MediaManager::MediaCaptureWindowState(
    nsIDOMWindow* aCapturedWindow, uint16_t* aCamera, uint16_t* aMicrophone,
    uint16_t* aScreen, uint16_t* aWindow, uint16_t* aBrowser,
    nsTArray<RefPtr<nsIMediaDevice>>& aDevices) {
  MOZ_ASSERT(NS_IsMainThread());

  CaptureState camera = CaptureState::Off;
  CaptureState microphone = CaptureState::Off;
  CaptureState screen = CaptureState::Off;
  CaptureState window = CaptureState::Off;
  CaptureState browser = CaptureState::Off;
  RefPtr<LocalMediaDeviceSetRefCnt> devices;

  nsCOMPtr<nsPIDOMWindowInner> piWin = do_QueryInterface(aCapturedWindow);
  if (piWin) {
    if (RefPtr<GetUserMediaWindowListener> listener =
            GetWindowListener(piWin->WindowID())) {
      camera = listener->CapturingSource(MediaSourceEnum::Camera);
      microphone = listener->CapturingSource(MediaSourceEnum::Microphone);
      screen = listener->CapturingSource(MediaSourceEnum::Screen);
      window = listener->CapturingSource(MediaSourceEnum::Window);
      browser = listener->CapturingSource(MediaSourceEnum::Browser);
      devices = listener->GetDevices();
    }
  }

  *aCamera = FromCaptureState(camera);
  *aMicrophone = FromCaptureState(microphone);
  *aScreen = FromCaptureState(screen);
  *aWindow = FromCaptureState(window);
  *aBrowser = FromCaptureState(browser);
  if (devices) {
    for (auto& device : *devices) {
      aDevices.AppendElement(device);
    }
  }

  LOG("%s: window %" PRIu64 " capturing %s %s %s %s %s", __FUNCTION__,
      piWin ? piWin->WindowID() : -1,
      *aCamera == nsIMediaManagerService::STATE_CAPTURE_ENABLED
          ? "camera (enabled)"
          : (*aCamera == nsIMediaManagerService::STATE_CAPTURE_DISABLED
                 ? "camera (disabled)"
                 : ""),
      *aMicrophone == nsIMediaManagerService::STATE_CAPTURE_ENABLED
          ? "microphone (enabled)"
          : (*aMicrophone == nsIMediaManagerService::STATE_CAPTURE_DISABLED
                 ? "microphone (disabled)"
                 : ""),
      *aScreen ? "screenshare" : "", *aWindow ? "windowshare" : "",
      *aBrowser ? "browsershare" : "");

  return NS_OK;
}

NS_IMETHODIMP
MediaManager::SanitizeDeviceIds(int64_t aSinceWhen) {
  MOZ_ASSERT(NS_IsMainThread());
  LOG("%s: sinceWhen = %" PRId64, __FUNCTION__, aSinceWhen);

  media::SanitizeOriginKeys(aSinceWhen, false);  // we fire and forget
  return NS_OK;
}

void MediaManager::StopScreensharing(uint64_t aWindowID) {
  // We need to stop window/screensharing for all streams in this innerwindow.

  if (RefPtr<GetUserMediaWindowListener> listener =
          GetWindowListener(aWindowID)) {
    listener->StopSharing();
  }
}

bool MediaManager::IsActivelyCapturingOrHasAPermission(uint64_t aWindowId) {
  // Does page currently have a gUM stream active?

  nsCOMPtr<nsIArray> array;
  GetActiveMediaCaptureWindows(getter_AddRefs(array));
  uint32_t len;
  array->GetLength(&len);
  for (uint32_t i = 0; i < len; i++) {
    nsCOMPtr<nsPIDOMWindowInner> win;
    array->QueryElementAt(i, NS_GET_IID(nsPIDOMWindowInner),
                          getter_AddRefs(win));
    if (win && win->WindowID() == aWindowId) {
      return true;
    }
  }

  // Or are persistent permissions (audio or video) granted?

  auto* window = nsGlobalWindowInner::GetInnerWindowWithId(aWindowId);
  if (NS_WARN_IF(!window) || NS_WARN_IF(!window->GetPrincipal())) {
    return false;
  }

  Document* doc = window->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    return false;
  }

  nsIPrincipal* principal = window->GetPrincipal();
  if (NS_WARN_IF(!principal)) {
    return false;
  }

  // Check if this site has persistent permissions.
  nsresult rv;
  RefPtr<PermissionDelegateHandler> permDelegate =
      doc->GetPermissionDelegateHandler();
  if (NS_WARN_IF(!permDelegate)) {
    return false;
  }

  uint32_t audio = nsIPermissionManager::UNKNOWN_ACTION;
  uint32_t video = nsIPermissionManager::UNKNOWN_ACTION;
  {
    rv = permDelegate->GetPermission("microphone"_ns, &audio, true);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return false;
    }
    rv = permDelegate->GetPermission("camera"_ns, &video, true);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return false;
    }
  }
  return audio == nsIPermissionManager::ALLOW_ACTION ||
         video == nsIPermissionManager::ALLOW_ACTION;
}

DeviceListener::DeviceListener()
    : mStopped(false),
      mMainThreadCheck(nullptr),
      mPrincipalHandle(PRINCIPAL_HANDLE_NONE),
      mWindowListener(nullptr) {}

void DeviceListener::Register(GetUserMediaWindowListener* aListener) {
  LOG("DeviceListener %p registering with window listener %p", this, aListener);

  MOZ_ASSERT(aListener, "No listener");
  MOZ_ASSERT(!mWindowListener, "Already registered");
  MOZ_ASSERT(!Activated(), "Already activated");

  mPrincipalHandle = aListener->GetPrincipalHandle();
  mWindowListener = aListener;
}

void DeviceListener::Activate(RefPtr<LocalMediaDevice> aDevice,
                              RefPtr<LocalTrackSource> aTrackSource,
                              bool aStartMuted) {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");

  LOG("DeviceListener %p activating %s device %p", this,
      nsCString(dom::MediaDeviceKindValues::GetString(aDevice->Kind())).get(),
      aDevice.get());

  MOZ_ASSERT(!mStopped, "Cannot activate stopped device listener");
  MOZ_ASSERT(!Activated(), "Already activated");

  mMainThreadCheck = PR_GetCurrentThread();
  bool offWhileDisabled =
      (aDevice->GetMediaSource() == MediaSourceEnum::Microphone &&
       Preferences::GetBool(
           "media.getusermedia.microphone.off_while_disabled.enabled", true)) ||
      (aDevice->GetMediaSource() == MediaSourceEnum::Camera &&
       Preferences::GetBool(
           "media.getusermedia.camera.off_while_disabled.enabled", true));
  mDeviceState = MakeUnique<DeviceState>(
      std::move(aDevice), std::move(aTrackSource), offWhileDisabled);
  mDeviceState->mDeviceMuted = aStartMuted;
  if (aStartMuted) {
    mDeviceState->mTrackSource->Mute();
  }
}

RefPtr<DeviceListener::DeviceListenerPromise>
DeviceListener::InitializeAsync() {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
  MOZ_DIAGNOSTIC_ASSERT(!mStopped);

  return MediaManager::Dispatch<DeviceListenerPromise>(
             __func__,
             [principal = GetPrincipalHandle(), device = mDeviceState->mDevice,
              track = mDeviceState->mTrackSource->mTrack,
              deviceMuted = mDeviceState->mDeviceMuted](
                 MozPromiseHolder<DeviceListenerPromise>& aHolder) {
               auto kind = device->Kind();
               device->SetTrack(track, principal);
               nsresult rv = deviceMuted ? NS_OK : device->Start();
               if (kind == MediaDeviceKind::Audioinput ||
                   kind == MediaDeviceKind::Videoinput) {
                 if ((rv == NS_ERROR_NOT_AVAILABLE &&
                      kind == MediaDeviceKind::Audioinput) ||
                     (NS_FAILED(rv) && kind == MediaDeviceKind::Videoinput)) {
                   PR_Sleep(200);
                   rv = device->Start();
                 }
                 if (rv == NS_ERROR_NOT_AVAILABLE &&
                     kind == MediaDeviceKind::Audioinput) {
                   nsCString log;
                   log.AssignLiteral("Concurrent mic process limit.");
                   aHolder.Reject(MakeRefPtr<MediaMgrError>(
                                      MediaMgrError::Name::NotReadableError,
                                      std::move(log)),
                                  __func__);
                   return;
                 }
               }
               if (NS_FAILED(rv)) {
                 nsCString log;
                 log.AppendPrintf(
                     "Starting %s failed",
                     nsCString(dom::MediaDeviceKindValues::GetString(kind))
                         .get());
                 aHolder.Reject(
                     MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError,
                                               std::move(log)),
                     __func__);
                 return;
               }
               LOG("started %s device %p",
                   nsCString(dom::MediaDeviceKindValues::GetString(kind)).get(),
                   device.get());
               aHolder.Resolve(true, __func__);
             })
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [self = RefPtr<DeviceListener>(this), this]() {
            if (mStopped) {
              // We were shut down during the async init
              return DeviceListenerPromise::CreateAndResolve(true, __func__);
            }

            MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mTrackEnabled);
            MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mDeviceEnabled);
            MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mStopped);

            mDeviceState->mDeviceEnabled = true;
            mDeviceState->mTrackEnabled = true;
            mDeviceState->mTrackEnabledTime = TimeStamp::Now();
            return DeviceListenerPromise::CreateAndResolve(true, __func__);
          },
          [self = RefPtr<DeviceListener>(this),
           this](RefPtr<MediaMgrError>&& aResult) {
            if (mStopped) {
              return DeviceListenerPromise::CreateAndReject(std::move(aResult),
                                                            __func__);
            }

            MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mTrackEnabled);
            MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mDeviceEnabled);
            MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mStopped);

            Stop();
            return DeviceListenerPromise::CreateAndReject(std::move(aResult),
                                                          __func__);
          });
}

void DeviceListener::Stop() {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");

  if (mStopped) {
    return;
  }
  mStopped = true;

  LOG("DeviceListener %p stopping", this);

  if (mDeviceState) {
    mDeviceState->mDisableTimer->Cancel();

    if (mDeviceState->mStopped) {
      // device already stopped.
      return;
    }
    mDeviceState->mStopped = true;

    mDeviceState->mTrackSource->Stop();

    MediaManager::Dispatch(NewTaskFrom([device = mDeviceState->mDevice]() {
      device->Stop();
      device->Deallocate();
    }));

    mWindowListener->ChromeAffectingStateChanged();
  }

  // Keep a strong ref to the removed window listener.
  RefPtr<GetUserMediaWindowListener> windowListener = mWindowListener;
  mWindowListener = nullptr;
  windowListener->Remove(this);
}

void DeviceListener::GetSettings(MediaTrackSettings& aOutSettings) const {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
  LocalMediaDevice* device = GetDevice();
  device->GetSettings(aOutSettings);

  MediaSourceEnum mediaSource = device->GetMediaSource();
  if (mediaSource == MediaSourceEnum::Camera ||
      mediaSource == MediaSourceEnum::Microphone) {
    aOutSettings.mDeviceId.Construct(device->mID);
    aOutSettings.mGroupId.Construct(device->mGroupID);
  }
}

auto DeviceListener::UpdateDevice(bool aOn) -> RefPtr<DeviceOperationPromise> {
  MOZ_ASSERT(NS_IsMainThread());
  RefPtr<DeviceListener> self = this;
  DeviceState& state = *mDeviceState;
  return MediaManager::Dispatch<DeviceOperationPromise>(
             __func__,
             [self, device = state.mDevice,
              aOn](MozPromiseHolder<DeviceOperationPromise>& h) {
               LOG("Turning %s device (%s)", aOn ? "on" : "off",
                   NS_ConvertUTF16toUTF8(device->mName).get());
               h.Resolve(aOn ? device->Start() : device->Stop(), __func__);
             })
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [self, this, &state, aOn](nsresult aResult) {
            if (state.mStopped) {
              // Device was stopped on main thread during the operation. Done.
              return DeviceOperationPromise::CreateAndResolve(aResult,
                                                              __func__);
            }
            LOG("DeviceListener %p turning %s %s input device %s", this,
                aOn ? "on" : "off",
                nsCString(
                    dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
                    .get(),
                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.
              if (aOn) {
                // Starting the device failed. Stopping the track here will
                // make the MediaStreamTrack end after a pass through the
                // MediaTrackGraph.
                Stop();
              } else {
                // Stopping the device failed. This is odd, but not fatal.
                MOZ_ASSERT_UNREACHABLE("The device should be stoppable");
              }
            }
            return DeviceOperationPromise::CreateAndResolve(aResult, __func__);
          },
          []() {
            MOZ_ASSERT_UNREACHABLE("Unexpected and unhandled reject");
            return DeviceOperationPromise::CreateAndReject(false, __func__);
          });
}

void DeviceListener::SetDeviceEnabled(bool aEnable) {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
  MOZ_ASSERT(Activated(), "No device to set enabled state for");

  DeviceState& state = *mDeviceState;

  LOG("DeviceListener %p %s %s device", this,
      aEnable ? "enabling" : "disabling",
      nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
          .get());

  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;
  }

  // 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__);
    state.mTrackEnabledTime = TimeStamp::Now();
  } else {
    const TimeDuration maxDelay =
        TimeDuration::FromMilliseconds(Preferences::GetUint(
            GetDevice()->Kind() == MediaDeviceKind::Audioinput
                ? "media.getusermedia.microphone.off_while_disabled.delay_ms"
                : "media.getusermedia.camera.off_while_disabled.delay_ms",
            3000));
    const TimeDuration durationEnabled =
        TimeStamp::Now() - state.mTrackEnabledTime;
    const TimeDuration delay = TimeDuration::Max(
        TimeDuration::FromMilliseconds(0), maxDelay - durationEnabled);
    timerPromise = state.mDisableTimer->WaitFor(delay, __func__);
  }

  RefPtr<DeviceListener> self = this;
  timerPromise
      ->Then(
          GetMainThreadSerialEventTarget(), __func__,
          [self, this, &state, aEnable]() 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("DeviceListener %p %s %s device - starting device operation",
                this, aEnable ? "enabling" : "disabling",
                nsCString(
                    dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
                    .get());

            if (state.mStopped) {
              // Source was stopped between timer resolving and this runnable.
              return DeviceOperationPromise::CreateAndResolve(NS_ERROR_ABORT,
                                                              __func__);
            }

            state.mDeviceEnabled = aEnable;

            if (mWindowListener) {
              mWindowListener->ChromeAffectingStateChanged();
            }
            if (!state.mOffWhileDisabled || state.mDeviceMuted) {
              // If the feature to turn a device off while disabled is itself
              // disabled, or the device is currently user agent muted, then
              // we shortcut the device operation and tell the
              // ux-updating code that everything went fine.
              return DeviceOperationPromise::CreateAndResolve(NS_OK, __func__);
            }
            return UpdateDevice(aEnable);
          },
          []() {
            // 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, aEnable](nsresult aResult) mutable {
            MOZ_ASSERT_IF(aResult != NS_ERROR_ABORT,
                          state.mDeviceEnabled == aEnable);
            MOZ_ASSERT(state.mOperationInProgress);
            state.mOperationInProgress = false;

            if (state.mStopped) {
              // Device was stopped on main thread during the operation.
              // Nothing to do.
              return;
            }

            if (NS_FAILED(aResult) && aResult != NS_ERROR_ABORT && !aEnable) {
              // To keep our internal state sane in this case, we disallow
              // future stops due to disable.
              state.mOffWhileDisabled = false;
              return;
            }

            // 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.

            if (state.mTrackEnabled != state.mDeviceEnabled) {
              // Track state changed during this operation. We'll start over.
              SetDeviceEnabled(state.mTrackEnabled);
            }
          },
          []() { MOZ_ASSERT_UNREACHABLE("Unexpected and unhandled reject"); });
}

void DeviceListener::SetDeviceMuted(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
  MOZ_ASSERT(Activated(), "No device to set muted state for");

  DeviceState& state = *mDeviceState;

  LOG("DeviceListener %p %s %s device", this, aMute ? "muting" : "unmuting",
      nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
          .get());

  if (state.mStopped) {
    // Device terminally stopped. Updating device state is pointless.
    return;
  }

  if (state.mDeviceMuted == aMute) {
    // Device is already in the desired state.
    return;
  }

  LOG("DeviceListener %p %s %s device - starting device operation", this,
      aMute ? "muting" : "unmuting",
      nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
          .get());

  state.mDeviceMuted = aMute;

  if (mWindowListener) {
    mWindowListener->ChromeAffectingStateChanged();
  }
  // Update trackSource to fire mute/unmute events on all its tracks
  if (aMute) {
    state.mTrackSource->Mute();
  } else {
    state.mTrackSource->Unmute();
  }
  if (!state.mOffWhileDisabled || !state.mDeviceEnabled) {
    // If the pref to turn the underlying device is itself off, or the device
    // is already off, it's unecessary to do anything else.
    return;
  }
  UpdateDevice(!aMute);
}

void DeviceListener::MuteOrUnmuteCamera(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mStopped) {
    return;
  }

  MOZ_RELEASE_ASSERT(mWindowListener);
  LOG("DeviceListener %p MuteOrUnmuteCamera: %s", this,
      aMute ? "mute" : "unmute");

  if (GetDevice() &&
      (GetDevice()->GetMediaSource() == MediaSourceEnum::Camera)) {
    SetDeviceMuted(aMute);
  }
}

void DeviceListener::MuteOrUnmuteMicrophone(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mStopped) {
    return;
  }

  MOZ_RELEASE_ASSERT(mWindowListener);
  LOG("DeviceListener %p MuteOrUnmuteMicrophone: %s", this,
      aMute ? "mute" : "unmute");

  if (GetDevice() &&
      (GetDevice()->GetMediaSource() == MediaSourceEnum::Microphone)) {
    SetDeviceMuted(aMute);
  }
}

bool DeviceListener::CapturingVideo() const {
  MOZ_ASSERT(NS_IsMainThread());
  return Activated() && mDeviceState && !mDeviceState->mStopped &&
         MediaEngineSource::IsVideo(GetDevice()->GetMediaSource()) &&
         (!GetDevice()->IsFake() ||
          Preferences::GetBool("media.navigator.permission.fake"));
}

bool DeviceListener::CapturingAudio() const {
  MOZ_ASSERT(NS_IsMainThread());
  return Activated() && mDeviceState && !mDeviceState->mStopped &&
         MediaEngineSource::IsAudio(GetDevice()->GetMediaSource()) &&
         (!GetDevice()->IsFake() ||
          Preferences::GetBool("media.navigator.permission.fake"));
}

CaptureState DeviceListener::CapturingSource(MediaSourceEnum aSource) const {
  MOZ_ASSERT(NS_IsMainThread());
  if (GetDevice()->GetMediaSource() != aSource) {
    // This DeviceListener doesn't capture a matching source
    return CaptureState::Off;
  }

  if (mDeviceState->mStopped) {
    // The source is a match but has been permanently stopped
    return CaptureState::Off;
  }

  if ((aSource == MediaSourceEnum::Camera ||
       aSource == MediaSourceEnum::Microphone) &&
      GetDevice()->IsFake() &&
      !Preferences::GetBool("media.navigator.permission.fake")) {
    // Fake Camera and Microphone only count if there is no fake permission
    return CaptureState::Off;
  }

  // Source is a match and is active and unmuted

  if (mDeviceState->mDeviceEnabled && !mDeviceState->mDeviceMuted) {
    return CaptureState::Enabled;
  }

  return CaptureState::Disabled;
}

RefPtr<DeviceListener::DeviceListenerPromise> DeviceListener::ApplyConstraints(
    const MediaTrackConstraints& aConstraints, CallerType aCallerType) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mStopped || mDeviceState->mStopped) {
    LOG("DeviceListener %p %s device applyConstraints, but device is stopped",
        this,
        nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
            .get());
    return DeviceListenerPromise::CreateAndResolve(false, __func__);
  }

  MediaManager* mgr = MediaManager::GetIfExists();
  if (!mgr) {
    return DeviceListenerPromise::CreateAndResolve(false, __func__);
  }

  return MediaManager::Dispatch<DeviceListenerPromise>(
      __func__, [device = mDeviceState->mDevice, aConstraints, aCallerType](
                    MozPromiseHolder<DeviceListenerPromise>& aHolder) 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 =
            device->Reconfigure(aConstraints, mgr->mPrefs, &badConstraint);
        if (NS_FAILED(rv)) {
          if (rv == NS_ERROR_INVALID_ARG) {
            // Reconfigure failed due to constraints
            if (!badConstraint) {
              nsTArray<RefPtr<LocalMediaDevice>> devices;
              devices.AppendElement(device);
              badConstraint = MediaConstraintsHelper::SelectSettings(
                  NormalizedConstraints(aConstraints), devices, aCallerType);
            }
          } else {
            // Unexpected. ApplyConstraints* cannot fail with any other error.
            badConstraint = "";
            LOG("ApplyConstraints-Task: Unexpected fail %" PRIx32,
                static_cast<uint32_t>(rv));
          }

          aHolder.Reject(MakeRefPtr<MediaMgrError>(
                             MediaMgrError::Name::OverconstrainedError, "",
                             NS_ConvertASCIItoUTF16(badConstraint)),
                         __func__);
          return;
        }
        // Reconfigure was successful
        aHolder.Resolve(false, __func__);
      });
}

PrincipalHandle DeviceListener::GetPrincipalHandle() const {
  return mPrincipalHandle;
}

void GetUserMediaWindowListener::StopSharing() {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");

  for (auto& l : mActiveListeners.Clone()) {
    MediaSourceEnum source = l->GetDevice()->GetMediaSource();
    if (source == MediaSourceEnum::Screen ||
        source == MediaSourceEnum::Window ||
        source == MediaSourceEnum::AudioCapture) {
      l->Stop();
    }
  }
}

void GetUserMediaWindowListener::StopRawID(const nsString& removedDeviceID) {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");

  for (auto& l : mActiveListeners.Clone()) {
    if (removedDeviceID.Equals(l->GetDevice()->RawID())) {
      l->Stop();
    }
  }
}

void GetUserMediaWindowListener::MuteOrUnmuteCameras(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");

  if (mCamerasAreMuted == aMute) {
    return;
  }
  mCamerasAreMuted = aMute;

  for (auto& l : mActiveListeners) {
    if (l->GetDevice()->Kind() == MediaDeviceKind::Videoinput) {
      l->MuteOrUnmuteCamera(aMute);
    }
  }
}

void GetUserMediaWindowListener::MuteOrUnmuteMicrophones(bool aMute) {
  MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");

  if (mMicrophonesAreMuted == aMute) {
    return;
  }
  mMicrophonesAreMuted = aMute;

  for (auto& l : mActiveListeners) {
    if (l->GetDevice()->Kind() == MediaDeviceKind::Audioinput) {
      l->MuteOrUnmuteMicrophone(aMute);
    }
  }
}

void GetUserMediaWindowListener::ChromeAffectingStateChanged() {
  MOZ_ASSERT(NS_IsMainThread());

  // We wait until stable state before notifying chrome so chrome only does
  // one update if more updates happen in this event loop.

  if (mChromeNotificationTaskPosted) {
    return;
  }

  nsCOMPtr<nsIRunnable> runnable =
      NewRunnableMethod("GetUserMediaWindowListener::NotifyChrome", this,
                        &GetUserMediaWindowListener::NotifyChrome);
  nsContentUtils::RunInStableState(runnable.forget());
  mChromeNotificationTaskPosted = true;
}

void GetUserMediaWindowListener::NotifyChrome() {
  MOZ_ASSERT(mChromeNotificationTaskPosted);
  mChromeNotificationTaskPosted = false;

  NS_DispatchToMainThread(NS_NewRunnableFunction(
      "MediaManager::NotifyChrome", [windowID = mWindowID]() {
        auto* window = nsGlobalWindowInner::GetInnerWindowWithId(windowID);
        if (!window) {
          MOZ_ASSERT_UNREACHABLE("Should have window");
          return;
        }

        nsresult rv = MediaManager::NotifyRecordingStatusChange(window);
        if (NS_FAILED(rv)) {
          MOZ_ASSERT_UNREACHABLE("Should be able to notify chrome");
          return;
        }
      }));
}

#undef LOG

}  // namespace mozilla
