// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/renderer_host/media/media_devices_manager.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <functional>
#include <map>
#include <string>
#include <vector>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/thread_checker.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/media/media_devices_permission_checker.h"
#include "content/browser/renderer_host/media/media_stream_manager.h"
#include "content/browser/renderer_host/media/video_capture_manager.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/common/features.h"
#include "content/public/browser/audio_service.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/common/content_features.h"
#include "media/audio/audio_device_description.h"
#include "media/audio/audio_system.h"
#include "media/base/media_switches.h"
#include "media/capture/capture_switches.h"
#include "media/capture/mojom/video_capture_types.mojom-shared.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/audio/public/mojom/device_notifications.mojom.h"
#include "services/video_capture/public/cpp/features.h"
#include "third_party/blink/public/common/mediastream/media_devices.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom.h"

#if BUILDFLAG(IS_MAC)
#include "base/functional/callback_helpers.h"
#include "base/task/single_thread_task_runner.h"
#include "content/browser/browser_main_loop.h"
#include "media/device_monitors/device_monitor_mac.h"
#endif

namespace content {

// Release video source provider in VideoCaptureDevicesChangedObserver
// if it is not used.
// Do not enable by default until https://crbug.com/377749384 is fixed.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
BASE_FEATURE(kReleaseVideoSourceProviderIfNotInUse,
             base::FEATURE_DISABLED_BY_DEFAULT);

const base::FeatureParam<base::TimeDelta> kReleaseVideoSourceProviderTimeout{
    &kReleaseVideoSourceProviderIfNotInUse,
    "release_video_source_provider_timeout", base::Seconds(60)};
#endif

BASE_FEATURE(kEnumerateDevicesRelaxedCache,
#if BUILDFLAG(IS_WIN)
             base::FEATURE_ENABLED_BY_DEFAULT
#else
             base::FEATURE_DISABLED_BY_DEFAULT
#endif
);

BASE_FEATURE(kEnumerateDevicesAlwaysProcessRequestsOnDevicesEnumerated,
             base::FEATURE_ENABLED_BY_DEFAULT);

namespace {
using media::mojom::DeviceEnumerationResult;

// Resolutions used if the source doesn't support capability enumeration.
struct {
  uint16_t width;
  uint16_t height;
} const kFallbackVideoResolutions[] = {{1920, 1080}, {1280, 720}, {640, 480}};

// Frame rates for sources with no support for capability enumeration.
const uint16_t kFallbackVideoFrameRates[] = {30, 60};

void SendLogMessage(const std::string& message) {
  MediaStreamManager::SendMessageToNativeLog("MDM::" + message);
}

const char* DeviceTypeToString(MediaDeviceType device_type) {
  switch (device_type) {
    case MediaDeviceType::kMediaAudioInput:
      return "AUDIO_INPUT";
    case MediaDeviceType::kMediaVideoInput:
      return "VIDEO_INPUT";
    case MediaDeviceType::kMediaAudioOutput:
      return "AUDIO_OUTPUT";
    default:
      NOTREACHED();
  }
}

std::string GetDevicesEnumeratedLogString(
    uint64_t request_id,
    MediaDeviceType device_type,
    const blink::WebMediaDeviceInfoArray& device_infos) {
  std::string str =
      base::StringPrintf("DevicesEnumerated({request_id=%llu}, {type=%s}, ",
                         request_id, DeviceTypeToString(device_type));
  base::StringAppendF(&str, "{labels=[");
  for (const auto& device_info : device_infos) {
    base::StringAppendF(&str, "%s, ", device_info.label.c_str());
  }
  if (device_infos.size() > 0) {
    str.erase(str.end() - 2, str.end());
  }
  str += "]})";
  return str;
}

blink::WebMediaDeviceInfoArray GetFakeAudioDevices(bool is_input) {
  blink::WebMediaDeviceInfoArray result;
  if (is_input) {
    result.emplace_back(media::AudioDeviceDescription::kDefaultDeviceId,
                        "Fake Default Audio Input",
                        "fake_group_audio_input_default");
    result.emplace_back("fake_audio_input_1", "Fake Audio Input 1",
                        "fake_group_audio_input_1");
    result.emplace_back("fake_audio_input_2", "Fake Audio Input 2",
                        "fake_group_audio_input_2");
  } else {
    result.emplace_back(media::AudioDeviceDescription::kDefaultDeviceId,
                        "Fake Default Audio Output",
                        "fake_group_audio_output_default");
    result.emplace_back("fake_audio_output_1", "Fake Audio Output 1",
                        "fake_group_audio_output_1");
    result.emplace_back("fake_audio_output_2", "Fake Audio Output 2",
                        "fake_group_audio_output_2");
  }

  return result;
}

std::string VideoLabelWithoutModelID(const std::string& label) {
  if (label.rfind(")") != label.size() - 1)
    return label;

  auto idx = label.rfind(" (");
  if (idx == std::string::npos)
    return label;

  return label.substr(0, idx - 1);
}

bool LabelHasUSBModel(const std::string& label) {
  return label.size() >= 11 && label[label.size() - 11] == '(' &&
         label[label.size() - 6] == ':' && label[label.size() - 1] == ')';
}

std::string GetUSBModelFromLabel(const std::string& label) {
  CHECK(LabelHasUSBModel(label), base::NotFatalUntil::M152);
  return label.substr(label.size() - 10, 9);
}

bool IsRealAudioDeviceID(const std::string& device_id) {
  return !media::AudioDeviceDescription::IsDefaultDevice(device_id) &&
         !media::AudioDeviceDescription::IsCommunicationsDevice(device_id);
}

bool EqualDeviceExcludingGroupID(const blink::WebMediaDeviceInfo& lhs,
                                 const blink::WebMediaDeviceInfo& rhs) {
  return lhs.device_id == rhs.device_id && lhs.label == rhs.label &&
         lhs.video_facing == rhs.video_facing &&
         lhs.availability == rhs.availability;
}

bool EqualDeviceIncludingGroupID(const blink::WebMediaDeviceInfo& lhs,
                                 const blink::WebMediaDeviceInfo& rhs) {
  return EqualDeviceExcludingGroupID(lhs, rhs) && lhs.group_id == rhs.group_id;
}

void ReplaceInvalidFrameRatesWithFallback(media::VideoCaptureFormats* formats) {
  for (auto& format : *formats) {
    if (format.frame_rate <= 0)
      format.frame_rate = kFallbackVideoFrameRates[0];
  }
}

void SendDeviceNotifierToAudioService(
    mojo::PendingReceiver<audio::mojom::DeviceNotifier>
        device_notifier_receiver) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M155);
  GetAudioService().BindDeviceNotifier(std::move(device_notifier_receiver));
}

void ReportVideoEnumerationStart() {
  base::UmaHistogramBoolean(
      "Media.MediaDevicesManager.VideoDeviceEnumeration.Start", true);
}

void ReportVideoEnumerationResult(DeviceEnumerationResult result_code) {
  base::UmaHistogramEnumeration(
      "Media.MediaDevicesManager.VideoDeviceEnumeration.Result", result_code);
}

BrowserContext* GetBrowserContextOnUIThread(
    GlobalRenderFrameHostId render_frame_host_id) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M155);
  auto* rfh = RenderFrameHostImpl::FromID(render_frame_host_id);
  if (!rfh) {
    return nullptr;
  }
  return rfh->GetBrowserContext();
}

// Sort the devices according to user pref. If the pref is unset or
// the render frame host doesn't have a `BrowserContext` the ordering will be
// unmodified.
void RankDevices(
    GlobalRenderFrameHostId render_frame_host_id,
    const MediaDevicesManager::BoolDeviceTypes& requested_types,
    base::OnceCallback<void(const MediaDeviceEnumeration&)> callback,
    const MediaDeviceEnumeration& enumeration) {
  CHECK_CURRENTLY_ON(BrowserThread::UI, base::NotFatalUntil::M155);

  auto* browser_context = GetBrowserContextOnUIThread(render_frame_host_id);
  if (!browser_context) {
    std::move(callback).Run(enumeration);
    return;
  }

  ContentBrowserClient* content_client_browser = GetContentClient()->browser();
  auto ranked_enumeration = enumeration;

  if (requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)]) {
    content_client_browser->PreferenceRankAudioDeviceInfos(
        browser_context, ranked_enumeration[static_cast<size_t>(
                             MediaDeviceType::kMediaAudioInput)]);
  }

  if (requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)]) {
    content_client_browser->PreferenceRankVideoDeviceInfos(
        browser_context, ranked_enumeration[static_cast<size_t>(
                             MediaDeviceType::kMediaVideoInput)]);
  }

  std::move(callback).Run(ranked_enumeration);
}

}  // namespace

