1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "content/browser/device_monitor_mac.h"
7 #import <QTKit/QTKit.h>
11 #include "base/bind_helpers.h"
12 #include "base/logging.h"
13 #include "base/mac/bind_objc_block.h"
14 #include "base/mac/scoped_nsobject.h"
15 #include "base/threading/thread_checker.h"
16 #include "content/public/browser/browser_thread.h"
17 #import "media/video/capture/mac/avfoundation_glue.h"
19 using content::BrowserThread;
23 // This class is used to keep track of system devices names and their types.
34 DeviceInfo(std::string unique_id, DeviceType type)
35 : unique_id_(unique_id), type_(type) {}
37 // Operator== is needed here to use this class in a std::find. A given
38 // |unique_id_| always has the same |type_| so for comparison purposes the
39 // latter can be safely ignored.
40 bool operator==(const DeviceInfo& device) const {
41 return unique_id_ == device.unique_id_;
44 const std::string& unique_id() const { return unique_id_; }
45 DeviceType type() const { return type_; }
48 std::string unique_id_;
50 // Allow generated copy constructor and assignment.
53 // Base abstract class used by DeviceMonitorMac to interact with either a QTKit
54 // or an AVFoundation implementation of events and notifications.
55 class DeviceMonitorMacImpl {
57 explicit DeviceMonitorMacImpl(content::DeviceMonitorMac* monitor)
61 device_removal_(nil) {
63 // Initialise the devices_cache_ with a not-valid entry. For the case in
64 // which there is one single device in the system and we get notified when
65 // it gets removed, this will prevent the system from thinking that no
66 // devices were added nor removed and not notifying the |monitor_|.
67 cached_devices_.push_back(DeviceInfo("invalid", DeviceInfo::kInvalid));
69 virtual ~DeviceMonitorMacImpl() {}
71 virtual void OnDeviceChanged() = 0;
73 // Method called by the default notification center when a device is removed
74 // or added to the system. It will compare the |cached_devices_| with the
75 // current situation, update it, and, if there's an update, signal to
76 // |monitor_| with the appropriate device type.
77 void ConsolidateDevicesListAndNotify(
78 const std::vector<DeviceInfo>& snapshot_devices);
81 content::DeviceMonitorMac* monitor_;
82 std::vector<DeviceInfo> cached_devices_;
84 // Handles to NSNotificationCenter block observers.
89 DISALLOW_COPY_AND_ASSIGN(DeviceMonitorMacImpl);
92 void DeviceMonitorMacImpl::ConsolidateDevicesListAndNotify(
93 const std::vector<DeviceInfo>& snapshot_devices) {
94 bool video_device_added = false;
95 bool audio_device_added = false;
96 bool video_device_removed = false;
97 bool audio_device_removed = false;
99 // Compare the current system devices snapshot with the ones cached to detect
100 // additions, present in the former but not in the latter. If we find a device
101 // in snapshot_devices entry also present in cached_devices, we remove it from
102 // the latter vector.
103 std::vector<DeviceInfo>::const_iterator it;
104 for (it = snapshot_devices.begin(); it != snapshot_devices.end(); ++it) {
105 std::vector<DeviceInfo>::iterator cached_devices_iterator =
106 std::find(cached_devices_.begin(), cached_devices_.end(), *it);
107 if (cached_devices_iterator == cached_devices_.end()) {
108 video_device_added |= ((it->type() == DeviceInfo::kVideo) ||
109 (it->type() == DeviceInfo::kMuxed));
110 audio_device_added |= ((it->type() == DeviceInfo::kAudio) ||
111 (it->type() == DeviceInfo::kMuxed));
112 DVLOG(1) << "Device has been added, id: " << it->unique_id();
114 cached_devices_.erase(cached_devices_iterator);
117 // All the remaining entries in cached_devices are removed devices.
118 for (it = cached_devices_.begin(); it != cached_devices_.end(); ++it) {
119 video_device_removed |= ((it->type() == DeviceInfo::kVideo) ||
120 (it->type() == DeviceInfo::kMuxed) ||
121 (it->type() == DeviceInfo::kInvalid));
122 audio_device_removed |= ((it->type() == DeviceInfo::kAudio) ||
123 (it->type() == DeviceInfo::kMuxed) ||
124 (it->type() == DeviceInfo::kInvalid));
125 DVLOG(1) << "Device has been removed, id: " << it->unique_id();
127 // Update the cached devices with the current system snapshot.
128 cached_devices_ = snapshot_devices;
130 if (video_device_added || video_device_removed)
131 monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE);
132 if (audio_device_added || audio_device_removed)
133 monitor_->NotifyDeviceChanged(base::SystemMonitor::DEVTYPE_AUDIO_CAPTURE);
136 class QTKitMonitorImpl : public DeviceMonitorMacImpl {
138 explicit QTKitMonitorImpl(content::DeviceMonitorMac* monitor);
139 virtual ~QTKitMonitorImpl();
141 virtual void OnDeviceChanged() OVERRIDE;
144 void OnAttributeChanged(NSNotification* notification);
149 QTKitMonitorImpl::QTKitMonitorImpl(content::DeviceMonitorMac* monitor)
150 : DeviceMonitorMacImpl(monitor) {
151 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
153 [nc addObserverForName:QTCaptureDeviceWasConnectedNotification
156 usingBlock:^(NSNotification* notification) {
157 OnDeviceChanged();}];
159 [nc addObserverForName:QTCaptureDeviceWasDisconnectedNotification
162 usingBlock:^(NSNotification* notification) {
163 OnDeviceChanged();}];
165 [nc addObserverForName:QTCaptureDeviceAttributeDidChangeNotification
168 usingBlock:^(NSNotification* notification) {
169 OnAttributeChanged(notification);}];
172 QTKitMonitorImpl::~QTKitMonitorImpl() {
173 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
174 [nc removeObserver:device_arrival_];
175 [nc removeObserver:device_removal_];
176 [nc removeObserver:device_change_];
179 void QTKitMonitorImpl::OnAttributeChanged(
180 NSNotification* notification) {
181 if ([[[notification userInfo]
182 objectForKey:QTCaptureDeviceChangedAttributeKey]
183 isEqualToString:QTCaptureDeviceSuspendedAttribute]) {
188 void QTKitMonitorImpl::OnDeviceChanged() {
189 std::vector<DeviceInfo> snapshot_devices;
191 NSArray* devices = [QTCaptureDevice inputDevices];
192 for (QTCaptureDevice* device in devices) {
193 DeviceInfo::DeviceType device_type = DeviceInfo::kUnknown;
194 // Act as if suspended video capture devices are not attached. For
195 // example, a laptop's internal webcam is suspended when the lid is closed.
196 if ([device hasMediaType:QTMediaTypeVideo] &&
197 ![[device attributeForKey:QTCaptureDeviceSuspendedAttribute]
199 device_type = DeviceInfo::kVideo;
200 } else if ([device hasMediaType:QTMediaTypeMuxed] &&
201 ![[device attributeForKey:QTCaptureDeviceSuspendedAttribute]
203 device_type = DeviceInfo::kMuxed;
204 } else if ([device hasMediaType:QTMediaTypeSound] &&
205 ![[device attributeForKey:QTCaptureDeviceSuspendedAttribute]
207 device_type = DeviceInfo::kAudio;
209 snapshot_devices.push_back(
210 DeviceInfo([[device uniqueID] UTF8String], device_type));
212 ConsolidateDevicesListAndNotify(snapshot_devices);
215 // Forward declaration for use by CrAVFoundationDeviceObserver.
216 class SuspendObserverDelegate;
220 // This class is a Key-Value Observer (KVO) shim. It is needed because C++
221 // classes cannot observe Key-Values directly. Created, manipulated, and
222 // destroyed on the UI Thread by SuspendObserverDelegate.
223 @interface CrAVFoundationDeviceObserver : NSObject {
225 // Callback for device changed, has to run on Device Thread.
226 base::Closure onDeviceChangedCallback_;
228 // Member to keep track of the devices we are already monitoring.
229 std::set<base::scoped_nsobject<CrAVCaptureDevice> > monitoredDevices_;
232 - (id)initWithOnChangedCallback:(const base::Closure&)callback;
233 - (void)startObserving:(base::scoped_nsobject<CrAVCaptureDevice>)device;
234 - (void)stopObserving:(CrAVCaptureDevice*)device;
235 - (void)clearOnDeviceChangedCallback;
241 // This class owns and manages the lifetime of a CrAVFoundationDeviceObserver.
242 // It is created and destroyed in UI thread by AVFoundationMonitorImpl, and it
243 // operates on this thread except for the expensive device enumerations which
244 // are run on Device Thread.
245 class SuspendObserverDelegate :
246 public base::RefCountedThreadSafe<SuspendObserverDelegate> {
248 explicit SuspendObserverDelegate(DeviceMonitorMacImpl* monitor);
250 // Create |suspend_observer_| for all devices and register OnDeviceChanged()
251 // as its change callback. Schedule bottom half in DoStartObserver().
253 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread);
254 // Enumerate devices in |device_thread| and run the bottom half in
255 // DoOnDeviceChange(). |suspend_observer_| calls back here on suspend event,
256 // and our parent AVFoundationMonitorImpl calls on connect/disconnect device.
257 void OnDeviceChanged(
258 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread);
259 // Remove the device monitor's weak reference. Remove ourselves as suspend
260 // notification observer from |suspend_observer_|.
261 void ResetDeviceMonitor();
264 friend class base::RefCountedThreadSafe<SuspendObserverDelegate>;
266 virtual ~SuspendObserverDelegate();
268 // Bottom half of StartObserver(), starts |suspend_observer_| for all devices.
269 // Assumes that |devices| has been retained prior to being called, and
270 // releases it internally.
271 void DoStartObserver(NSArray* devices);
272 // Bottom half of OnDeviceChanged(), starts |suspend_observer_| for current
273 // devices and composes a snapshot of them to send it to
274 // |avfoundation_monitor_impl_|. Assumes that |devices| has been retained
275 // prior to being called, and releases it internally.
276 void DoOnDeviceChanged(NSArray* devices);
278 base::scoped_nsobject<CrAVFoundationDeviceObserver> suspend_observer_;
279 DeviceMonitorMacImpl* avfoundation_monitor_impl_;
282 SuspendObserverDelegate::SuspendObserverDelegate(DeviceMonitorMacImpl* monitor)
283 : avfoundation_monitor_impl_(monitor) {
284 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
287 void SuspendObserverDelegate::StartObserver(
288 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) {
289 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
291 base::Closure on_device_changed_callback =
292 base::Bind(&SuspendObserverDelegate::OnDeviceChanged,
293 this, device_thread);
294 suspend_observer_.reset([[CrAVFoundationDeviceObserver alloc]
295 initWithOnChangedCallback:on_device_changed_callback]);
297 // Enumerate the devices in Device thread and post the observers start to be
298 // done on UI thread. The devices array is retained in |device_thread| and
299 // released in DoStartObserver().
300 base::PostTaskAndReplyWithResult(device_thread, FROM_HERE,
301 base::BindBlock(^{ return [[AVCaptureDeviceGlue devices] retain]; }),
302 base::Bind(&SuspendObserverDelegate::DoStartObserver, this));
305 void SuspendObserverDelegate::OnDeviceChanged(
306 const scoped_refptr<base::SingleThreadTaskRunner>& device_thread) {
307 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
308 // Enumerate the devices in Device thread and post the consolidation of the
309 // new devices and the old ones to be done on UI thread. The devices array
310 // is retained in |device_thread| and released in DoOnDeviceChanged().
311 PostTaskAndReplyWithResult(device_thread, FROM_HERE,
312 base::BindBlock(^{ return [[AVCaptureDeviceGlue devices] retain]; }),
313 base::Bind(&SuspendObserverDelegate::DoOnDeviceChanged, this));
316 void SuspendObserverDelegate::ResetDeviceMonitor() {
317 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
318 avfoundation_monitor_impl_ = NULL;
319 [suspend_observer_ clearOnDeviceChangedCallback];
322 SuspendObserverDelegate::~SuspendObserverDelegate() {
323 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
326 void SuspendObserverDelegate::DoStartObserver(NSArray* devices) {
327 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
328 base::scoped_nsobject<NSArray> auto_release(devices);
329 for (CrAVCaptureDevice* device in devices) {
330 base::scoped_nsobject<CrAVCaptureDevice> device_ptr([device retain]);
331 [suspend_observer_ startObserving:device_ptr];
335 void SuspendObserverDelegate::DoOnDeviceChanged(NSArray* devices) {
336 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
337 base::scoped_nsobject<NSArray> auto_release(devices);
338 std::vector<DeviceInfo> snapshot_devices;
339 for (CrAVCaptureDevice* device in devices) {
340 base::scoped_nsobject<CrAVCaptureDevice> device_ptr([device retain]);
341 [suspend_observer_ startObserving:device_ptr];
343 BOOL suspended = [device respondsToSelector:@selector(isSuspended)] &&
344 [device isSuspended];
345 DeviceInfo::DeviceType device_type = DeviceInfo::kUnknown;
346 if ([device hasMediaType:AVFoundationGlue::AVMediaTypeVideo()]) {
349 device_type = DeviceInfo::kVideo;
350 } else if ([device hasMediaType:AVFoundationGlue::AVMediaTypeMuxed()]) {
351 device_type = suspended ? DeviceInfo::kAudio : DeviceInfo::kMuxed;
352 } else if ([device hasMediaType:AVFoundationGlue::AVMediaTypeAudio()]) {
353 device_type = DeviceInfo::kAudio;
355 snapshot_devices.push_back(DeviceInfo([[device uniqueID] UTF8String],
359 // |avfoundation_monitor_impl_| might have been NULLed asynchronously before
360 // arriving at this line.
361 if (avfoundation_monitor_impl_) {
362 avfoundation_monitor_impl_->ConsolidateDevicesListAndNotify(
367 // AVFoundation implementation of the Mac Device Monitor, registers as a global
368 // device connect/disconnect observer and plugs suspend/wake up device observers
369 // per device. This class is created and lives in UI thread. Owns a
370 // SuspendObserverDelegate that notifies when a device is suspended/resumed.
371 class AVFoundationMonitorImpl : public DeviceMonitorMacImpl {
373 AVFoundationMonitorImpl(
374 content::DeviceMonitorMac* monitor,
375 const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner);
376 virtual ~AVFoundationMonitorImpl();
378 virtual void OnDeviceChanged() OVERRIDE;
381 // {Video,AudioInput}DeviceManager's "Device" thread task runner used for
382 // posting tasks to |suspend_observer_delegate_|; valid after
383 // MediaStreamManager calls StartMonitoring().
384 const scoped_refptr<base::SingleThreadTaskRunner> device_task_runner_;
386 scoped_refptr<SuspendObserverDelegate> suspend_observer_delegate_;
388 DISALLOW_COPY_AND_ASSIGN(AVFoundationMonitorImpl);
391 AVFoundationMonitorImpl::AVFoundationMonitorImpl(
392 content::DeviceMonitorMac* monitor,
393 const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner)
394 : DeviceMonitorMacImpl(monitor),
395 device_task_runner_(device_task_runner),
396 suspend_observer_delegate_(new SuspendObserverDelegate(this)) {
397 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
398 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
400 [nc addObserverForName:AVFoundationGlue::
401 AVCaptureDeviceWasConnectedNotification()
404 usingBlock:^(NSNotification* notification) {
405 OnDeviceChanged();}];
407 [nc addObserverForName:AVFoundationGlue::
408 AVCaptureDeviceWasDisconnectedNotification()
411 usingBlock:^(NSNotification* notification) {
412 OnDeviceChanged();}];
413 suspend_observer_delegate_->StartObserver(device_task_runner_);
416 AVFoundationMonitorImpl::~AVFoundationMonitorImpl() {
417 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
418 suspend_observer_delegate_->ResetDeviceMonitor();
419 NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
420 [nc removeObserver:device_arrival_];
421 [nc removeObserver:device_removal_];
424 void AVFoundationMonitorImpl::OnDeviceChanged() {
425 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
426 suspend_observer_delegate_->OnDeviceChanged(device_task_runner_);
431 @implementation CrAVFoundationDeviceObserver
433 - (id)initWithOnChangedCallback:(const base::Closure&)callback {
434 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
435 if ((self = [super init])) {
436 DCHECK(!callback.is_null());
437 onDeviceChangedCallback_ = callback;
443 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
444 std::set<base::scoped_nsobject<CrAVCaptureDevice> >::iterator it =
445 monitoredDevices_.begin();
446 while (it != monitoredDevices_.end())
447 [self removeObservers:*(it++)];
451 - (void)startObserving:(base::scoped_nsobject<CrAVCaptureDevice>)device {
452 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
453 DCHECK(device != nil);
454 // Skip this device if there are already observers connected to it.
455 if (std::find(monitoredDevices_.begin(), monitoredDevices_.end(), device) !=
456 monitoredDevices_.end()) {
459 [device addObserver:self
460 forKeyPath:@"suspended"
462 context:device.get()];
463 [device addObserver:self
464 forKeyPath:@"connected"
466 context:device.get()];
467 monitoredDevices_.insert(device);
470 - (void)stopObserving:(CrAVCaptureDevice*)device {
471 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
472 DCHECK(device != nil);
474 std::set<base::scoped_nsobject<CrAVCaptureDevice> >::iterator found =
475 std::find(monitoredDevices_.begin(), monitoredDevices_.end(), device);
476 DCHECK(found != monitoredDevices_.end());
477 [self removeObservers:*found];
478 monitoredDevices_.erase(found);
481 - (void)clearOnDeviceChangedCallback {
482 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
483 onDeviceChangedCallback_.Reset();
486 - (void)removeObservers:(CrAVCaptureDevice*)device {
487 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
488 // Check sanity of |device| via its -observationInfo. http://crbug.com/371271.
489 if ([device observationInfo]) {
490 [device removeObserver:self
491 forKeyPath:@"suspended"];
492 [device removeObserver:self
493 forKeyPath:@"connected"];
497 - (void)observeValueForKeyPath:(NSString*)keyPath
499 change:(NSDictionary*)change
500 context:(void*)context {
501 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
502 if ([keyPath isEqual:@"suspended"])
503 onDeviceChangedCallback_.Run();
504 if ([keyPath isEqual:@"connected"])
505 [self stopObserving:static_cast<CrAVCaptureDevice*>(context)];
508 @end // @implementation CrAVFoundationDeviceObserver
512 DeviceMonitorMac::DeviceMonitorMac() {
513 // Both QTKit and AVFoundation do not need to be fired up until the user
514 // exercises a GetUserMedia. Bringing up either library and enumerating the
515 // devices in the system is an operation taking in the range of hundred of ms,
516 // so it is triggered explicitly from MediaStreamManager::StartMonitoring().
519 DeviceMonitorMac::~DeviceMonitorMac() {}
521 void DeviceMonitorMac::StartMonitoring(
522 const scoped_refptr<base::SingleThreadTaskRunner>& device_task_runner) {
523 DCHECK(thread_checker_.CalledOnValidThread());
524 if (AVFoundationGlue::IsAVFoundationSupported()) {
525 DVLOG(1) << "Monitoring via AVFoundation";
526 device_monitor_impl_.reset(new AVFoundationMonitorImpl(this,
527 device_task_runner));
529 DVLOG(1) << "Monitoring via QTKit";
530 device_monitor_impl_.reset(new QTKitMonitorImpl(this));
534 void DeviceMonitorMac::NotifyDeviceChanged(
535 base::SystemMonitor::DeviceType type) {
536 DCHECK(thread_checker_.CalledOnValidThread());
537 // TODO(xians): Remove the global variable for SystemMonitor.
538 base::SystemMonitor::Get()->ProcessDevicesChanged(type);
541 } // namespace content