| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // 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 <string> |
| |
| #include "base/command_line.h" |
| #include "base/location.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/task_runner_util.h" |
| #include "base/threading/thread_checker.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/media/media_stream_manager.h" |
| #include "content/browser/renderer_host/media/video_capture_manager.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "media/audio/audio_device_description.h" |
| #include "media/audio/audio_system.h" |
| #include "media/base/media_switches.h" |
| |
| #if defined(OS_MACOSX) |
| #include "base/bind_helpers.h" |
| #include "base/profiler/scoped_tracker.h" |
| #include "base/single_thread_task_runner.h" |
| #include "content/browser/browser_main_loop.h" |
| #include "media/device_monitors/device_monitor_mac.h" |
| #endif |
| |
| namespace content { |
| |
| namespace { |
| |
| // Private helper method to generate a string for the log message that lists the |
| // human readable names of |devices|. |
| std::string GetLogMessageString(MediaDeviceType device_type, |
| const MediaDeviceInfoArray& device_infos) { |
| std::string output_string = |
| base::StringPrintf("Getting devices of type %d:\n", device_type); |
| if (device_infos.empty()) |
| return output_string + "No devices found."; |
| for (const auto& device_info : device_infos) |
| output_string += " " + device_info.label + "\n"; |
| return output_string; |
| } |
| |
| MediaDeviceInfoArray GetFakeAudioDevices(bool is_input) { |
| MediaDeviceInfoArray 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; |
| } |
| |
| } // namespace |
| |
| struct MediaDevicesManager::EnumerationRequest { |
| EnumerationRequest(const BoolDeviceTypes& requested_types, |
| const EnumerationCallback& callback) |
| : callback(callback) { |
| requested = requested_types; |
| has_seen_result.fill(false); |
| } |
| |
| BoolDeviceTypes requested; |
| BoolDeviceTypes has_seen_result; |
| 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. |
| class MediaDevicesManager::CacheInfo { |
| public: |
| CacheInfo() |
| : current_event_sequence_(0), |
| seq_last_update_(0), |
| seq_last_invalidation_(0), |
| is_update_ongoing_(false) {} |
| |
| void InvalidateCache() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| seq_last_invalidation_ = NewEventSequence(); |
| } |
| |
| bool IsLastUpdateValid() const { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| return seq_last_update_ > seq_last_invalidation_ && !is_update_ongoing_; |
| } |
| |
| 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; |
| } |
| |
| bool is_update_ongoing() const { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| return is_update_ongoing_; |
| } |
| |
| private: |
| int64_t NewEventSequence() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| return ++current_event_sequence_; |
| } |
| |
| int64_t current_event_sequence_; |
| int64_t seq_last_update_; |
| int64_t seq_last_invalidation_; |
| bool is_update_ongoing_; |
| base::ThreadChecker thread_checker_; |
| }; |
| |
| MediaDevicesManager::MediaDevicesManager( |
| media::AudioSystem* audio_system, |
| const scoped_refptr<VideoCaptureManager>& video_capture_manager, |
| MediaStreamManager* media_stream_manager) |
| : use_fake_devices_(base::CommandLine::ForCurrentProcess()->HasSwitch( |
| switches::kUseFakeDeviceForMediaStream)), |
| audio_system_(audio_system), |
| video_capture_manager_(video_capture_manager), |
| media_stream_manager_(media_stream_manager), |
| cache_infos_(NUM_MEDIA_DEVICE_TYPES), |
| monitoring_started_(false), |
| weak_factory_(this) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(audio_system_); |
| DCHECK(video_capture_manager_.get()); |
| cache_policies_.fill(CachePolicy::NO_CACHE); |
| has_seen_result_.fill(false); |
| } |
| |
| MediaDevicesManager::~MediaDevicesManager() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| } |
| |
| void MediaDevicesManager::EnumerateDevices( |
| const BoolDeviceTypes& requested_types, |
| const EnumerationCallback& callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| StartMonitoring(); |
| |
| requests_.emplace_back(requested_types, callback); |
| bool all_results_cached = true; |
| for (size_t i = 0; i < NUM_MEDIA_DEVICE_TYPES; ++i) { |
| if (requested_types[i] && cache_policies_[i] == CachePolicy::NO_CACHE) { |
| all_results_cached = false; |
| DoEnumerateDevices(static_cast<MediaDeviceType>(i)); |
| } |
| } |
| |
| if (all_results_cached) |
| ProcessRequests(); |
| } |
| |
| void MediaDevicesManager::SubscribeDeviceChangeNotifications( |
| MediaDeviceType type, |
| MediaDeviceChangeSubscriber* subscriber) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| auto it = std::find(device_change_subscribers_[type].begin(), |
| device_change_subscribers_[type].end(), subscriber); |
| if (it == device_change_subscribers_[type].end()) |
| device_change_subscribers_[type].push_back(subscriber); |
| } |
| |
| void MediaDevicesManager::UnsubscribeDeviceChangeNotifications( |
| MediaDeviceType type, |
| MediaDeviceChangeSubscriber* subscriber) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| auto it = std::find(device_change_subscribers_[type].begin(), |
| device_change_subscribers_[type].end(), subscriber); |
| if (it != device_change_subscribers_[type].end()) |
| device_change_subscribers_[type].erase(it); |
| } |
| |
| void MediaDevicesManager::SetCachePolicy(MediaDeviceType type, |
| CachePolicy policy) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(IsValidMediaDeviceType(type)); |
| if (cache_policies_[type] == policy) |
| return; |
| |
| cache_policies_[type] = policy; |
| // If the new policy is SYSTEM_MONITOR, issue an enumeration to populate the |
| // cache. |
| if (policy == CachePolicy::SYSTEM_MONITOR) { |
| cache_infos_[type].InvalidateCache(); |
| DoEnumerateDevices(type); |
| } |
| } |
| |
| void MediaDevicesManager::StartMonitoring() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (monitoring_started_) |
| return; |
| |
| if (!base::SystemMonitor::Get()) |
| return; |
| |
| monitoring_started_ = true; |
| base::SystemMonitor::Get()->AddDevicesChangedObserver(this); |
| |
| for (size_t i = 0; i < NUM_MEDIA_DEVICE_TYPES; ++i) { |
| DCHECK(cache_policies_[i] != CachePolicy::SYSTEM_MONITOR); |
| SetCachePolicy(static_cast<MediaDeviceType>(i), |
| CachePolicy::SYSTEM_MONITOR); |
| } |
| |
| #if defined(OS_MACOSX) |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&MediaDevicesManager::StartMonitoringOnUIThread, |
| base::Unretained(this))); |
| |
| // TODO(guidou): Remove this statement once the Mac device monitor is fixed to |
| // correctly report device-change events for output-only audio devices. |
| // See http://crbug.com/648173. |
| SetCachePolicy(MEDIA_DEVICE_TYPE_AUDIO_OUTPUT, CachePolicy::NO_CACHE); |
| #endif |
| } |
| |
| #if defined(OS_MACOSX) |
| void MediaDevicesManager::StartMonitoringOnUIThread() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| // TODO(erikchen): Remove ScopedTracker below once crbug.com/458404 is fixed. |
| tracked_objects::ScopedTracker tracking_profile1( |
| FROM_HERE_WITH_EXPLICIT_FUNCTION( |
| "458404 MediaDevicesManager::GetBrowserMainLoop")); |
| BrowserMainLoop* browser_main_loop = content::BrowserMainLoop::GetInstance(); |
| if (!browser_main_loop) |
| return; |
| |
| // TODO(erikchen): Remove ScopedTracker below once crbug.com/458404 is |
| // fixed. |
| tracked_objects::ScopedTracker tracking_profile2( |
| FROM_HERE_WITH_EXPLICIT_FUNCTION( |
| "458404 MediaDevicesManager::GetTaskRunner")); |
| const scoped_refptr<base::SingleThreadTaskRunner> task_runner = |
| audio_system_->GetTaskRunner(); |
| // TODO(erikchen): Remove ScopedTracker below once crbug.com/458404 is |
| // fixed. |
| tracked_objects::ScopedTracker tracking_profile3( |
| FROM_HERE_WITH_EXPLICIT_FUNCTION( |
| "458404 MediaDevicesManager::DeviceMonitorMac::StartMonitoring")); |
| browser_main_loop->device_monitor_mac()->StartMonitoring(task_runner); |
| } |
| #endif |
| |
| void MediaDevicesManager::StopMonitoring() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| if (!monitoring_started_) |
| return; |
| base::SystemMonitor::Get()->RemoveDevicesChangedObserver(this); |
| monitoring_started_ = false; |
| for (size_t i = 0; i < NUM_MEDIA_DEVICE_TYPES; ++i) |
| SetCachePolicy(static_cast<MediaDeviceType>(i), CachePolicy::NO_CACHE); |
| } |
| |
| bool MediaDevicesManager::IsMonitoringStarted() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| return monitoring_started_; |
| } |
| |
| void MediaDevicesManager::OnDevicesChanged( |
| base::SystemMonitor::DeviceType device_type) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| switch (device_type) { |
| case base::SystemMonitor::DEVTYPE_AUDIO: |
| HandleDevicesChanged(MEDIA_DEVICE_TYPE_AUDIO_INPUT); |
| HandleDevicesChanged(MEDIA_DEVICE_TYPE_AUDIO_OUTPUT); |
| break; |
| case base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE: |
| HandleDevicesChanged(MEDIA_DEVICE_TYPE_VIDEO_INPUT); |
| break; |
| default: |
| break; // Uninteresting device change. |
| } |
| } |
| |
| MediaDeviceInfoArray MediaDevicesManager::GetCachedDeviceInfo( |
| MediaDeviceType type) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| return current_snapshot_[type]; |
| } |
| |
| void MediaDevicesManager::DoEnumerateDevices(MediaDeviceType type) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(IsValidMediaDeviceType(type)); |
| CacheInfo& cache_info = cache_infos_[type]; |
| if (cache_info.is_update_ongoing()) |
| return; |
| |
| cache_info.UpdateStarted(); |
| switch (type) { |
| case MEDIA_DEVICE_TYPE_AUDIO_INPUT: |
| EnumerateAudioDevices(true /* is_input */); |
| break; |
| case MEDIA_DEVICE_TYPE_VIDEO_INPUT: |
| video_capture_manager_->EnumerateDevices( |
| base::Bind(&MediaDevicesManager::VideoInputDevicesEnumerated, |
| weak_factory_.GetWeakPtr())); |
| break; |
| case MEDIA_DEVICE_TYPE_AUDIO_OUTPUT: |
| EnumerateAudioDevices(false /* is_input */); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void MediaDevicesManager::EnumerateAudioDevices(bool is_input) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| MediaDeviceType type = |
| is_input ? MEDIA_DEVICE_TYPE_AUDIO_INPUT : MEDIA_DEVICE_TYPE_AUDIO_OUTPUT; |
| if (use_fake_devices_) { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&MediaDevicesManager::DevicesEnumerated, |
| weak_factory_.GetWeakPtr(), type, |
| GetFakeAudioDevices(is_input))); |
| return; |
| } |
| |
| audio_system_->GetDeviceDescriptions( |
| base::Bind(&MediaDevicesManager::AudioDevicesEnumerated, |
| weak_factory_.GetWeakPtr(), type), |
| is_input); |
| } |
| |
| void MediaDevicesManager::VideoInputDevicesEnumerated( |
| const media::VideoCaptureDeviceDescriptors& descriptors) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| MediaDeviceInfoArray snapshot; |
| for (const auto& descriptor : descriptors) { |
| snapshot.emplace_back(descriptor); |
| } |
| DevicesEnumerated(MEDIA_DEVICE_TYPE_VIDEO_INPUT, snapshot); |
| } |
| |
| void MediaDevicesManager::AudioDevicesEnumerated( |
| MediaDeviceType type, |
| media::AudioDeviceDescriptions device_descriptions) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| MediaDeviceInfoArray snapshot; |
| for (const media::AudioDeviceDescription& description : device_descriptions) { |
| snapshot.emplace_back(description); |
| } |
| DevicesEnumerated(type, snapshot); |
| } |
| |
| void MediaDevicesManager::DevicesEnumerated( |
| MediaDeviceType type, |
| const MediaDeviceInfoArray& snapshot) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(IsValidMediaDeviceType(type)); |
| UpdateSnapshot(type, snapshot); |
| cache_infos_[type].UpdateCompleted(); |
| has_seen_result_[type] = true; |
| |
| std::string log_message = |
| "New device enumeration result:\n" + GetLogMessageString(type, snapshot); |
| MediaStreamManager::SendMessageToNativeLog(log_message); |
| |
| if (cache_policies_[type] == CachePolicy::NO_CACHE) { |
| for (auto& request : requests_) |
| request.has_seen_result[type] = true; |
| } |
| |
| // Note that IsLastUpdateValid is always true when policy is NO_CACHE. |
| if (cache_infos_[type].IsLastUpdateValid()) { |
| ProcessRequests(); |
| } else { |
| DoEnumerateDevices(type); |
| } |
| } |
| |
| void MediaDevicesManager::UpdateSnapshot( |
| MediaDeviceType type, |
| const MediaDeviceInfoArray& new_snapshot) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(IsValidMediaDeviceType(type)); |
| |
| // Only cache the device list when the device list has been changed. |
| bool need_update_device_change_subscribers = false; |
| MediaDeviceInfoArray& old_snapshot = current_snapshot_[type]; |
| |
| if (old_snapshot.size() != new_snapshot.size() || |
| !std::equal(new_snapshot.begin(), new_snapshot.end(), |
| old_snapshot.begin())) { |
| if (type == MEDIA_DEVICE_TYPE_AUDIO_INPUT || |
| type == MEDIA_DEVICE_TYPE_VIDEO_INPUT) { |
| NotifyMediaStreamManager(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 = |
| has_seen_result_[type] && |
| (old_snapshot.size() != 0 || new_snapshot.size() != 0); |
| current_snapshot_[type] = new_snapshot; |
| } |
| |
| if (need_update_device_change_subscribers) |
| NotifyDeviceChangeSubscribers(type, new_snapshot); |
| } |
| |
| void MediaDevicesManager::ProcessRequests() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| requests_.erase(std::remove_if(requests_.begin(), requests_.end(), |
| [this](const EnumerationRequest& request) { |
| if (IsEnumerationRequestReady(request)) { |
| request.callback.Run(current_snapshot_); |
| return true; |
| } |
| return false; |
| }), |
| requests_.end()); |
| } |
| |
| bool MediaDevicesManager::IsEnumerationRequestReady( |
| const EnumerationRequest& request_info) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| bool is_ready = true; |
| for (size_t i = 0; i < NUM_MEDIA_DEVICE_TYPES; ++i) { |
| if (!request_info.requested[i]) |
| continue; |
| switch (cache_policies_[i]) { |
| case CachePolicy::SYSTEM_MONITOR: |
| if (!cache_infos_[i].IsLastUpdateValid()) |
| is_ready = false; |
| break; |
| case CachePolicy::NO_CACHE: |
| if (!request_info.has_seen_result[i]) |
| is_ready = false; |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| return is_ready; |
| } |
| |
| void MediaDevicesManager::HandleDevicesChanged(MediaDeviceType type) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(IsValidMediaDeviceType(type)); |
| cache_infos_[type].InvalidateCache(); |
| DoEnumerateDevices(type); |
| } |
| |
| void MediaDevicesManager::NotifyMediaStreamManager( |
| MediaDeviceType type, |
| const MediaDeviceInfoArray& new_snapshot) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(type == MEDIA_DEVICE_TYPE_AUDIO_INPUT || |
| type == MEDIA_DEVICE_TYPE_VIDEO_INPUT); |
| |
| if (!media_stream_manager_) |
| return; |
| |
| for (const auto& old_device_info : current_snapshot_[type]) { |
| auto it = std::find_if(new_snapshot.begin(), new_snapshot.end(), |
| [&old_device_info](const MediaDeviceInfo& info) { |
| return info.device_id == old_device_info.device_id; |
| }); |
| |
| // If a device was removed, notify the MediaStreamManager to stop all |
| // streams using that device. |
| if (it == new_snapshot.end()) |
| media_stream_manager_->StopRemovedDevice(type, old_device_info); |
| } |
| |
| media_stream_manager_->NotifyDevicesChanged(type, new_snapshot); |
| } |
| |
| void MediaDevicesManager::NotifyDeviceChangeSubscribers( |
| MediaDeviceType type, |
| const MediaDeviceInfoArray& snapshot) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(IsValidMediaDeviceType(type)); |
| |
| for (auto* subscriber : device_change_subscribers_[type]) { |
| subscriber->OnDevicesChanged(type, snapshot); |
| } |
| } |
| |
| } // namespace content |