std::string GuessVideoGroupID(const blink::WebMediaDeviceInfoArray& audio_infos,
                              const blink::WebMediaDeviceInfo& video_info) {
  const std::string video_label = VideoLabelWithoutModelID(video_info.label);

  // If |video_label| is very small, do not guess in order to avoid false
  // positives.
  if (video_label.size() <= 3)
    return video_info.device_id;

  base::RepeatingCallback<bool(const blink::WebMediaDeviceInfo&)>
      video_label_is_included_in_audio_label = base::BindRepeating(
          [](const std::string& video_label,
             const blink::WebMediaDeviceInfo& audio_info) {
            return audio_info.label.find(video_label) != std::string::npos;
          },
          std::cref(video_label));

  const bool video_has_usb_model = LabelHasUSBModel(video_info.label);
  std::string video_usb_model = video_has_usb_model
                                    ? GetUSBModelFromLabel(video_info.label)
                                    : std::string();
  base::RepeatingCallback<bool(const blink::WebMediaDeviceInfo&)>
      usb_model_matches = base::BindRepeating(
          [](bool video_has_usb_model, const std::string& video_usb_model,
             const blink::WebMediaDeviceInfo& audio_info) {
            return video_has_usb_model && LabelHasUSBModel(audio_info.label)
                       ? video_usb_model ==
                             GetUSBModelFromLabel(audio_info.label)
                       : false;
          },
          video_has_usb_model, std::cref(video_usb_model));

  for (auto* callback :
       {&video_label_is_included_in_audio_label, &usb_model_matches}) {
    // The label for the default and communication audio devices may contain the
    // same label as the real devices, so they should be ignored when trying to
    // find unique matches.
    auto real_device_matches =
        [callback](const blink::WebMediaDeviceInfo& audio_info) {
          return IsRealAudioDeviceID(audio_info.device_id) &&
                 (*callback).Run(audio_info);
        };
    auto it_first = std::ranges::find_if(audio_infos, real_device_matches);
    if (it_first == audio_infos.end())
      continue;

    auto it = it_first;
    bool duplicate_found = false;
    while ((it = std::find_if(it + 1, audio_infos.end(),
                              real_device_matches)) != audio_infos.end()) {
      // If there is more than one match, it is impossible to know which group
      // ID is the correct one. This may occur if multiple devices of the same
      // model are installed.
      if (it->group_id != it_first->group_id) {
        duplicate_found = true;
        break;
      }
    }

    if (!duplicate_found)
      return it_first->group_id;
  }

  return video_info.device_id;
}

bool MediaDevicesManager::IsRelaxedCacheFeatureEnabled() {
  return base::FeatureList::IsEnabled(kEnumerateDevicesRelaxedCache);
}

struct MediaDevicesManager::EnumerationRequest {
  EnumerationRequest(const BoolDeviceTypes& requested_types,
                     EnumerationCallback callback)
      : callback(std::move(callback)) {
    requested = requested_types;
  }

  BoolDeviceTypes requested;
  BoolDeviceTypes has_seen_result_for_request;
  EnumerationCallback callback;
};

// This class helps manage the consistency of cached enumeration results.
// It uses a sequence number for each invalidation and enumeration.
// A cache is considered valid if the sequence number for the last enumeration
// is greater than the sequence number for the last invalidation.
// The advantage of using invalidations over directly issuing enumerations upon
// each system notification is that some platforms issue multiple notifications
// on each device change. The cost of performing multiple redundant
// invalidations is significantly lower than the cost of issuing multiple
// redundant enumerations.
// In some rare cases, issuing an enumeration might also trigger (spurious)
// system notifications. This can cause an infinite loop of
// enumerations/notifications that can keep a cache invalid indefinitely
// (see https://crbug.com/325590346) and leave the system consuming resources.
// To prevent this, the cache enters a relaxed mode after multiple spurious
// invalidations, such that further invalidations are ignored and the cache is
// considered valid for some time after an enumeration.
class MediaDevicesManager::CacheInfo {
 public:
  explicit CacheInfo(bool allow_relaxed_mode)
      : allow_relaxed_mode_(allow_relaxed_mode) {}

  void InvalidateCache() {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    seq_last_invalidation_ = NewEventSequence();
  }

  // Returns true if the cached result is valid according to the cache policy
  // after receiving a new value from the lower layers (as a result of a
  // low-level enumeration request).
  // In normal cache mode the result is considered invalid if a new invalidation
  // was received after the low-level enumeration started.
  // In relaxed mode the result is valid as long as it is recent enough,
  // regardless of any invalidations.
  bool IsLastUpdateValid() const {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    return (seq_last_update_ > seq_last_invalidation_ && !is_update_ongoing_) ||
           (IsRelaxedCacheFeatureEnabled() && is_in_relaxed_mode_ &&
            !IsCacheExpired());
  }

  // Returns true if the current cached values need to be updated upon receiving
  // an invalidation, depending on the cache policy.
  // This is intended to be used to decide if a new low-level enumeration needs
  // to be issued.
  // In normal cache mode, the cache needs to be updated as soon as an
  // invalidation is received.
  // In relaxed mode if the cached value is recent enough, it does not need to
  // be updated even if there is an invalidation.
  bool NeedsUpdateUponInvalidation() const {
    CHECK(IsRelaxedCacheFeatureEnabled());
    return is_in_relaxed_mode_
               ? seq_last_invalidation_ > seq_last_update_ && IsCacheExpired()
               : seq_last_invalidation_ > seq_last_update_;
  }

  void UpdateStarted() {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    CHECK(!is_update_ongoing_, base::NotFatalUntil::M155);
    seq_last_update_ = NewEventSequence();
    is_update_ongoing_ = true;
  }

  void UpdateCompleted() {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    CHECK(is_update_ongoing_, base::NotFatalUntil::M155);
    is_update_ongoing_ = false;
    if (IsRelaxedCacheFeatureEnabled() && is_in_relaxed_mode_) {
      seq_last_update_ = NewEventSequence();
      time_last_update_ = base::TimeTicks::Now();
    }
  }

  bool is_update_ongoing() const {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    return is_update_ongoing_;
  }

  // This should be called whenever a legitimate reason to allow a future
  // low-level device enumeration is detected.
  void ResetSpuriousInvalidations() {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    CHECK(IsRelaxedCacheFeatureEnabled());
    num_spurious_invalidations_ = 0;
  }

  // This should be called whenever a low-level enumeration does not result in
  // changes to the cache.
  void RecordSpuriousInvalidation() {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    CHECK(IsRelaxedCacheFeatureEnabled());
    if (is_in_relaxed_mode_ || !allow_relaxed_mode_) {
      return;
    }
    if (++num_spurious_invalidations_ >= kMaxSpuriousInvalidations) {
      is_in_relaxed_mode_ = true;
      ResetSpuriousInvalidations();
    }
  }

  bool allow_relaxed_mode() const { return allow_relaxed_mode_; }

 private:
  bool IsCacheExpired() const {
    CHECK(IsRelaxedCacheFeatureEnabled());
    return (base::TimeTicks::Now() - time_last_update_) >=
           kExpireTimeInRelaxedMode;
  }

  int64_t NewEventSequence() {
    CHECK(thread_checker_.CalledOnValidThread(), base::NotFatalUntil::M155);
    return ++current_event_sequence_;
  }

  int64_t current_event_sequence_ = 0;
  int64_t seq_last_update_ = 0;
  int64_t seq_last_invalidation_ = 0;
  bool is_update_ongoing_ = false;
  int num_spurious_invalidations_ = 0;
  const bool allow_relaxed_mode_;
  // This is eventually set to true on systems where the normal cache policy
  // can result in notification loops. Once set to true, it is never set back
  // to false in order to avoid these loops. See https://crbug.com/325590346.
  // An invariant that must be preserved at all times is that
  // `is_in_relaxed_mode_` can be set to true only if `allow_relaxed_mode_` is
  // true.
  bool is_in_relaxed_mode_ = false;
  base::TimeTicks time_last_update_;
  base::ThreadChecker thread_checker_;
};

MediaDevicesManager::SubscriptionRequest::SubscriptionRequest(
    GlobalRenderFrameHostId render_frame_host_id,
    const BoolDeviceTypes& subscribe_types,
    mojo::Remote<blink::mojom::MediaDevicesListener> listener)
    : render_frame_host_id(render_frame_host_id),
      subscribe_types(subscribe_types),
      listener_(std::move(listener)) {}

MediaDevicesManager::SubscriptionRequest::SubscriptionRequest(
    SubscriptionRequest&&) = default;

MediaDevicesManager::SubscriptionRequest::~SubscriptionRequest() = default;

MediaDevicesManager::SubscriptionRequest&
MediaDevicesManager::SubscriptionRequest::operator=(SubscriptionRequest&&) =
    default;

