blob: 2fa88c031a4d257b907d28b67810607c0309a912 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_MEDIA_PREFS_CAPTURE_DEVICE_RANKING_H_
#define CHROME_BROWSER_MEDIA_PREFS_CAPTURE_DEVICE_RANKING_H_
#include "chrome/browser/media/prefs/pref_names.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "media/audio/audio_device_description.h"
#include "media/capture/video/video_capture_device_info.h"
#include "third_party/blink/public/common/mediastream/media_devices.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"
#include <string>
namespace media_prefs::internal {
// Returns a stable device id given `device_info`. This isn't guaranteed
// to be unique, but it is stable across reboots and device plug/unplug.
std::string DeviceInfoToStableId(
const media::VideoCaptureDeviceInfo& device_info);
std::string DeviceInfoToStableId(
const media::AudioDeviceDescription& device_info);
std::string DeviceInfoToStableId(const blink::WebMediaDeviceInfo& device_info);
std::string DeviceInfoToStableId(const blink::MediaStreamDevice& device_info);
// Returns a unique id given `device_info`. This is unique for the current
// session, but isn't guaranteed to be stable through reboots or device
// plug/unplug.
std::string DeviceInfoToUniqueId(
const media::VideoCaptureDeviceInfo& device_info);
std::string DeviceInfoToUniqueId(
const media::AudioDeviceDescription& device_info);
std::string DeviceInfoToUniqueId(const blink::WebMediaDeviceInfo& device_info);
std::string DeviceInfoToUniqueId(const blink::MediaStreamDevice& device_info);
} // namespace media_prefs::internal
namespace {
using media_prefs::internal::DeviceInfoToStableId;
using media_prefs::internal::DeviceInfoToUniqueId;
// Returns the rank index for the passed `info`. If `info` doesn't have a
// rank then return size of `id_to_rank` because that will rank lower than
// any of the ranked devices.
template <typename T>
size_t GetRank(const T& info,
const base::flat_map<std::string, size_t>& id_to_rank) {
auto found_iter = id_to_rank.find(DeviceInfoToStableId(info));
if (found_iter != id_to_rank.end()) {
return found_iter->second;
}
return id_to_rank.size();
}
// Map device ids to a numeric ranking. Smaller values are more preferred.
template <typename Iterator>
base::flat_map<std::string, size_t> GetIdToRankMap(Iterator begin,
Iterator end) {
base::flat_map<std::string, size_t> id_to_rank;
for (auto iter = begin; iter < end; ++iter) {
id_to_rank[iter->GetString()] = iter - begin;
}
return id_to_rank;
}
template <typename T>
const base::Value::List& GetAndMaybeMigratePref(PrefService& prefs,
const std::string& pref_name,
std::vector<T>& device_infos);
// Reorders the passed `device_infos`, so that ranked devices are ordered at the
// beginning of the list. Devices don't have a ranking will retain their
// ordering, but will be shifted below ranked devices.
//
// This must be run on the UI thread as it reads from the PrefService.
template <typename T>
void PreferenceRankDeviceInfos(PrefService& prefs,
const std::string& pref_name,
std::vector<T>& device_infos) {
if (!prefs.FindPreference(pref_name)) {
LOG(WARNING) << "Can't rank device infos because " << pref_name
<< " isn't registered";
return;
}
const base::Value::List& ranking =
GetAndMaybeMigratePref(prefs, pref_name, device_infos);
const auto id_to_rank = GetIdToRankMap(ranking.begin(), ranking.end());
std::stable_sort(
device_infos.begin(), device_infos.end(),
[id_to_rank = std::move(id_to_rank)](const T& a, const T& b) {
return GetRank(a, id_to_rank) < GetRank(b, id_to_rank);
});
}
// Updates device ranking given a preferred device and the list of competing
// devices.
//
// The algorithm is as follows:
// 1. Identify the best ranked device among the current devices
// a. If a device isn't found in the ranking list, then it is appended
// 2. If the best ranked device is the preferred device, then exit
// 3. Else, move the preferred device to be just before the best ranked device
//
// The goal of the algorithm is to ensure that given the same set of devices we
// would get the same device ranking.
//
// Example case:
// Update 1 -
// The user selects the USB mic as preferred, and the other current
// devices are at the end of the list in the order enumerated by the OS.
// current ranking: []
// preferred_device_iter: 'USB Camera Mic',
// current_device_infos: 'bluetooth mic', 'builtin mic', 'USB Camera Mic'
//
// updated ranking: 'USB Camera Mic', 'bluetooth mic', 'builtin mic'
//
// Update 2 -
// The user disconnects the USB camera because they left their desk
// and they select the 'builtin mic' as preferred.
// current ranking: 'USB Camera Mic', 'builtin mic', 'bluetooth mic'
// preferred_device_iter: 'builtin mic'
// current_deivce_infos: 'bluetooth mic', 'builtin mic'
//
// updated ranking: 'USB Camera Mic', 'builtin mic', 'bluetooth mic'
//
// Note that the second update leaves the 'USB Camera Mic' as preferred over the
// other two mics because it wasn't present to be considered.
//
// This must be run on the UI thread as it read/writes to the PrefService.
template <typename T>
void UpdateDevicePreferenceRanking(
PrefService& prefs,
const std::string& pref_name,
const typename std::vector<T>::const_iterator preferred_device_iter,
const std::vector<T>& current_device_infos) {
CHECK(preferred_device_iter < current_device_infos.end());
auto preferred_device_stable_id =
DeviceInfoToStableId(*preferred_device_iter);
ScopedListPrefUpdate ranking(&prefs, pref_name);
const auto id_to_rank = GetIdToRankMap(ranking->begin(), ranking->end());
// Start `best_competing_rank` with a bigger number than is possible.
auto best_competing_rank = ranking->size();
for (const auto& device_info : current_device_infos) {
// Skip the preferred device because we're looking for the best ranked
// competing device.
if (DeviceInfoToUniqueId(device_info) ==
DeviceInfoToUniqueId(*preferred_device_iter)) {
continue;
}
auto rank = id_to_rank.find(DeviceInfoToStableId(device_info));
if (rank == id_to_rank.end()) {
// The device doesn't have a rank, so insert it at the end of the list.
ranking->Append(DeviceInfoToStableId(device_info));
continue;
}
if (rank->second < best_competing_rank) {
best_competing_rank = rank->second;
}
}
auto preferred_device_rank = id_to_rank.find(preferred_device_stable_id);
if (preferred_device_rank == id_to_rank.end()) {
// The preferred device didn't previously have a rank, so insert it before
// the best ranked competitor.
ranking->Insert(ranking->begin() + best_competing_rank,
base::Value{preferred_device_stable_id});
return;
}
// Only move the preferred device if there is a better ranked competitor.
if (best_competing_rank < preferred_device_rank->second) {
// Move the preferred device to be just before the best ranked competitor.
ranking->erase(ranking->begin() + preferred_device_rank->second);
ranking->Insert(ranking->begin() + best_competing_rank,
base::Value{preferred_device_stable_id});
}
}
// Get the value of the ranking pref.
//
// If the ranking pref is unset, it will be initialized from the default device
// pref. If the default device pref is unset or the device isn't in
// `device_infos`, then the ranking pref will remain uninitialized.
// `pref_name` is required to be in {kAudioInputUserPreferenceRanking,
// kVideoInputUserPreferenceRanking}.
//
// Example scenarios:
// For all scenarios the user has set dev2 as the default and updated the
// browser.
//
// Scenario A:
// 1) User connects [dev1, dev2]
// 2) User visits a website that calls enumerateDevices or getUserMedia
// 3) Device ranking pref is updated to [dev2, dev1]
// 4) All subsequent usages refer to the device ranking pref and ignore legacy
// default pref
//
// Scenario B:
// 1) User connects [dev1]
// 2) User visits a website that calls enumerateDevices or getUserMedia
// 3) Device ranking pref remains uninitialized because the default device
// isn't present
// 4) User connects [dev1, dev2]
// 5) User visits a website that calls enumerateDevices or getUserMedia
// 6) Device ranking pref is updated to [dev2, dev1]
// 7) All subsequent usages refer to the device ranking pref and
// ignore legacy default pref.
//
// Scenario C:
// 1) User connects [dev1]
// 2) User visits a website that calls enumerateDevices or getUserMedia
// 3) Device ranking pref remains uninitialized because the default device
// isn't present
// 4) User expresses preference for dev1 in permission bubble or
// Chrome settings
// 5) Device ranking pref is updated to [dev1]
// 6) All subsequent usages refer to the device ranking pref and ignore legacy
// default pref.
//
// TODO(crbug.com/311205211): Remove this special initialization logic once the
// default device pref is removed.
template <typename T>
const base::Value::List& GetAndMaybeMigratePref(PrefService& prefs,
const std::string& pref_name,
std::vector<T>& device_infos) {
const base::Value::List& ranking = prefs.GetList(pref_name);
if (!ranking.empty()) {
return ranking;
}
// Initialize the ranking pref with the legacy default device because it is
// unset.
std::string default_device_pref_name;
if (pref_name == kAudioInputUserPreferenceRanking) {
default_device_pref_name = prefs::kDefaultAudioCaptureDeviceDeprecated;
} else if (pref_name == kVideoInputUserPreferenceRanking) {
default_device_pref_name = prefs::kDefaultVideoCaptureDeviceDeprecated;
} else {
NOTREACHED();
}
if (!prefs.HasPrefPath(default_device_pref_name)) {
LOG(WARNING) << "Can't initialize the value of " << pref_name << " because "
<< default_device_pref_name << " isn't registered or is empty";
return ranking;
}
const auto& default_id = prefs.GetString(default_device_pref_name);
for (auto iter = device_infos.begin(); iter < device_infos.end(); ++iter) {
if (DeviceInfoToUniqueId(*iter) == default_id) {
UpdateDevicePreferenceRanking(prefs, pref_name, iter, device_infos);
return prefs.GetList(pref_name);
}
}
return ranking;
}
} // namespace
namespace media_prefs {
void RegisterUserPrefs(PrefRegistrySimple* registry);
template <typename T>
void PreferenceRankAudioDeviceInfos(PrefService& prefs,
std::vector<T>& device_infos) {
static_assert(std::is_same_v<media::AudioDeviceDescription, T> ||
std::is_same_v<blink::WebMediaDeviceInfo, T> ||
std::is_same_v<blink::MediaStreamDevice, T>);
PreferenceRankDeviceInfos(prefs, kAudioInputUserPreferenceRanking,
device_infos);
}
template <typename T>
void PreferenceRankVideoDeviceInfos(PrefService& prefs,
std::vector<T>& device_infos) {
static_assert(std::is_same_v<media::VideoCaptureDeviceInfo, T> ||
std::is_same_v<blink::WebMediaDeviceInfo, T> ||
std::is_same_v<blink::MediaStreamDevice, T>);
PreferenceRankDeviceInfos(prefs, kVideoInputUserPreferenceRanking,
device_infos);
}
template <typename T>
void UpdateAudioDevicePreferenceRanking(
PrefService& prefs,
const typename std::vector<T>::const_iterator preferred_device_iter,
const std::vector<T>& current_device_infos) {
static_assert(std::is_same_v<media::AudioDeviceDescription, T> ||
std::is_same_v<blink::WebMediaDeviceInfo, T> ||
std::is_same_v<blink::MediaStreamDevice, T>);
UpdateDevicePreferenceRanking(prefs, kAudioInputUserPreferenceRanking,
preferred_device_iter, current_device_infos);
}
template <typename T>
void UpdateVideoDevicePreferenceRanking(
PrefService& prefs,
const typename std::vector<T>::const_iterator preferred_device_iter,
const std::vector<T>& current_device_infos) {
static_assert(std::is_same_v<media::VideoCaptureDeviceInfo, T> ||
std::is_same_v<blink::WebMediaDeviceInfo, T> ||
std::is_same_v<blink::MediaStreamDevice, T>);
UpdateDevicePreferenceRanking(prefs, kVideoInputUserPreferenceRanking,
preferred_device_iter, current_device_infos);
}
} // namespace media_prefs
#endif // CHROME_BROWSER_MEDIA_PREFS_CAPTURE_DEVICE_RANKING_H_