blob: 0785d1d380dbabe8175e1504f8cf4ae7d9bd2f00 [file] [log] [blame]
// 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/containers/contains.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/location.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(ReleaseVideoSourceProviderIfNotInUse,
base::FEATURE_DISABLED_BY_DEFAULT);
const base::FeatureParam<base::TimeDelta> kReleaseVideoSourceProviderTimeout{
&kReleaseVideoSourceProviderIfNotInUse,
"release_video_source_provider_timeout", base::Seconds(60)};
#endif
BASE_FEATURE(EnumerateDevicesRelaxedCache,
#if BUILDFLAG(IS_WIN)
base::FEATURE_ENABLED_BY_DEFAULT
#else
base::FEATURE_DISABLED_BY_DEFAULT
#endif
);
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(
MediaDeviceType device_type,
const blink::WebMediaDeviceInfoArray& device_infos) {
std::string str = base::StringPrintf("DevicesEnumerated({type=%s}, ",
DeviceTypeToString(device_type));
base::StringAppendF(&str, "{labels=[");
for (const auto& device_info : device_infos)
base::StringAppendF(&str, "%s, ", device_info.label.c_str());
if (!str.empty()) {
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) {
DCHECK(LabelHasUSBModel(label));
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 BindDeviceNotifierFromUIThread(
mojo::PendingReceiver<audio::mojom::DeviceNotifier> receiver) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
GetAudioService().BindDeviceNotifier(std::move(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) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
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() {
DCHECK(thread_checker_.CalledOnValidThread());
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 {
DCHECK(thread_checker_.CalledOnValidThread());
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() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(!is_update_ongoing_);
seq_last_update_ = NewEventSequence();
is_update_ongoing_ = true;
}
void UpdateCompleted() {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(is_update_ongoing_);
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 {
DCHECK(thread_checker_.CalledOnValidThread());
return is_update_ongoing_;
}
// This should be called whenever a legitimate reason to allow a future
// low-level device enumeration is detected.
void ResetSpuriousInvalidations() {
DCHECK(thread_checker_.CalledOnValidThread());
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() {
DCHECK(thread_checker_.CalledOnValidThread());
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() {
DCHECK(thread_checker_.CalledOnValidThread());
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 disconnect_cb)
: disconnect_cb_(std::move(disconnect_cb)) {
ConnectToService();
}
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() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!mojo_audio_device_notifier_);
DCHECK(!receiver_.is_bound());
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
&BindDeviceNotifierFromUIThread,
mojo_audio_device_notifier_.BindNewPipeAndPassReceiver()));
mojo_audio_device_notifier_.set_disconnect_handler(base::BindOnce(
&MediaDevicesManager::AudioServiceDeviceListener::OnConnectionError,
weak_factory_.GetWeakPtr()));
mojo_audio_device_notifier_->RegisterListener(
receiver_.BindNewPipeAndPassRemote());
}
void OnConnectionError() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(disconnect_cb_);
mojo_audio_device_notifier_.reset();
receiver_.reset();
disconnect_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()));
}
// |disconnect_cb_| is a callback used to invalidate the cache and do a
// fresh enumeration to avoid losing out on the changes that might happen
// when the audio service is not active.
const base::RepeatingClosure disconnect_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)) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(audio_system_);
DCHECK(video_capture_manager_.get());
DCHECK(!stop_removed_input_device_cb_.is_null());
DCHECK(!ui_input_device_change_cb_.is_null());
// 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());
SendLogMessage("MediaDevicesManager()");
cache_policies_.fill(CachePolicy::NO_CACHE);
}
MediaDevicesManager::~MediaDevicesManager() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
#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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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;
}
StartMonitoring(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;
DoEnumerateDevices(static_cast<MediaDeviceType>(i));
}
}
if (all_results_cached) {
ProcessClientRequests();
}
}
void MediaDevicesManager::EnumerateAndRankDevices(
GlobalRenderFrameHostId render_frame_host_id,
const BoolDeviceTypes& requested_types,
EnumerationCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
EnumerateDevices(
requested_types,
base::BindPostTask(
GetUIThreadTaskRunner(),
base::BindOnce(
&RankDevices, render_frame_host_id, requested_types,
base::BindPostTaskToCurrentDefault(std::move(callback)))));
}
void MediaDevicesManager::EnumerateAndRankDevices(
GlobalRenderFrameHostId render_frame_host_id,
const BoolDeviceTypes& requested_types,
bool request_video_input_capabilities,
bool request_audio_input_capabilities,
EnumerateDevicesCallback callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(request_video_input_capabilities &&
requested_types[static_cast<size_t>(
MediaDeviceType::kMediaVideoInput)] ||
!request_video_input_capabilities);
DCHECK(request_audio_input_capabilities &&
requested_types[static_cast<size_t>(
MediaDeviceType::kMediaAudioInput)] ||
!request_audio_input_capabilities);
SendLogMessage(base::StringPrintf(
"EnumerateDevices({render_process_id=%d}, {render_frame_id=%d}, "
"{request_audio=%s}, {request_video=%s})",
render_frame_host_id.child_id, render_frame_host_id.frame_routing_id,
base::ToString(request_audio_input_capabilities),
base::ToString(request_video_input_capabilities)));
GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(
get_salt_and_origin_cb_, render_frame_host_id,
base::BindPostTaskToCurrentDefault(base::BindOnce(
&MediaDevicesManager::CheckPermissionsForEnumerateDevices,
weak_factory_.GetWeakPtr(), 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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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 base::Contains(authorized_devices->second, device_info);
}
void MediaDevicesManager::GetSpeakerSelectionAndMicrophonePermissionState(
GlobalRenderFrameHostId render_frame_host_id,
base::OnceCallback<void(PermissionDeniedState, bool)> callback) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
permission_checker_->GetSpeakerSelectionAndMicrophonePermissionState(
render_frame_host_id.child_id, render_frame_host_id.frame_routing_id,
std::move(callback));
}
uint32_t MediaDevicesManager::SubscribeDeviceChangeNotifications(
GlobalRenderFrameHostId render_frame_host_id,
const BoolDeviceTypes& subscribe_types,
mojo::PendingRemote<blink::mojom::MediaDevicesListener> listener) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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;
}
StartMonitoring(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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(blink::IsValidMediaDeviceType(type));
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) {
cache_infos_[static_cast<size_t>(type)].InvalidateCache();
DoEnumerateDevices(type);
}
}
void MediaDevicesManager::StartMonitoring() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Start monitoring all device types.
StartMonitoring(DeviceStartMonitoringMode::kStartAudioAndVideo);
}
void MediaDevicesManager::StartMonitoring(
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)) {
DCHECK(!audio_service_device_listener_);
// base::Unretained(this) is safe here because |this| owns
// |audio_service_device_listener_|.
audio_service_device_listener_ =
std::make_unique<AudioServiceDeviceListener>(
/*disconnect_cb=*/base::BindRepeating(
&MediaDevicesManager::HandleDevicesChanged,
base::Unretained(this), MediaDeviceType::kMediaAudioInput));
}
#endif
SendLogMessage("StartMonitoring()");
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);
}
if (start_audio_device_monitoring && !monitoring_started_for_audio_) {
SetCachePolicy(MediaDeviceType::kMediaAudioInput,
CachePolicy::SYSTEM_MONITOR);
SetCachePolicy(MediaDeviceType::kMediaAudioOutput,
CachePolicy::SYSTEM_MONITOR);
}
}
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() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// Stop monitoring for the all device types.
StopMonitoring(DeviceStopMonitoringMode::kStopAudioAndVideo);
}
void MediaDevicesManager::StopMonitoring(
DeviceStopMonitoringMode device_monitoring_mode) {
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([this=%p])", this));
}
}
void MediaDevicesManager::OnDevicesChanged(
base::SystemMonitor::DeviceType device_type) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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 {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
dispatcher_hosts_.Add(std::move(dispatcher_host), std::move(receiver));
}
void MediaDevicesManager::SetPermissionChecker(
std::unique_ptr<MediaDevicesPermissionChecker> permission_checker) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(permission_checker);
permission_checker_ = std::move(permission_checker);
}
void MediaDevicesManager::CheckPermissionsForEnumerateDevices(
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
permission_checker_->CheckPermissions(
requested_types, render_frame_host_id.child_id,
render_frame_host_id.frame_routing_id,
base::BindOnce(&MediaDevicesManager::OnPermissionsCheckDone,
weak_factory_.GetWeakPtr(), 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(
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// 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::OnDevicesEnumerated,
weak_factory_.GetWeakPtr(), render_frame_host_id,
requested_types, request_video_input_capabilities,
request_audio_input_capabilities, std::move(callback),
std::move(salt_and_origin), has_permissions));
}
void MediaDevicesManager::OnDevicesEnumerated(
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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 (base::Contains(authorized_devices->second, enum_device)) {
translation[static_cast<size_t>(MediaDeviceType::kMediaAudioOutput)]
.push_back(TranslateMediaDeviceInfo(
/*has_permission=*/true, salt_and_origin, enum_device));
}
}
}
}
GetAudioInputCapabilities(video_input_capabilities_requested,
audio_input_capabilities_requested,
std::move(callback), enumeration, translation);
}
void MediaDevicesManager::GetAudioInputCapabilities(
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
EnumerationState state;
size_t state_id = next_enumeration_state_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) {
FinalizeDevicesEnumerated(std::move(state));
return;
}
enumeration_states_[state_id] = std::move(state);
DCHECK_EQ(raw_enumeration_results[static_cast<size_t>(
MediaDeviceType::kMediaAudioInput)]
.size(),
hashed_enumeration_results[static_cast<size_t>(
MediaDeviceType::kMediaAudioInput)]
.size());
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(base::Contains(enumeration_states_, state_id));
auto& enumeration_state = enumeration_states_[state_id];
DCHECK_GT(enumeration_state.num_pending_audio_input_capabilities, 0);
AudioInputDeviceCapabilitiesPtr& capabilities =
enumeration_state.audio_capabilities[capabilities_index];
if (parameters) {
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();
}
DCHECK(capabilities->parameters.IsValid());
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) {
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) {
DCHECK_EQ(raw_device_infos.size(), translated_device_infos.size());
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::DoEnumerateDevices(MediaDeviceType type) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(blink::IsValidMediaDeviceType(type));
CacheInfo& cache_info = cache_infos_[static_cast<size_t>(type)];
if (cache_info.is_update_ongoing()) {
return;
}
SendLogMessage(base::StringPrintf("DoEnumerateDevices({type=%s})",
DeviceTypeToString(type)));
cache_info.UpdateStarted();
switch (type) {
case MediaDeviceType::kMediaAudioInput:
EnumerateAudioDevices(true /* is_input */);
break;
case MediaDeviceType::kMediaVideoInput:
ReportVideoEnumerationStart();
video_capture_manager_->EnumerateDevices(
base::BindOnce(&MediaDevicesManager::VideoInputDevicesEnumerated,
weak_factory_.GetWeakPtr()));
break;
case MediaDeviceType::kMediaAudioOutput:
EnumerateAudioDevices(false /* is_input */);
break;
default:
NOTREACHED();
}
}
void MediaDevicesManager::EnumerateAudioDevices(bool is_input) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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(), type,
GetFakeAudioDevices(is_input)));
return;
}
audio_system_->GetDeviceDescriptions(
is_input, base::BindOnce(&MediaDevicesManager::AudioDevicesEnumerated,
weak_factory_.GetWeakPtr(), type));
}
void MediaDevicesManager::VideoInputDevicesEnumerated(
DeviceEnumerationResult result_code,
const media::VideoCaptureDeviceDescriptors& descriptors) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
ReportVideoEnumerationResult(result_code);
if (result_code != DeviceEnumerationResult::kSuccess) {
std::string log_message =
base::StringPrintf("VideoInputDevicesEnumerated got error %d",
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(MediaDeviceType::kMediaVideoInput, snapshot);
}
void MediaDevicesManager::AudioDevicesEnumerated(
MediaDeviceType type,
media::AudioDeviceDescriptions device_descriptions) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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(type, snapshot);
}
void MediaDevicesManager::DevicesEnumerated(
MediaDeviceType type,
const blink::WebMediaDeviceInfoArray& snapshot) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(blink::IsValidMediaDeviceType(type));
UpdateSnapshot(type, snapshot);
cache_infos_[static_cast<size_t>(type)].UpdateCompleted();
cache_is_populated_[static_cast<size_t>(type)] = true;
SendLogMessage(GetDevicesEnumeratedLogString(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;
}
}
// Note that IsLastUpdateValid is always true when policy is NO_CACHE.
if (cache_infos_[static_cast<size_t>(type)].IsLastUpdateValid()) {
ProcessClientRequests();
} else {
DoEnumerateDevices(type);
}
}
void MediaDevicesManager::UpdateSnapshot(
MediaDeviceType type,
const blink::WebMediaDeviceInfoArray& new_snapshot,
bool use_group_id) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(blink::IsValidMediaDeviceType(type));
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() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
// 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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(blink::IsValidMediaDeviceType(type));
if (!cache_infos_[static_cast<size_t>(type)].is_update_ongoing()) {
SendLogMessage(base::StringPrintf("HandleDevicesChanged({type=%s}",
DeviceTypeToString(type)));
}
cache_infos_[static_cast<size_t>(type)].InvalidateCache();
if (!IsRelaxedCacheFeatureEnabled() ||
cache_infos_[static_cast<size_t>(type)].NeedsUpdateUponInvalidation()) {
DoEnumerateDevices(type);
}
}
void MediaDevicesManager::MaybeStopRemovedInputDevices(
MediaDeviceType type,
const blink::WebMediaDeviceInfoArray& new_snapshot) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(type == MediaDeviceType::kMediaAudioInput ||
type == MediaDeviceType::kMediaVideoInput);
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 (!base::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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
permission_checker_->CheckPermission(
type, render_frame_host_id.child_id,
render_frame_host_id.frame_routing_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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
DCHECK(blink::IsValidMediaDeviceType(type));
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>(
/*disconnect_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() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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() {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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) {
DCHECK_CURRENTLY_ON(BrowserThread::IO);
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