class MediaDevicesManager::AudioServiceDeviceListener
    : public audio::mojom::DeviceListener {
 public:
  AudioServiceDeviceListener(base::RepeatingClosure invalidate_cache_cb,
                             base::RepeatingClosure enumerate_system_devices_cb)
      : invalidate_cache_cb_(std::move(invalidate_cache_cb)),
        enumerate_system_devices_cb_(std::move(enumerate_system_devices_cb)) {
    ConnectToService(/*is_reconnect=*/false);
  }

  AudioServiceDeviceListener(const AudioServiceDeviceListener&) = delete;
  AudioServiceDeviceListener& operator=(const AudioServiceDeviceListener&) =
      delete;

  ~AudioServiceDeviceListener() override = default;

  void DevicesChanged() override {
    auto* system_monitor = base::SystemMonitor::Get();
    if (system_monitor)
      system_monitor->ProcessDevicesChanged(base::SystemMonitor::DEVTYPE_AUDIO);
  }

 private:
  void ConnectToService(bool is_reconnect) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    CHECK(!mojo_audio_device_notifier_, base::NotFatalUntil::M155);
    CHECK(!receiver_.is_bound(), base::NotFatalUntil::M155);
    CHECK(enumerate_system_devices_cb_);
    mojo::PendingReceiver<audio::mojom::DeviceNotifier>
        device_notifier_receiver =
            mojo_audio_device_notifier_.BindNewPipeAndPassReceiver();
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE, base::BindOnce(&SendDeviceNotifierToAudioService,
                                  std::move(device_notifier_receiver)));
    mojo_audio_device_notifier_.set_disconnect_handler(base::BindOnce(
        &MediaDevicesManager::AudioServiceDeviceListener::OnConnectionError,
        weak_factory_.GetWeakPtr()));
    mojo_audio_device_notifier_->RegisterListener(
        receiver_.BindNewPipeAndPassRemote());

    if (is_reconnect) {
      enumerate_system_devices_cb_.Run();
    }
  }

  void OnConnectionError() {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    CHECK(invalidate_cache_cb_);
    mojo_audio_device_notifier_.reset();
    receiver_.reset();
    invalidate_cache_cb_.Run();

    // Resetting the error handler in a posted task since doing it synchronously
    // results in a browser crash. See https://crbug.com/845142.
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(&AudioServiceDeviceListener::ConnectToService,
                       weak_factory_.GetWeakPtr(), /*is_reconnect=*/true));
  }

  // |invalidate_cache_cb_| is a callback used to invalidate the cache upon
  // disconnection.
  const base::RepeatingClosure invalidate_cache_cb_;
  // |enumerate_system_devices_cb_| is a callback used to force a fresh
  // enumeration upon successful reconnection.
  const base::RepeatingClosure enumerate_system_devices_cb_;
  mojo::Receiver<audio::mojom::DeviceListener> receiver_{this};
  mojo::Remote<audio::mojom::DeviceNotifier> mojo_audio_device_notifier_;

  SEQUENCE_CHECKER(sequence_checker_);

  base::WeakPtrFactory<AudioServiceDeviceListener> weak_factory_{this};
};

MediaDevicesManager::MediaDevicesManager(
    media::AudioSystem* audio_system,
    const scoped_refptr<VideoCaptureManager>& video_capture_manager,
    StopRemovedInputDeviceCallback stop_removed_input_device_cb,
    UIInputDeviceChangeCallback ui_input_device_change_cb)
    : use_fake_devices_(base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kUseFakeDeviceForMediaStream)),
      audio_system_(audio_system),
      video_capture_manager_(video_capture_manager),
      stop_removed_input_device_cb_(std::move(stop_removed_input_device_cb)),
      ui_input_device_change_cb_(std::move(ui_input_device_change_cb)),
      permission_checker_(std::make_unique<MediaDevicesPermissionChecker>()),
      get_salt_and_origin_cb_(
          base::BindRepeating(&GetMediaDeviceSaltAndOrigin)) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(audio_system_, base::NotFatalUntil::M155);
  CHECK(video_capture_manager_.get(), base::NotFatalUntil::M155);
  CHECK(!stop_removed_input_device_cb_.is_null(), base::NotFatalUntil::M155);
  CHECK(!ui_input_device_change_cb_.is_null(), base::NotFatalUntil::M155);
  // Enable relaxed mode only for cameras.
  // Audio devices do not need relaxed mode and it can interfere with bluetooth
  // notifications. See https://crbug.com/417256410
  cache_infos_.emplace_back(/*allow_relaxed_mode=*/false);
  cache_infos_.emplace_back(/*allow_relaxed_mode=*/true);
  cache_infos_.emplace_back(/*allow_relaxed_mode=*/false);
  CHECK_EQ(cache_infos_.size(),
           static_cast<size_t>(MediaDeviceType::kNumMediaDeviceTypes));
  CHECK(!cache_infos_[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)]
             .allow_relaxed_mode());
  CHECK(cache_infos_[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)]
            .allow_relaxed_mode());
  CHECK(!cache_infos_[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]
             .allow_relaxed_mode());
  cache_policies_.fill(CachePolicy::NO_CACHE);
}
MediaDevicesManager::~MediaDevicesManager() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  if (base::FeatureList::IsEnabled(kReleaseVideoSourceProviderIfNotInUse)) {
    disconnect_video_source_provider_timer_.Stop();
  }
#endif
}

void MediaDevicesManager::EnumerateDevices(
    const BoolDeviceTypes& requested_types,
    EnumerationCallback callback,
    uint64_t request_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  SendLogMessage(
      base::StringPrintf("EnumerateDevices({request_id=%llu})", request_id));
  bool start_audio_monitoring =
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] ||
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)];
  bool start_video_monitoring =
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)];

  DeviceStartMonitoringMode start_mode = DeviceStartMonitoringMode::kNone;
  if (start_audio_monitoring && start_video_monitoring) {
    start_mode = DeviceStartMonitoringMode::kStartAudioAndVideo;
  } else if (start_audio_monitoring) {
    start_mode = DeviceStartMonitoringMode::kStartAudio;
  } else if (start_video_monitoring) {
    start_mode = DeviceStartMonitoringMode::kStartVideo;
  }
  StartMonitoringAndPopulateCache(request_id, start_mode);

  client_requests_.emplace_back(requested_types, std::move(callback));
  bool all_results_cached = true;
  for (size_t i = 0;
       i < static_cast<size_t>(MediaDeviceType::kNumMediaDeviceTypes); ++i) {
    if (IsRelaxedCacheFeatureEnabled()) {
      // Reset the spurious invalidation count in case of a new client request
      // to reduce the probability of entering relaxed mode in the cache and
      // returning potentially outdated results.
      cache_infos_[i].ResetSpuriousInvalidations();
    }
    if (requested_types[i] && cache_policies_[i] == CachePolicy::NO_CACHE) {
      all_results_cached = false;
      EnumerateSystemDevices(request_id, static_cast<MediaDeviceType>(i));
    }
  }

  if (all_results_cached) {
    ProcessClientRequests();
  }
}

void MediaDevicesManager::EnumerateAndRankDevices(
    GlobalRenderFrameHostId render_frame_host_id,
    const BoolDeviceTypes& requested_types,
    EnumerationCallback callback,
    uint64_t request_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  SendLogMessage(base::StringPrintf(
      "EnumerateAndRankDevices({request_id=%llu})", request_id));
  EnumerateDevices(
      requested_types,
      base::BindPostTask(
          GetUIThreadTaskRunner(),
          base::BindOnce(
              &RankDevices, render_frame_host_id, requested_types,
              base::BindPostTaskToCurrentDefault(std::move(callback)))),
      request_id);
}

void MediaDevicesManager::HandleEnumerateDevicesRequest(
    GlobalRenderFrameHostId render_frame_host_id,
    const BoolDeviceTypes& requested_types,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(request_video_input_capabilities &&
                requested_types[static_cast<size_t>(
                    MediaDeviceType::kMediaVideoInput)] ||
            !request_video_input_capabilities,
        base::NotFatalUntil::M155);
  CHECK(request_audio_input_capabilities &&
                requested_types[static_cast<size_t>(
                    MediaDeviceType::kMediaAudioInput)] ||
            !request_audio_input_capabilities,
        base::NotFatalUntil::M155);
  uint64_t request_id = ++next_enumeration_request_id_;
  SendLogMessage(base::StringPrintf(
      "HandleEnumerateDevicesRequest({request_id=%llu}, "
      "{render_process_id=%d}, "
      "{render_frame_id=%d}, {audio_input=%s}, {video_input=%s}, "
      "{audio_output=%s}, {request_audio_capabilities=%s}, "
      "{request_video_capabilities=%s})",
      request_id, render_frame_host_id.child_id.value(),
      render_frame_host_id.frame_routing_id,
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)]
          ? "true"
          : "false",
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)]
          ? "true"
          : "false",
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]
          ? "true"
          : "false",
      base::ToString(request_audio_input_capabilities).c_str(),
      base::ToString(request_video_input_capabilities).c_str()));

  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          get_salt_and_origin_cb_, render_frame_host_id,
          base::BindPostTaskToCurrentDefault(base::BindOnce(
              &MediaDevicesManager::CheckPermissionsForEnumerateDevices,
              weak_factory_.GetWeakPtr(), request_id, render_frame_host_id,
              requested_types, request_video_input_capabilities,
              request_audio_input_capabilities, std::move(callback)))));
}

void MediaDevicesManager::AddAudioDeviceToOriginMap(
    GlobalRenderFrameHostId render_frame_host_id,
    const blink::WebMediaDeviceInfo& device_info) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  audio_device_origin_map_[render_frame_host_id].insert(device_info);
}

bool MediaDevicesManager::IsAudioOutputDeviceExplicitlyAuthorized(
    GlobalRenderFrameHostId render_frame_host_id,
    const std::string& raw_device_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  auto authorized_devices = audio_device_origin_map_.find(render_frame_host_id);
  if (authorized_devices == audio_device_origin_map_.end()) {
    return false;
  }
  blink::WebMediaDeviceInfo device_info;
  device_info.device_id = raw_device_id;
  return authorized_devices->second.contains(device_info);
}

void MediaDevicesManager::GetSpeakerSelectionAndMicrophonePermissionState(
    GlobalRenderFrameHostId render_frame_host_id,
    base::OnceCallback<void(PermissionDeniedState, bool)> callback) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);

  permission_checker_->GetSpeakerSelectionAndMicrophonePermissionState(
      render_frame_host_id, std::move(callback));
}

