| // Copyright (c) 2012 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 "media/device_monitors/device_monitor_mac.h" |
| |
| #include <AVFoundation/AVFoundation.h> |
| #include <set> |
| |
| #include "base/bind_helpers.h" |
| #include "base/logging.h" |
| #include "base/mac/bind_objc_block.h" |
| #include "base/mac/scoped_nsobject.h" |
| #include "base/macros.h" |
| #include "base/profiler/scoped_tracker.h" |
| #include "base/task_runner_util.h" |
| #include "base/threading/thread_checker.h" |
| |
| namespace { |
| |
| // This class is used to keep track of system devices names and their types. |
| class DeviceInfo { |
| public: |
| enum DeviceType { kAudio, kVideo, kMuxed, kUnknown, kInvalid }; |
| |
| DeviceInfo(const std::string& unique_id, DeviceType type) |
| : unique_id_(unique_id), type_(type) {} |
| |
| // Operator== is needed here to use this class in a std::find. A given |
| // |unique_id_| always has the same |type_| so for comparison purposes the |
| // latter can be safely ignored. |
| bool operator==(const DeviceInfo& device) const { |
| return unique_id_ == device.unique_id_; |
| } |
| |
| const std::string& unique_id() const { return unique_id_; } |
| DeviceType type() const { return type_; } |
| |
| private: |
| std::string unique_id_; |
| DeviceType type_; |
| // Allow generated copy constructor and assignment. |
| }; |
| |
| // Base abstract class used by DeviceMonitorMac. |
| class DeviceMonitorMacImpl { |
| public: |
| explicit DeviceMonitorMacImpl(media::DeviceMonitorMac* monitor) |
| : monitor_(monitor), |
| cached_devices_(), |
| device_arrival_(nil), |
| device_removal_(nil) { |
| DCHECK(monitor); |
| // Initialise the devices_cache_ with a not-valid entry. For the case in |
| // which there is one single device in the system and we get notified when |
| // it gets removed, this will prevent the system from thinking that no |
| // devices were added nor removed and not notifying the |monitor_|. |
| cached_devices_.push_back(DeviceInfo("invalid", DeviceInfo::kInvalid)); |
| } |
| virtual ~DeviceMonitorMacImpl() {} |
| |
| virtual void OnDeviceChanged() = 0; |
| |
| // Method called by the default notification center when a device is removed |
| // or added to the system. It will compare the |cached_devices_| with the |
| // current situation, update it, and, if there's an update, signal to |
| // |monitor_| with the appropriate device type. |
| void ConsolidateDevicesListAndNotify( |
| const std::vector<DeviceInfo>& snapshot_devices); |
| |
| protected: |
| media::DeviceMonitorMac* monitor_; |
| std::vector<DeviceInfo> cached_devices_; |
| |
| // Handles to NSNotificationCenter block observers. |
| id device_arrival_; |
| id device_removal_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DeviceMonitorMacImpl); |
| }; |
| |
| void DeviceMonitorMacImpl::ConsolidateDevicesListAndNotify( |
| const std::vector<DeviceInfo>& snapshot_devices) { |
| bool video_device_added = false; |
| bool audio_device_added = false; |
| bool video_device_removed = false; |
| bool audio_device_removed = false; |
| |
| // Compare the current system devices snapshot with the ones cached to detect |
| // additions, present in the former but not in the latter. If we find a device |
| // in snapshot_devices entry also present in cached_devices, we remove it from |
| // the latter vector. |
| std::vector<DeviceInfo>::const_iterator it; |
| for (it = snapshot_devices.begin(); it != snapshot_devices.end(); ++it) { |
| std::vector<DeviceInfo>::iterator cached_devices_iterator = |
| std::find(cached_devices_.begin(), cached_devices_.end(), *it); |
| if (cached_devices_iterator == cached_devices_.end()) { |
| video_device_added |= ((it->type() == DeviceInfo::kVideo) || |
| (it->type() == DeviceInfo::kMuxed)); |
| audio_device_added |= ((it->type() == DeviceInfo::kAudio) || |
| (it->type() == DeviceInfo::kMuxed)); |
| DVLOG(1) << "Device has been added, id: " << it->unique_id(); |
| } else { |
| cached_devices_.erase(cached_devices_iterator); |
| } |
| } |
| // All the remaining entries in cached_devices are removed devices. |
| for (it = cached_devices_.begin(); it != cached_devices_.end(); ++it) { |
| video_device_removed |= ((it->type() == DeviceInfo::kVideo) || |
| (it->type() == DeviceInfo::kMuxed) || |
| (it->type() == DeviceInfo::kInvalid)); |
| audio_device_removed |= ((it->type() == DeviceInfo::kAudio) || |
| (it->type() == DeviceInfo::kMuxed) || |
| (it->type() == DeviceInfo::kInvalid)); |
| DVLOG(1) << "Device has been removed, id: " << it->unique_id(); |
| } |
| // Update the cached devices with the current system snapshot. |
| cached_devices_ = snapshot_devices; |
| |
| if (video_device_added || video_device_removed) |
| monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE); |
| if (audio_device_added || audio_device_removed) |
| monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_AUDIO); |
| } |
| |
| // Forward declaration for use by CrAVFoundationDeviceObserver. |
| class SuspendObserverDelegate; |
| |
| } // namespace |
| |
| // This class is a Key-Value Observer (KVO) shim. It is needed because C++ |
| // classes cannot observe Key-Values directly. Created, manipulated, and |
| // destroyed on the UI Thread by SuspendObserverDelegate. |
| @interface CrAVFoundationDeviceObserver : NSObject { |
| @private |
| // Callback for device changed, has to run on Device Thread. |
| base::Closure onDeviceChangedCallback_; |
| |
| // Member to keep track of the devices we are already monitoring. |
| std::set<base::scoped_nsobject<AVCaptureDevice>> monitoredDevices_; |
| |
| // Pegged to the "main" thread -- usually content::BrowserThread::UI. |
| base::ThreadChecker mainThreadChecker_; |
| } |
| |
| - (id)initWithOnChangedCallback:(const base::Closure&)callback; |
| - (void)startObserving:(base::scoped_nsobject<AVCaptureDevice>)device; |
| - (void)stopObserving:(AVCaptureDevice*)device; |
| - (void)clearOnDeviceChangedCallback; |
| |
| @end |
| |
| namespace { |
| |
| // This class owns and manages the lifetime of a CrAVFoundationDeviceObserver. |
| // It is created and destroyed on AVFoundationMonitorImpl's main thread (usually |
| // browser's UI thread), and it operates on this thread except for the expensive |
| // device enumerations which are run on Device Thread. |
| class SuspendObserverDelegate |
| : public base::RefCountedThreadSafe<SuspendObserverDelegate> { |
| public: |
| explicit SuspendObserverDelegate(DeviceMonitorMacImpl* monitor); |
| |
| // Create |suspend_observer_| for all devices and register OnDeviceChanged() |
| // as its change callback. Schedule bottom half in DoStartObserver(). |
| void StartObserver( |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_thread); |
| // Enumerate devices in |device_thread| and run the bottom half in |
| // DoOnDeviceChange(). |suspend_observer_| calls back here on suspend event, |
| // and our parent AVFoundationMonitorImpl calls on connect/disconnect device. |
| void OnDeviceChanged( |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_thread); |
| // Remove the device monitor's weak reference. Remove ourselves as suspend |
| // notification observer from |suspend_observer_|. |
| void ResetDeviceMonitor(); |
| |
| private: |
| friend class base::RefCountedThreadSafe<SuspendObserverDelegate>; |
| |
| virtual ~SuspendObserverDelegate(); |
| |
| // Bottom half of StartObserver(), starts |suspend_observer_| for all devices. |
| // Assumes that |devices| has been retained prior to being called, and |
| // releases it internally. |
| void DoStartObserver(NSArray* devices); |
| // Bottom half of OnDeviceChanged(), starts |suspend_observer_| for current |
| // devices and composes a snapshot of them to send it to |
| // |avfoundation_monitor_impl_|. Assumes that |devices| has been retained |
| // prior to being called, and releases it internally. |
| void DoOnDeviceChanged(NSArray* devices); |
| |
| base::scoped_nsobject<CrAVFoundationDeviceObserver> suspend_observer_; |
| DeviceMonitorMacImpl* avfoundation_monitor_impl_; |
| |
| // Pegged to the "main" thread -- usually content::BrowserThread::UI. |
| base::ThreadChecker main_thread_checker_; |
| }; |
| |
| SuspendObserverDelegate::SuspendObserverDelegate(DeviceMonitorMacImpl* monitor) |
| : avfoundation_monitor_impl_(monitor) { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| } |
| |
| void SuspendObserverDelegate::StartObserver( |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| |
| base::Closure on_device_changed_callback = base::Bind( |
| &SuspendObserverDelegate::OnDeviceChanged, this, device_thread); |
| suspend_observer_.reset([[CrAVFoundationDeviceObserver alloc] |
| initWithOnChangedCallback:on_device_changed_callback]); |
| |
| // Enumerate the devices in Device thread and post the observers start to be |
| // done on UI thread. The devices array is retained in |device_thread| and |
| // released in DoStartObserver(). |
| base::PostTaskAndReplyWithResult( |
| device_thread.get(), FROM_HERE, base::BindBlock(^{ |
| return [[AVCaptureDevice devices] retain]; |
| }), |
| base::Bind(&SuspendObserverDelegate::DoStartObserver, this)); |
| } |
| |
| void SuspendObserverDelegate::OnDeviceChanged( |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| // Enumerate the devices in Device thread and post the consolidation of the |
| // new devices and the old ones to be done on main thread. The devices array |
| // is retained in |device_thread| and released in DoOnDeviceChanged(). |
| PostTaskAndReplyWithResult( |
| device_thread.get(), FROM_HERE, base::BindBlock(^{ |
| return [[AVCaptureDevice devices] retain]; |
| }), |
| base::Bind(&SuspendObserverDelegate::DoOnDeviceChanged, this)); |
| } |
| |
| void SuspendObserverDelegate::ResetDeviceMonitor() { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| avfoundation_monitor_impl_ = NULL; |
| [suspend_observer_ clearOnDeviceChangedCallback]; |
| } |
| |
| SuspendObserverDelegate::~SuspendObserverDelegate() { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| } |
| |
| void SuspendObserverDelegate::DoStartObserver(NSArray* devices) { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| base::scoped_nsobject<NSArray> auto_release(devices); |
| for (AVCaptureDevice* device in devices) { |
| base::scoped_nsobject<AVCaptureDevice> device_ptr([device retain]); |
| [suspend_observer_ startObserving:device_ptr]; |
| } |
| } |
| |
| void SuspendObserverDelegate::DoOnDeviceChanged(NSArray* devices) { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| base::scoped_nsobject<NSArray> auto_release(devices); |
| std::vector<DeviceInfo> snapshot_devices; |
| for (AVCaptureDevice* device in devices) { |
| base::scoped_nsobject<AVCaptureDevice> device_ptr([device retain]); |
| [suspend_observer_ startObserving:device_ptr]; |
| |
| BOOL suspended = [device respondsToSelector:@selector(isSuspended)] && |
| [device isSuspended]; |
| DeviceInfo::DeviceType device_type = DeviceInfo::kUnknown; |
| if ([device hasMediaType:AVMediaTypeVideo]) { |
| if (suspended) |
| continue; |
| device_type = DeviceInfo::kVideo; |
| } else if ([device hasMediaType:AVMediaTypeMuxed]) { |
| device_type = suspended ? DeviceInfo::kAudio : DeviceInfo::kMuxed; |
| } else if ([device hasMediaType:AVMediaTypeAudio]) { |
| device_type = DeviceInfo::kAudio; |
| } |
| snapshot_devices.push_back( |
| DeviceInfo([[device uniqueID] UTF8String], device_type)); |
| } |
| // Make sure no references are held to |devices| when |
| // ConsolidateDevicesListAndNotify is called since the VideoCaptureManager |
| // and AudioCaptureManagers also enumerates the available devices but on |
| // another thread. |
| auto_release.reset(); |
| // |avfoundation_monitor_impl_| might have been NULLed asynchronously before |
| // arriving at this line. |
| if (avfoundation_monitor_impl_) { |
| avfoundation_monitor_impl_->ConsolidateDevicesListAndNotify( |
| snapshot_devices); |
| } |
| } |
| |
| // AVFoundation implementation of the Mac Device Monitor, registers as a global |
| // device connect/disconnect observer and plugs suspend/wake up device observers |
| // per device. This class is created and lives on the main Application thread |
| // (UI for content). Owns a SuspendObserverDelegate that notifies when a device |
| // is suspended/resumed. |
| class AVFoundationMonitorImpl : public DeviceMonitorMacImpl { |
| public: |
| AVFoundationMonitorImpl( |
| media::DeviceMonitorMac* monitor, |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner); |
| ~AVFoundationMonitorImpl() override; |
| |
| void OnDeviceChanged() override; |
| |
| private: |
| // {Video,AudioInput}DeviceManager's "Device" thread task runner used for |
| // posting tasks to |suspend_observer_delegate_|; |
| const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_; |
| |
| // Pegged to the "main" thread -- usually content::BrowserThread::UI. |
| base::ThreadChecker main_thread_checker_; |
| |
| scoped_refptr<SuspendObserverDelegate> suspend_observer_delegate_; |
| |
| DISALLOW_COPY_AND_ASSIGN(AVFoundationMonitorImpl); |
| }; |
| |
| AVFoundationMonitorImpl::AVFoundationMonitorImpl( |
| media::DeviceMonitorMac* monitor, |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner) |
| : DeviceMonitorMacImpl(monitor), |
| device_task_runner_(device_task_runner), |
| suspend_observer_delegate_(new SuspendObserverDelegate(this)) { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
| device_arrival_ = |
| [nc addObserverForName:AVCaptureDeviceWasConnectedNotification |
| object:nil |
| queue:nil |
| usingBlock:^(NSNotification* notification) { |
| OnDeviceChanged(); |
| }]; |
| device_removal_ = |
| [nc addObserverForName:AVCaptureDeviceWasDisconnectedNotification |
| object:nil |
| queue:nil |
| usingBlock:^(NSNotification* notification) { |
| OnDeviceChanged(); |
| }]; |
| suspend_observer_delegate_->StartObserver(device_task_runner_); |
| } |
| |
| AVFoundationMonitorImpl::~AVFoundationMonitorImpl() { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| suspend_observer_delegate_->ResetDeviceMonitor(); |
| NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
| [nc removeObserver:device_arrival_]; |
| [nc removeObserver:device_removal_]; |
| } |
| |
| void AVFoundationMonitorImpl::OnDeviceChanged() { |
| DCHECK(main_thread_checker_.CalledOnValidThread()); |
| suspend_observer_delegate_->OnDeviceChanged(device_task_runner_); |
| } |
| |
| } // namespace |
| |
| @implementation CrAVFoundationDeviceObserver |
| |
| - (id)initWithOnChangedCallback:(const base::Closure&)callback { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| if ((self = [super init])) { |
| DCHECK(!callback.is_null()); |
| onDeviceChangedCallback_ = callback; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| std::set<base::scoped_nsobject<AVCaptureDevice>>::iterator it = |
| monitoredDevices_.begin(); |
| while (it != monitoredDevices_.end()) |
| [self removeObservers:*(it++)]; |
| [super dealloc]; |
| } |
| |
| - (void)startObserving:(base::scoped_nsobject<AVCaptureDevice>)device { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| DCHECK(device != nil); |
| // Skip this device if there are already observers connected to it. |
| if (std::find(monitoredDevices_.begin(), monitoredDevices_.end(), device) != |
| monitoredDevices_.end()) { |
| return; |
| } |
| [device addObserver:self |
| forKeyPath:@"suspended" |
| options:0 |
| context:device.get()]; |
| [device addObserver:self |
| forKeyPath:@"connected" |
| options:0 |
| context:device.get()]; |
| monitoredDevices_.insert(device); |
| } |
| |
| - (void)stopObserving:(AVCaptureDevice*)device { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| DCHECK(device != nil); |
| |
| std::set<base::scoped_nsobject<AVCaptureDevice>>::iterator found = |
| std::find(monitoredDevices_.begin(), monitoredDevices_.end(), device); |
| DCHECK(found != monitoredDevices_.end()); |
| [self removeObservers:*found]; |
| monitoredDevices_.erase(found); |
| } |
| |
| - (void)clearOnDeviceChangedCallback { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| onDeviceChangedCallback_.Reset(); |
| } |
| |
| - (void)removeObservers:(AVCaptureDevice*)device { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| // Check sanity of |device| via its -observationInfo. http://crbug.com/371271. |
| if ([device observationInfo]) { |
| [device removeObserver:self |
| forKeyPath:@"suspended"]; |
| [device removeObserver:self |
| forKeyPath:@"connected"]; |
| } |
| } |
| |
| - (void)observeValueForKeyPath:(NSString*)keyPath |
| ofObject:(id)object |
| change:(NSDictionary*)change |
| context:(void*)context { |
| DCHECK(mainThreadChecker_.CalledOnValidThread()); |
| if ([keyPath isEqual:@"suspended"]) |
| onDeviceChangedCallback_.Run(); |
| if ([keyPath isEqual:@"connected"]) |
| [self stopObserving:static_cast<AVCaptureDevice*>(context)]; |
| } |
| |
| @end // @implementation CrAVFoundationDeviceObserver |
| |
| namespace media { |
| |
| DeviceMonitorMac::DeviceMonitorMac() { |
| // AVFoundation do not need to be fired up until the user |
| // exercises a GetUserMedia. Bringing up either library and enumerating the |
| // devices in the system is an operation taking in the range of hundred of ms, |
| // so it is triggered explicitly from MediaStreamManager::StartMonitoring(). |
| } |
| |
| DeviceMonitorMac::~DeviceMonitorMac() {} |
| |
| void DeviceMonitorMac::StartMonitoring( |
| const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| // TODO(erikchen): Remove ScopedTracker below once http://crbug.com/458404 |
| // is fixed. |
| tracked_objects::ScopedTracker tracking_profile( |
| FROM_HERE_WITH_EXPLICIT_FUNCTION( |
| "458404 DeviceMonitorMac::StartMonitoring::AVFoundation")); |
| DVLOG(1) << "Monitoring via AVFoundation"; |
| device_monitor_impl_.reset( |
| new AVFoundationMonitorImpl(this, device_task_runner)); |
| } |
| |
| void DeviceMonitorMac::NotifyDeviceChanged( |
| base::SystemMonitor::DeviceType type) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| // TODO(xians): Remove the global variable for SystemMonitor. |
| base::SystemMonitor::Get()->ProcessDevicesChanged(type); |
| } |
| |
| } // namespace media |