uint32_t MediaDevicesManager::SubscribeDeviceChangeNotifications(
    GlobalRenderFrameHostId render_frame_host_id,
    const BoolDeviceTypes& subscribe_types,
    mojo::PendingRemote<blink::mojom::MediaDevicesListener> listener) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  bool start_audio_monitoring =
      subscribe_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] ||
      subscribe_types[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)];
  bool start_video_monitoring =
      subscribe_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)];
  DeviceStartMonitoringMode start_mode = DeviceStartMonitoringMode::kNone;
  if (start_audio_monitoring && start_video_monitoring) {
    start_mode = DeviceStartMonitoringMode::kStartAudioAndVideo;
  } else if (start_audio_monitoring) {
    start_mode = DeviceStartMonitoringMode::kStartAudio;
  } else if (start_video_monitoring) {
    start_mode = DeviceStartMonitoringMode::kStartVideo;
  }
  StartMonitoringAndPopulateCache(0, start_mode);

  uint32_t subscription_id = ++last_subscription_id_;
  mojo::Remote<blink::mojom::MediaDevicesListener> media_devices_listener;
  media_devices_listener.Bind(std::move(listener));
  media_devices_listener.set_disconnect_handler(
      base::BindOnce(&MediaDevicesManager::UnsubscribeDeviceChangeNotifications,
                     weak_factory_.GetWeakPtr(), subscription_id));
  subscriptions_.emplace(
      subscription_id,
      SubscriptionRequest(render_frame_host_id, subscribe_types,
                          std::move(media_devices_listener)));
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  if (base::FeatureList::IsEnabled(kReleaseVideoSourceProviderIfNotInUse)) {
    MaybeScheduleDisconnectVideoSourceProviderTimer();
  }
#endif

  // Fetch the first device_id_salt for this subscriber's frame, to be able to
  // later detect changes.
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          get_salt_and_origin_cb_, render_frame_host_id,
          base::BindPostTaskToCurrentDefault(base::BindOnce(
              &MediaDevicesManager::SetSubscriptionLastSeenDeviceIdSalt,
              weak_factory_.GetWeakPtr(), subscription_id))));

  return subscription_id;
}

void MediaDevicesManager::SetSubscriptionLastSeenDeviceIdSalt(
    uint32_t subscription_id,
    const MediaDeviceSaltAndOrigin& salt_and_origin) {
  auto it = subscriptions_.find(subscription_id);

  if (it == subscriptions_.end())
    return;
  SubscriptionRequest& request = it->second;

  request.last_seen_device_id_salt_ = salt_and_origin.device_id_salt();
}

void MediaDevicesManager::UnsubscribeDeviceChangeNotifications(
    uint32_t subscription_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  subscriptions_.erase(subscription_id);
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  if (base::FeatureList::IsEnabled(kReleaseVideoSourceProviderIfNotInUse)) {
    MaybeScheduleDisconnectVideoSourceProviderTimer();
  }
#endif
}

void MediaDevicesManager::SetCachePolicy(MediaDeviceType type,
                                         CachePolicy policy,
                                         uint64_t request_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);
  if (cache_policies_[static_cast<size_t>(type)] == policy)
    return;

  cache_policies_[static_cast<size_t>(type)] = policy;
  // If the new policy is SYSTEM_MONITOR, issue an enumeration to populate the
  // cache.
  if (policy == CachePolicy::SYSTEM_MONITOR) {
    InvalidateCache(type);
    EnumerateSystemDevices(request_id, type);
  }
}

void MediaDevicesManager::StartMonitoringAndPopulateCache(uint64_t request_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);

  // Start monitoring all device types.
  StartMonitoringAndPopulateCache(
      request_id, DeviceStartMonitoringMode::kStartAudioAndVideo);
}

void MediaDevicesManager::StartMonitoringAndPopulateCache(
    uint64_t request_id,
    DeviceStartMonitoringMode start_monitoring_mode) {
  bool start_audio_device_monitoring =
      (start_monitoring_mode == DeviceStartMonitoringMode::kStartAudio) ||
      (start_monitoring_mode == DeviceStartMonitoringMode::kStartAudioAndVideo);
  bool start_video_device_monitoring_mode =
      (start_monitoring_mode == DeviceStartMonitoringMode::kStartVideo) ||
      (start_monitoring_mode == DeviceStartMonitoringMode::kStartAudioAndVideo);

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
  if (monitoring_started_for_video_ &&
      base::FeatureList::IsEnabled(kReleaseVideoSourceProviderIfNotInUse)) {
    if (video_capture_service_device_changed_observer_) {
      video_capture_service_device_changed_observer_
          ->EnsureConnectedToService();
    }
  }
#endif

  if (!base::SystemMonitor::Get())
    return;

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
  if (start_audio_device_monitoring && !monitoring_started_for_audio_ &&
      base::FeatureList::IsEnabled(features::kAudioServiceOutOfProcess)) {
    CHECK(!audio_service_device_listener_, base::NotFatalUntil::M155);

    // base::Unretained(this) is safe here because |this| owns
    // |audio_service_device_listener_|.
    audio_service_device_listener_ =
        std::make_unique<AudioServiceDeviceListener>(
            /*invalidate_cache_cb=*/base::BindRepeating(
                [](MediaDevicesManager* mdm) {
                  mdm->InvalidateCache(MediaDeviceType::kMediaAudioInput);
                  mdm->InvalidateCache(MediaDeviceType::kMediaAudioOutput);
                },
                base::Unretained(this)),
            /*enumerate_system_devices_cb=*/base::BindRepeating(
                [](MediaDevicesManager* mdm) {
                  mdm->HandleDevicesChanged(MediaDeviceType::kMediaAudioInput);
                  mdm->HandleDevicesChanged(MediaDeviceType::kMediaAudioOutput);
                },
                base::Unretained(this)));
  }
#endif
  if ((start_audio_device_monitoring && !monitoring_started_for_audio_) ||
      (start_video_device_monitoring_mode && !monitoring_started_for_video_)) {
    SendLogMessage(base::StringPrintf(
        "StartMonitoringAndPopulateCache({request_id=%llu}, {audio=%s}, "
        "{video=%s})",
        request_id, start_audio_device_monitoring ? "true" : "false",
        start_video_device_monitoring_mode ? "true" : "false"));
  }

  if (!added_device_changed_observer_) {
    base::SystemMonitor::Get()->AddDevicesChangedObserver(this);
    added_device_changed_observer_ = true;
  }

  if (base::FeatureList::IsEnabled(features::kMediaDevicesSystemMonitorCache)) {
    if (start_video_device_monitoring_mode && !monitoring_started_for_video_) {
      SetCachePolicy(MediaDeviceType::kMediaVideoInput,
                     CachePolicy::SYSTEM_MONITOR, request_id);
    }

    if (start_audio_device_monitoring && !monitoring_started_for_audio_) {
      SetCachePolicy(MediaDeviceType::kMediaAudioInput,
                     CachePolicy::SYSTEM_MONITOR, request_id);
      SetCachePolicy(MediaDeviceType::kMediaAudioOutput,
                     CachePolicy::SYSTEM_MONITOR, request_id);
    }
  }

  if (start_video_device_monitoring_mode && !monitoring_started_for_video_) {
#if BUILDFLAG(IS_MAC)
    RegisterVideoCaptureDevicesChangedObserver();
#endif
#if BUILDFLAG(IS_WIN)
    if ((base::FeatureList::IsEnabled(
             video_capture::features::
                 kWinCameraMonitoringInVideoCaptureService) ||
         switches::IsMediaFoundationCameraUsageMonitoringEnabled()) &&
        !base::FeatureList::IsEnabled(
            features::kRunVideoCaptureServiceInBrowserProcess)) {
      RegisterVideoCaptureDevicesChangedObserver();
    }
#endif
  }

  monitoring_started_for_video_ |= start_video_device_monitoring_mode;
  monitoring_started_for_audio_ |= start_audio_device_monitoring;
}

void MediaDevicesManager::StopMonitoring() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);

  // Stop monitoring for the all device types.
  StopMonitoring(DeviceStopMonitoringMode::kStopAudioAndVideo);
}

void MediaDevicesManager::StopMonitoring(
    DeviceStopMonitoringMode device_monitoring_mode) {
  // Capture state before stopping
  bool stopped_audio = monitoring_started_for_audio_;
  bool stopped_video = monitoring_started_for_video_;

  bool stop_audio_device_monitoring =
      (device_monitoring_mode == DeviceStopMonitoringMode::kStopAudio) ||
      (device_monitoring_mode == DeviceStopMonitoringMode::kStopAudioAndVideo);

  bool stop_video_device_monitoring_mode =
      (device_monitoring_mode == DeviceStopMonitoringMode::kStopVideo) ||
      (device_monitoring_mode == DeviceStopMonitoringMode::kStopAudioAndVideo);

  if (stop_audio_device_monitoring && monitoring_started_for_audio_) {
    audio_service_device_listener_.reset();
    monitoring_started_for_audio_ = false;
    SetCachePolicy(MediaDeviceType::kMediaAudioInput, CachePolicy::NO_CACHE);
    SetCachePolicy(MediaDeviceType::kMediaAudioOutput, CachePolicy::NO_CACHE);
  }

  if (stop_video_device_monitoring_mode && monitoring_started_for_video_) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC)
    if (base::FeatureList::IsEnabled(kReleaseVideoSourceProviderIfNotInUse)) {
      disconnect_video_source_provider_timer_.Stop();
    }
#endif
    video_capture_service_device_changed_observer_.reset();
    monitoring_started_for_video_ = false;
    SetCachePolicy(MediaDeviceType::kMediaVideoInput, CachePolicy::NO_CACHE);
  }

  if (!monitoring_started_for_audio_ && !monitoring_started_for_video_ &&
      added_device_changed_observer_) {
    base::SystemMonitor::Get()->RemoveDevicesChangedObserver(this);
    added_device_changed_observer_ = false;
    SendLogMessage(base::StringPrintf("StopMonitoring({audio=%s}, {video=%s})",
                                      stopped_audio ? "true" : "false",
                                      stopped_video ? "true" : "false"));
  }
}

void MediaDevicesManager::OnDevicesChanged(
    base::SystemMonitor::DeviceType device_type) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  switch (device_type) {
    case base::SystemMonitor::DEVTYPE_AUDIO:
      HandleDevicesChanged(MediaDeviceType::kMediaAudioInput);
      HandleDevicesChanged(MediaDeviceType::kMediaAudioOutput);
      break;
    case base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE:
      HandleDevicesChanged(MediaDeviceType::kMediaVideoInput);
      break;
    default:
      break;  // Uninteresting device change.
  }
}

media::VideoCaptureFormats MediaDevicesManager::GetVideoInputFormats(
    const std::string& device_id,
    bool try_in_use_first) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  media::VideoCaptureFormats formats;

  if (try_in_use_first) {
    std::optional<media::VideoCaptureFormat> format =
        video_capture_manager_->GetDeviceFormatInUse(
            blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, device_id);
    if (format.has_value()) {
      formats.push_back(format.value());
      ReplaceInvalidFrameRatesWithFallback(&formats);
      return formats;
    }
  }

  video_capture_manager_->GetDeviceSupportedFormats(device_id, &formats);
  ReplaceInvalidFrameRatesWithFallback(&formats);
  // Remove formats that have zero resolution.
  std::erase_if(formats, [](const media::VideoCaptureFormat& format) {
    return format.frame_size.GetArea() <= 0;
  });

  // If the device does not report any valid format, use a fallback list of
  // standard formats.
  if (formats.empty()) {
    for (const auto& resolution : kFallbackVideoResolutions) {
      for (const auto frame_rate : kFallbackVideoFrameRates) {
        formats.push_back(media::VideoCaptureFormat(
            gfx::Size(resolution.width, resolution.height), frame_rate,
            media::PIXEL_FORMAT_I420));
      }
    }
  }

  return formats;
}

blink::WebMediaDeviceInfoArray MediaDevicesManager::GetCachedDeviceInfo(
    MediaDeviceType type) const {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  return current_snapshot_[static_cast<size_t>(type)];
}

void MediaDevicesManager::RegisterDispatcherHost(
    std::unique_ptr<blink::mojom::MediaDevicesDispatcherHost> dispatcher_host,
    mojo::PendingReceiver<blink::mojom::MediaDevicesDispatcherHost> receiver) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  dispatcher_hosts_.Add(std::move(dispatcher_host), std::move(receiver));
}

void MediaDevicesManager::SetPermissionChecker(
    std::unique_ptr<MediaDevicesPermissionChecker> permission_checker) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(permission_checker, base::NotFatalUntil::M155);
  permission_checker_ = std::move(permission_checker);
}

void MediaDevicesManager::CheckPermissionsForEnumerateDevices(
    uint64_t request_id,
    GlobalRenderFrameHostId render_frame_host_id,
    const BoolDeviceTypes& requested_types,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback,
    const MediaDeviceSaltAndOrigin& salt_and_origin) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  SendLogMessage(base::StringPrintf(
      "CheckPermissionsForEnumerateDevices({request_id=%llu})", request_id));
  permission_checker_->CheckPermissions(
      requested_types, render_frame_host_id,
      base::BindOnce(&MediaDevicesManager::OnPermissionsCheckDone,
                     weak_factory_.GetWeakPtr(), request_id,
                     render_frame_host_id, requested_types,
                     request_video_input_capabilities,
                     request_audio_input_capabilities, std::move(callback),
                     std::move(salt_and_origin)));
}

void MediaDevicesManager::OnPermissionsCheckDone(
    uint64_t request_id,
    GlobalRenderFrameHostId render_frame_host_id,
    const MediaDevicesManager::BoolDeviceTypes& requested_types,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    const MediaDevicesManager::BoolDeviceTypes& has_permissions) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  SendLogMessage(base::StringPrintf(
      "OnPermissionsCheckDone({request_id=%llu}, {has_audio_permission=%s}, "
      "{has_video_permission=%s})",
      request_id,
      base::ToString(has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaAudioInput)]),
      base::ToString(has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaVideoInput)])));

  if (requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] &&
      !has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaAudioInput)]) {
    SendLogMessage(
        base::StringPrintf("OnPermissionsCheckDone({request_id=%llu}) => "
                           "[WARNING: Audio permission denied by user/browser]",
                           request_id));
  }

  if (requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)] &&
      !has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaVideoInput)]) {
    SendLogMessage(
        base::StringPrintf("OnPermissionsCheckDone({request_id=%llu}) => "
                           "[WARNING: Video permission denied by user/browser]",
                           request_id));
  }

  // The video-capture subsystem currently does not support group IDs.
  // If video input devices are requested, also request audio input devices in
  // order to be able to use an heuristic that guesses group IDs for video
  // devices by finding matches in audio input devices.
  // TODO(crbug.com/41263713): Remove |internal_requested_types| and use
  // |requested_types| directly when video capture supports group IDs.
  BoolDeviceTypes internal_requested_types;
  internal_requested_types[static_cast<size_t>(
      MediaDeviceType::kMediaAudioInput)] =
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] ||
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)];
  internal_requested_types[static_cast<size_t>(
      MediaDeviceType::kMediaVideoInput)] =
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)];
  internal_requested_types[static_cast<size_t>(
      MediaDeviceType::kMediaAudioOutput)] =
      requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)];

  EnumerateAndRankDevices(
      render_frame_host_id, internal_requested_types,
      base::BindOnce(&MediaDevicesManager::OnDevicesEnumeratedAndRanked,
                     weak_factory_.GetWeakPtr(), request_id,
                     render_frame_host_id, requested_types,
                     request_video_input_capabilities,
                     request_audio_input_capabilities, std::move(callback),
                     std::move(salt_and_origin), has_permissions),
      request_id);
}

void MediaDevicesManager::OnDevicesEnumeratedAndRanked(
    uint64_t request_id,
    GlobalRenderFrameHostId render_frame_host_id,
    const MediaDevicesManager::BoolDeviceTypes& requested_types,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    const MediaDevicesManager::BoolDeviceTypes& has_permissions,
    const MediaDeviceEnumeration& enumeration) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  SendLogMessage(
      base::StringPrintf("OnDevicesEnumeratedAndRanked({request_id=%llu}, "
                         "{has_audio_permission=%s}, "
                         "{has_video_permission=%s})",
                         request_id,
                         base::ToString(has_permissions[static_cast<size_t>(
                             MediaDeviceType::kMediaAudioInput)]),
                         base::ToString(has_permissions[static_cast<size_t>(
                             MediaDeviceType::kMediaVideoInput)])));

  if (requested_types[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] &&
      !has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaAudioInput)]) {
    SendLogMessage(
        base::StringPrintf("OnDevicesEnumeratedAndRanked({request_id=%llu}) => "
                           "[WARNING: Audio permission denied by user/browser]",
                           request_id));
  }

  if (requested_types[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)] &&
      !has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaVideoInput)]) {
    SendLogMessage(
        base::StringPrintf("OnDevicesEnumeratedAndRanked({request_id=%llu}) => "
                           "[WARNING: Video permission denied by user/browser]",
                           request_id));
  }

  const bool video_input_capabilities_requested =
      has_permissions[static_cast<size_t>(MediaDeviceType::kMediaVideoInput)] &&
      request_video_input_capabilities;
  const bool audio_input_capabilities_requested =
      has_permissions[static_cast<size_t>(MediaDeviceType::kMediaAudioInput)] &&
      request_audio_input_capabilities;

  std::vector<blink::WebMediaDeviceInfoArray> translation(
      static_cast<size_t>(MediaDeviceType::kNumMediaDeviceTypes));
  for (size_t i = 0;
       i < static_cast<size_t>(MediaDeviceType::kNumMediaDeviceTypes); ++i) {
    if (!requested_types[i])
      continue;

    for (const auto& device_info : enumeration[i]) {
      if (!has_permissions[i] && !translation[i].empty()) {
        break;
      }

      translation[i].push_back(TranslateMediaDeviceInfo(
          has_permissions[i], salt_and_origin, device_info));
    }
  }

  // Expose devices authorized by selectAudioOutput.
  if (!audio_device_origin_map_.empty() &&
      !has_permissions[static_cast<size_t>(
          MediaDeviceType::kMediaAudioInput)] &&
      translation[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]
              .size() == 1 &&
      translation[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)][0]
          .device_id.empty()) {
    translation[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]
        .clear();
    auto authorized_devices =
        audio_device_origin_map_.find(render_frame_host_id);
    if (authorized_devices != audio_device_origin_map_.end()) {
      for (const auto& enum_device : enumeration[static_cast<size_t>(
               MediaDeviceType::kMediaAudioOutput)]) {
        if (authorized_devices->second.contains(enum_device)) {
          translation[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]
              .push_back(TranslateMediaDeviceInfo(
                  /*has_permission=*/true, salt_and_origin, enum_device));
        }
      }
    }
  }

  GetAudioInputCapabilities(request_id, video_input_capabilities_requested,
                            audio_input_capabilities_requested,
                            std::move(callback), enumeration, translation);
}

void MediaDevicesManager::GetAudioInputCapabilities(
    uint64_t request_id,
    bool request_video_input_capabilities,
    bool request_audio_input_capabilities,
    EnumerateDevicesCallback callback,
    const MediaDeviceEnumeration& raw_enumeration_results,
    const std::vector<blink::WebMediaDeviceInfoArray>&
        hashed_enumeration_results) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);

  EnumerationState state;
  size_t state_id = next_enumeration_state_id_++;
  state.request_id = request_id;
  state.video_input_capabilities_requested = request_video_input_capabilities;
  state.audio_input_capabilities_requested = request_audio_input_capabilities;
  state.completion_cb = std::move(callback);
  state.raw_enumeration_results = raw_enumeration_results;
  state.hashed_enumeration_results = hashed_enumeration_results;
  state.num_pending_audio_input_capabilities =
      hashed_enumeration_results[static_cast<size_t>(
                                     MediaDeviceType::kMediaAudioInput)]
          .size();

  if (!state.audio_input_capabilities_requested ||
      state.num_pending_audio_input_capabilities == 0) {
    SendLogMessage(base::StringPrintf(
        "GetAudioInputCapabilities({request_id=%llu}, {bypassing=true}, "
        "{reason=%s})",
        request_id,
        !state.audio_input_capabilities_requested ? "not_requested"
                                                  : "no_devices_found"));
    FinalizeDevicesEnumerated(std::move(state));
    return;
  }

  enumeration_states_[state_id] = std::move(state);
  CHECK_EQ(raw_enumeration_results[static_cast<size_t>(
                                       MediaDeviceType::kMediaAudioInput)]
               .size(),
           hashed_enumeration_results[static_cast<size_t>(
                                          MediaDeviceType::kMediaAudioInput)]
               .size(),
           base::NotFatalUntil::M155);
  std::size_t num_audio_input_devices =
      raw_enumeration_results[static_cast<size_t>(
                                  MediaDeviceType::kMediaAudioInput)]
          .size();
  for (std::size_t i = 0; i < num_audio_input_devices; i++) {
    auto raw_device_info = raw_enumeration_results[static_cast<size_t>(
        MediaDeviceType::kMediaAudioInput)][i];
    auto hashed_device_info = hashed_enumeration_results[static_cast<size_t>(
        MediaDeviceType::kMediaAudioInput)][i];

    AudioInputDeviceCapabilitiesPtr capabilities =
        blink::mojom::AudioInputDeviceCapabilities::New();
    capabilities->device_id = hashed_device_info.device_id;
    capabilities->parameters =
        media::AudioParameters::UnavailableDeviceParams();
    enumeration_states_[state_id].audio_capabilities.push_back(
        std::move(capabilities));
    size_t capabilities_index =
        enumeration_states_[state_id].audio_capabilities.size() - 1;
    if (use_fake_devices_) {
      GetIOThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(&MediaDevicesManager::GotAudioInputCapabilities,
                         weak_factory_.GetWeakPtr(), state_id,
                         capabilities_index,
                         media::AudioParameters::UnavailableDeviceParams()));
    } else {
      audio_system_->GetInputStreamParameters(
          raw_device_info.device_id,
          base::BindOnce(&MediaDevicesManager::GotAudioInputCapabilities,
                         weak_factory_.GetWeakPtr(), state_id,
                         capabilities_index));
    }
  }
}

void MediaDevicesManager::GotAudioInputCapabilities(
    size_t state_id,
    size_t capabilities_index,
    const std::optional<media::AudioParameters>& parameters) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(enumeration_states_.contains(state_id), base::NotFatalUntil::M155);

  auto& enumeration_state = enumeration_states_[state_id];
  CHECK_GT(enumeration_state.num_pending_audio_input_capabilities, 0,
           base::NotFatalUntil::M155);

  AudioInputDeviceCapabilitiesPtr& capabilities =
      enumeration_state.audio_capabilities[capabilities_index];

  const std::string& device_label =
      enumeration_state
          .hashed_enumeration_results[static_cast<size_t>(
              MediaDeviceType::kMediaAudioInput)][capabilities_index]
          .label;

  if (parameters) {
    SendLogMessage(base::StringPrintf(
        "GotAudioInputCapabilities({request_id=%llu}, "
        "{label=[%s]}, {is_valid=%s}, {sample_rate=%d}, {channels=%d}, "
        "{latency_ms=%lld})",
        enumeration_state.request_id, device_label.c_str(),
        parameters->IsValid() ? "true" : "false", parameters->sample_rate(),
        parameters->channels(),
        parameters->GetBufferDuration().InMilliseconds()));
    capabilities->parameters = *parameters;
    // Data from the |parameters| field is duplicated in the |channels|,
    // |sample_rate| and |latency| fields due to the lack of availability
    // of the media::AudioParameters native mojo mapping in blink.
    // TODO(crbug.com/40550966): Remove redundant fields when |parameters|
    // is accessible from Blink.
    capabilities->is_valid = parameters->IsValid();
    capabilities->channels = parameters->channels();
    capabilities->sample_rate = parameters->sample_rate();
    capabilities->latency = parameters->GetBufferDuration();
  }
  CHECK(capabilities->parameters.IsValid(), base::NotFatalUntil::M155);

  if (--enumeration_state.num_pending_audio_input_capabilities == 0) {
    FinalizeDevicesEnumerated(std::move(enumeration_state));
    enumeration_states_.erase(state_id);
  }
}

void MediaDevicesManager::FinalizeDevicesEnumerated(
    EnumerationState enumeration_state) {
  SendLogMessage(base::StringPrintf(
      "FinalizeDevicesEnumerated({request_id=%llu}, {audio_count=%zu}, "
      "{video_count=%zu})",
      enumeration_state.request_id,
      enumeration_state
          .hashed_enumeration_results[static_cast<size_t>(
              MediaDeviceType::kMediaAudioInput)]
          .size(),
      enumeration_state
          .hashed_enumeration_results[static_cast<size_t>(
              MediaDeviceType::kMediaVideoInput)]
          .size()));
  std::move(enumeration_state.completion_cb)
      .Run(std::move(enumeration_state.hashed_enumeration_results),
           enumeration_state.video_input_capabilities_requested
               ? ComputeVideoInputCapabilities(
                     enumeration_state
                         .raw_enumeration_results[static_cast<size_t>(
                             MediaDeviceType::kMediaVideoInput)],
                     enumeration_state
                         .hashed_enumeration_results[static_cast<size_t>(
                             MediaDeviceType::kMediaVideoInput)])
               : std::vector<VideoInputDeviceCapabilitiesPtr>(),
           std::move(enumeration_state.audio_capabilities));
}

std::vector<VideoInputDeviceCapabilitiesPtr>
MediaDevicesManager::ComputeVideoInputCapabilities(
    const blink::WebMediaDeviceInfoArray& raw_device_infos,
    const blink::WebMediaDeviceInfoArray& translated_device_infos) {
  CHECK_EQ(raw_device_infos.size(), translated_device_infos.size(),
           base::NotFatalUntil::M155);
  std::vector<VideoInputDeviceCapabilitiesPtr> video_input_capabilities;
  for (size_t i = 0; i < raw_device_infos.size(); ++i) {
    VideoInputDeviceCapabilitiesPtr capabilities =
        blink::mojom::VideoInputDeviceCapabilities::New();
    capabilities->device_id = translated_device_infos[i].device_id;
    capabilities->formats = GetVideoInputFormats(raw_device_infos[i].device_id,
                                                 false /* try_in_use_first */);
    capabilities->facing_mode = translated_device_infos[i].video_facing;
    if (translated_device_infos[i].availability) {
      capabilities->availability =
          static_cast<media::mojom::CameraAvailability>(
              *translated_device_infos[i].availability);
    }
    video_input_capabilities.push_back(std::move(capabilities));
  }
  return video_input_capabilities;
}

void MediaDevicesManager::EnumerateSystemDevices(uint64_t request_id,
                                                 MediaDeviceType type) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);
  CacheInfo& cache_info = cache_infos_[static_cast<size_t>(type)];
  if (cache_info.is_update_ongoing()) {
    SendLogMessage(base::StringPrintf(
        "EnumerateSystemDevices({request_id=%llu}, {type=%s}) "
        "=> [Joining ongoing scan]",
        request_id, DeviceTypeToString(type)));
    return;
  }
  SendLogMessage(
      base::StringPrintf("EnumerateSystemDevices({request_id=%llu}, {type=%s})",
                         request_id, DeviceTypeToString(type)));

  cache_info.UpdateStarted();
  switch (type) {
    case MediaDeviceType::kMediaAudioInput:
      EnumerateAudioDevices(request_id, true /* is_input */);
      break;
    case MediaDeviceType::kMediaVideoInput:
      ReportVideoEnumerationStart();
      video_capture_manager_->EnumerateDevices(
          base::BindOnce(&MediaDevicesManager::VideoInputDevicesEnumerated,
                         weak_factory_.GetWeakPtr(), request_id));
      break;
    case MediaDeviceType::kMediaAudioOutput:
      EnumerateAudioDevices(request_id, false /* is_input */);
      break;
    case MediaDeviceType::kNumMediaDeviceTypes:
      NOTREACHED();
  }
}

void MediaDevicesManager::EnumerateAudioDevices(uint64_t request_id,
                                                bool is_input) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  MediaDeviceType type = is_input ? MediaDeviceType::kMediaAudioInput
                                  : MediaDeviceType::kMediaAudioOutput;
  if (use_fake_devices_) {
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(&MediaDevicesManager::DevicesEnumerated,
                                  weak_factory_.GetWeakPtr(), request_id, type,
                                  GetFakeAudioDevices(is_input)));
    return;
  }

  audio_system_->GetDeviceDescriptions(
      is_input, base::BindOnce(&MediaDevicesManager::AudioDevicesEnumerated,
                               weak_factory_.GetWeakPtr(), request_id, type));
}

void MediaDevicesManager::VideoInputDevicesEnumerated(
    uint64_t request_id,
    DeviceEnumerationResult result_code,
    const media::VideoCaptureDeviceDescriptors& descriptors) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  ReportVideoEnumerationResult(result_code);

  if (result_code != DeviceEnumerationResult::kSuccess) {
    std::string log_message = base::StringPrintf(
        "VideoInputDevicesEnumerated({request_id=%llu}) got error %d",
        request_id, static_cast<int>(result_code));
    // Log to both WebRTC logs (for feedback reports) and text logs for
    // manually-collected chrome logs at customers.
    SendLogMessage(log_message);
    VLOG(1) << log_message;
    // TODO(crbug.com/40221155): Propagate this as an error response to the
    // page and expose in the JS API.
  }
  blink::WebMediaDeviceInfoArray snapshot;
  for (const auto& descriptor : descriptors) {
    snapshot.emplace_back(descriptor);
  }
  DevicesEnumerated(request_id, MediaDeviceType::kMediaVideoInput, snapshot);
}

void MediaDevicesManager::AudioDevicesEnumerated(
    uint64_t request_id,
    MediaDeviceType type,
    media::AudioDeviceDescriptions device_descriptions) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);

  blink::WebMediaDeviceInfoArray snapshot;
  for (const media::AudioDeviceDescription& description : device_descriptions) {
    snapshot.emplace_back(
        description.unique_id, description.device_name, description.group_id,
        media::VideoCaptureControlSupport(), blink::mojom::FacingMode::kNone);
  }
  DevicesEnumerated(request_id, type, snapshot);
}

void MediaDevicesManager::DevicesEnumerated(
    uint64_t request_id,
    MediaDeviceType type,
    const blink::WebMediaDeviceInfoArray& snapshot) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);
  UpdateSnapshot(type, snapshot);
  cache_infos_[static_cast<size_t>(type)].UpdateCompleted();
  cache_is_populated_[static_cast<size_t>(type)] = true;
  SendLogMessage(GetDevicesEnumeratedLogString(request_id, type, snapshot));

  if (cache_policies_[static_cast<size_t>(type)] == CachePolicy::NO_CACHE) {
    for (auto& request : client_requests_) {
      request.has_seen_result_for_request[static_cast<size_t>(type)] = true;
    }
  }

  if (base::FeatureList::IsEnabled(
          kEnumerateDevicesAlwaysProcessRequestsOnDevicesEnumerated) ||
      cache_infos_[static_cast<size_t>(type)].IsLastUpdateValid()) {
    ProcessClientRequests();
  }

  // Note that IsLastUpdateValid is always true when policy is NO_CACHE.
  if (!cache_infos_[static_cast<size_t>(type)].IsLastUpdateValid()) {
    EnumerateSystemDevices(request_id, type);
  }
}

void MediaDevicesManager::UpdateSnapshot(
    MediaDeviceType type,
    const blink::WebMediaDeviceInfoArray& new_snapshot,
    bool use_group_id) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);

  bool need_update_device_change_subscribers = false;
  bool current_snapshot_changed = false;
  blink::WebMediaDeviceInfoArray& old_snapshot =
      current_snapshot_[static_cast<size_t>(type)];

  if (type == MediaDeviceType::kMediaAudioInput ||
      type == MediaDeviceType::kMediaVideoInput) {
    MaybeStopRemovedInputDevices(type, new_snapshot);
  }

  // Update the cached snapshot and send notifications only if the device list
  // has changed.
  if (!std::ranges::equal(new_snapshot, old_snapshot,
                          use_group_id ? EqualDeviceIncludingGroupID
                                       : EqualDeviceExcludingGroupID)) {
    // Prevent sending notifications until group IDs are updated using
    // a heuristic in ProcessClientRequests().
    // TODO(crbug.com/41263713): Remove |is_video_with_group_ids| and the
    // corresponding checks when the video-capture subsystem supports
    // group IDs.
    bool is_video_with_good_group_ids =
        type == MediaDeviceType::kMediaVideoInput &&
        (new_snapshot.size() == 0 || !new_snapshot[0].group_id.empty());
    if (type == MediaDeviceType::kMediaAudioInput ||
        is_video_with_good_group_ids) {
      ui_input_device_change_cb_.Run(type, new_snapshot);
    }

    // Do not notify device-change subscribers after the first enumeration
    // result, since it is not due to an actual device change.
    need_update_device_change_subscribers =
        cache_is_populated_[static_cast<size_t>(type)] &&
        (old_snapshot.size() != 0 || new_snapshot.size() != 0) &&
        (type != MediaDeviceType::kMediaVideoInput ||
         is_video_with_good_group_ids);
    current_snapshot_[static_cast<size_t>(type)] = new_snapshot;
    current_snapshot_changed = true;
  }

  if (IsRelaxedCacheFeatureEnabled() && !use_group_id) {
    // `use_group_id` is true only to update group IDs for video devices before
    // potentially sending a result to clients, and false when it is the result
    // of an enumeration potentially triggered by an invalidation. Update the
    // spurious invalidation counts only in the latter case.
    if (!current_snapshot_changed) {
      cache_infos_[static_cast<size_t>(type)].RecordSpuriousInvalidation();
    } else {
      cache_infos_[static_cast<size_t>(type)].ResetSpuriousInvalidations();
    }
  }

  // Generate salts for each subscriber even if the device list hasn't changed,
  // as we may need to notify them anyway.
  for (const auto& subscription : subscriptions_) {
    const SubscriptionRequest& request = subscription.second;
    if (request.subscribe_types[static_cast<size_t>(type)]) {
      GetUIThreadTaskRunner({})->PostTask(
          FROM_HERE,
          base::BindOnce(
              get_salt_and_origin_cb_, request.render_frame_host_id,
              base::BindPostTaskToCurrentDefault(base::BindOnce(
                  &MediaDevicesManager::OnSaltAndOriginForSubscription,
                  weak_factory_.GetWeakPtr(), subscription.first,
                  request.render_frame_host_id, type, new_snapshot,
                  need_update_device_change_subscribers))));
    }
  }
}

void MediaDevicesManager::ProcessClientRequests() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  // Populate the group ID field for video devices using a heuristic that looks
  // for device coincidences with audio input devices.
  // TODO(crbug.com/41263713): Remove this once the video-capture subsystem
  // supports group IDs.
  if (cache_is_populated_[static_cast<size_t>(
          MediaDeviceType::kMediaVideoInput)]) {
    blink::WebMediaDeviceInfoArray video_devices =
        current_snapshot_[static_cast<size_t>(
            MediaDeviceType::kMediaVideoInput)];
    for (auto& video_device_info : video_devices) {
      video_device_info.group_id =
          GuessVideoGroupID(current_snapshot_[static_cast<size_t>(
                                MediaDeviceType::kMediaAudioInput)],
                            video_device_info);
    }
    UpdateSnapshot(MediaDeviceType::kMediaVideoInput, video_devices,
                   /*use_group_id=*/true);
  }

  std::erase_if(client_requests_, [this](EnumerationRequest& request) {
    if (IsEnumerationRequestReady(request)) {
      std::move(request.callback).Run(current_snapshot_);
      return true;
    }
    return false;
  });
}

bool MediaDevicesManager::IsEnumerationRequestReady(
    const EnumerationRequest& request_info) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  for (size_t i = 0;
       i < static_cast<size_t>(MediaDeviceType::kNumMediaDeviceTypes); ++i) {
    if (!request_info.requested[i]) {
      continue;
    }
    switch (cache_policies_[i]) {
      case CachePolicy::SYSTEM_MONITOR:
        if (!cache_is_populated_[i]) {
          return false;
        }
        break;
      case CachePolicy::NO_CACHE:
        if (!request_info.has_seen_result_for_request[i]) {
          return false;
        }
        break;
      default:
        NOTREACHED();
    }
  }
  return true;
}

void MediaDevicesManager::HandleDevicesChanged(MediaDeviceType type) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);

  uint64_t request_id = ++next_enumeration_request_id_;

  if (!cache_infos_[static_cast<size_t>(type)].is_update_ongoing()) {
    SendLogMessage(
        base::StringPrintf("HandleDevicesChanged({type=%s}, {request_id=%llu})",
                           DeviceTypeToString(type), request_id));
  }
  InvalidateCache(type);
  if (!IsRelaxedCacheFeatureEnabled() ||
      cache_infos_[static_cast<size_t>(type)].NeedsUpdateUponInvalidation()) {
    EnumerateSystemDevices(request_id, type);
  }
}

void MediaDevicesManager::InvalidateCache(MediaDeviceType type) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);
  cache_infos_[static_cast<size_t>(type)].InvalidateCache();
}

void MediaDevicesManager::MaybeStopRemovedInputDevices(
    MediaDeviceType type,
    const blink::WebMediaDeviceInfoArray& new_snapshot) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(type == MediaDeviceType::kMediaAudioInput ||
            type == MediaDeviceType::kMediaVideoInput,
        base::NotFatalUntil::M155);

  std::vector<blink::WebMediaDeviceInfo> removed_audio_devices;
  for (const auto& old_device_info :
       current_snapshot_[static_cast<size_t>(type)]) {
    // If a device was removed, notify the MediaStreamManager to stop all
    // streams using that device.
    if (!std::ranges::contains(new_snapshot, old_device_info.device_id,
                               &blink::WebMediaDeviceInfo::device_id)) {
      stop_removed_input_device_cb_.Run(type, old_device_info);

      if (type == MediaDeviceType::kMediaAudioInput) {
        removed_audio_devices.push_back(old_device_info);
      }
    }
  }

  // "default" and "communications" audio devices that have been removed,
  // require an extra notification. In fact, such audio devices have associated
  // virtual audio devices in the snapshot with the special "default" or
  // "communications" IDs. The code below implements an heuristic, such that to
  // identify if an audio device was default, it checks whether the old
  // snapshot contained an audio device with the same group ID and device ID
  // matching either "default" or "communications".
  // NOTE: ChromeOS is able to seamlessly redirect streams to the new default
  // device, hence the event should not be triggered.
#if !BUILDFLAG(IS_CHROMEOS)
  for (const auto& removed_audio_device : removed_audio_devices) {
    for (const auto& old_device_info :
         current_snapshot_[static_cast<size_t>(type)]) {
      if (removed_audio_device.group_id == old_device_info.group_id &&
          (old_device_info.device_id ==
               media::AudioDeviceDescription::kDefaultDeviceId ||
           old_device_info.device_id ==
               media::AudioDeviceDescription::kCommunicationsDeviceId)) {
        stop_removed_input_device_cb_.Run(type, old_device_info);
      }
    }
  }
#endif  // !BUILDFLAG(IS_CHROMEOS)
}

void MediaDevicesManager::OnSaltAndOriginForSubscription(
    uint32_t subscription_id,
    GlobalRenderFrameHostId render_frame_host_id,
    MediaDeviceType type,
    const blink::WebMediaDeviceInfoArray& device_infos,
    bool devices_changed,
    const MediaDeviceSaltAndOrigin& salt_and_origin) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);

  auto it = subscriptions_.find(subscription_id);
  if (it == subscriptions_.end())
    return;
  SubscriptionRequest& request = it->second;

  // Continue to propagate a change notification if either the actual device
  // list has changed, or the device_id_salt has changed.
  bool salt_reset =
      request.last_seen_device_id_salt_ &&
      salt_and_origin.device_id_salt() != request.last_seen_device_id_salt_;
  if (IsRelaxedCacheFeatureEnabled() && salt_reset) {
    // Salt changes fire invalidations because the device IDs as seen by
    // clients change. This means that if we see a new salt it may be due
    // to a valid invalidation, so we reset the spurious count.
    cache_infos_[static_cast<size_t>(type)].ResetSpuriousInvalidations();
  }

  if (devices_changed || salt_reset) {
    MediaDevicesManager::CheckPermissionForDeviceChange(
        subscription_id, render_frame_host_id, type, device_infos,
        salt_and_origin);
  }
  request.last_seen_device_id_salt_ = salt_and_origin.device_id_salt();
}

void MediaDevicesManager::CheckPermissionForDeviceChange(
    uint32_t subscription_id,
    GlobalRenderFrameHostId render_frame_host_id,
    MediaDeviceType type,
    const blink::WebMediaDeviceInfoArray& device_infos,
    const MediaDeviceSaltAndOrigin& salt_and_origin) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  permission_checker_->CheckPermission(
      type, render_frame_host_id,
      base::BindOnce(&MediaDevicesManager::OnCheckedPermissionForDeviceChange,
                     weak_factory_.GetWeakPtr(), subscription_id,
                     render_frame_host_id, type, device_infos,
                     salt_and_origin));
}

void MediaDevicesManager::OnCheckedPermissionForDeviceChange(
    uint32_t subscription_id,
    GlobalRenderFrameHostId render_frame_host_id,
    MediaDeviceType type,
    const blink::WebMediaDeviceInfoArray& device_infos,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    bool has_permission) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  MediaDevicesManager::BoolDeviceTypes requested_types;
  requested_types[static_cast<size_t>(type)] = true;
  MediaDeviceEnumeration enumeration;
  enumeration[static_cast<size_t>(type)] = device_infos;

  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&RankDevices, render_frame_host_id, requested_types,
                     base::BindPostTaskToCurrentDefault(base::BindOnce(
                         &MediaDevicesManager::NotifyDeviceChange,
                         weak_factory_.GetWeakPtr(), subscription_id, type,
                         salt_and_origin, has_permission)),
                     enumeration));
}

void MediaDevicesManager::NotifyDeviceChange(
    uint32_t subscription_id,
    MediaDeviceType type,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    bool has_permission,
    const MediaDeviceEnumeration& enumeration) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(blink::IsValidMediaDeviceType(type), base::NotFatalUntil::M155);
  auto it = subscriptions_.find(subscription_id);
  if (it == subscriptions_.end())
    return;
  SendLogMessage(
      base::StringPrintf("NotifyDeviceChange({subscription_id=%u}, {type=%s}",
                         subscription_id, DeviceTypeToString(type)));

  const SubscriptionRequest& request = it->second;
  request.listener_->OnDevicesChanged(
      type,
      TranslateMediaDeviceInfoArray(has_permission, salt_and_origin,
                                    enumeration[static_cast<size_t>(type)]));
}

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
void MediaDevicesManager::RegisterVideoCaptureDevicesChangedObserver() {
  CHECK(!video_capture_service_device_changed_observer_);
  if (base::FeatureList::IsEnabled(
          features::kRunVideoCaptureServiceInBrowserProcess)) {
    // Do not create a mojo connection when the video capture service is running
    // in the browser process as the device monitor will send device change
    // notifications directly to the system monitor in the browser process.
    return;
  }
  // base::Unretained(this) is safe here because |this| owns
  // |video_capture_service_device_changed_observer_|.
  video_capture_service_device_changed_observer_ =
      std::make_unique<VideoCaptureDevicesChangedObserver>(
          /*invalidate_cache_cb=*/base::BindRepeating(
              &MediaDevicesManager::InvalidateCache, base::Unretained(this),
              MediaDeviceType::kMediaVideoInput),
          /*enumerate_system_devices_cb=*/
          base::BindRepeating(&MediaDevicesManager::HandleDevicesChanged,
                              base::Unretained(this),
                              MediaDeviceType::kMediaVideoInput),
          /*listener_cb=*/base::BindRepeating([] {
            if (auto* monitor = base::SystemMonitor::Get()) {
              monitor->ProcessDevicesChanged(
                  base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE);
            }
          }));
  video_capture_service_device_changed_observer_->EnsureConnectedToService();
}

void MediaDevicesManager::OnDisconnectVideoSourceProviderTimer() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  CHECK(video_capture_service_device_changed_observer_);
  if (is_video_capture_hosts_set_empty_ && subscriptions_.empty()) {
    SendLogMessage("It is time to disconnect video source provider interface.");
    video_capture_service_device_changed_observer_
        ->DisconnectVideoSourceProvider();
  }
}

void MediaDevicesManager::MaybeScheduleDisconnectVideoSourceProviderTimer() {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  if (!video_capture_service_device_changed_observer_) {
    return;
  }

  if (is_video_capture_hosts_set_empty_ && subscriptions_.empty()) {
    if (!disconnect_video_source_provider_timer_.IsRunning()) {
      SendLogMessage("Start disconnect video source provider timer.");
      disconnect_video_source_provider_timer_.Start(
          FROM_HERE, kReleaseVideoSourceProviderTimeout.Get(),
          base::BindOnce(
              &MediaDevicesManager::OnDisconnectVideoSourceProviderTimer,
              base::Unretained(this)));
    }
  } else {
    if (disconnect_video_source_provider_timer_.IsRunning()) {
      SendLogMessage(
          "Disconnect video source provider timer is running, stop it.");
      disconnect_video_source_provider_timer_.Stop();
    }
  }
}

void MediaDevicesManager::UpdateVideoCaptureHostsEmptyState(bool empty) {
  CHECK_CURRENTLY_ON(BrowserThread::IO, base::NotFatalUntil::M155);
  is_video_capture_hosts_set_empty_ = empty;
  MaybeScheduleDisconnectVideoSourceProviderTimer();
}
#endif

MediaDevicesManager::EnumerationState::EnumerationState() = default;
MediaDevicesManager::EnumerationState::EnumerationState(
    EnumerationState&& other) = default;
MediaDevicesManager::EnumerationState::~EnumerationState() = default;
MediaDevicesManager::EnumerationState& MediaDevicesManager::EnumerationState::
operator=(EnumerationState&& other) = default;

}  // namespace content
